From c3ea94ac42bd2785ec144d3007ca4db5f3b95a02 Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Thu, 12 Dec 2019 10:17:21 -0400 Subject: Import ledger_3.1.3-1.debian.tar.xz [dgit import tarball ledger 3.1.3-1 ledger_3.1.3-1.debian.tar.xz] --- README.source | 31 +++++ changelog | 300 +++++++++++++++++++++++++++++++++++++++++++++++++ control | 38 +++++++ copyright | 139 +++++++++++++++++++++++ dirs | 3 + docs | 1 + elpa-ledger.elpa | 1 + examples | 2 + get-orig-source.sh | 7 ++ ledger.dirs | 3 + ledger.install | 3 + ledger.maintscript | 1 + python3-ledger.install | 1 + rules | 19 ++++ source/format | 1 + watch | 3 + 16 files changed, 553 insertions(+) create mode 100644 README.source create mode 100644 changelog create mode 100644 control create mode 100644 copyright create mode 100644 dirs create mode 100644 docs create mode 100644 elpa-ledger.elpa create mode 100644 examples create mode 100644 get-orig-source.sh create mode 100644 ledger.dirs create mode 100644 ledger.install create mode 100644 ledger.maintscript create mode 100644 python3-ledger.install create mode 100755 rules create mode 100644 source/format create mode 100644 watch diff --git a/README.source b/README.source new file mode 100644 index 00000000..26571c17 --- /dev/null +++ b/README.source @@ -0,0 +1,31 @@ + +1) Getting the git repo setup. + + Upstream uses a git submodule for lib/utfcpp, so we do to. After + you get the repo cloned + + % git submodule init + % git submodule update + +2) Making a snapshot tarball + + The process of making a snapshot tarball from the branch 'upstream' + is automated via + + % ./debian/rules get-orig-source + +3) Updating the changelog. + + Of course you can do this by hand, but + + % ./debian/rules dch-snapshot + + Will start a new entry with an version that matches the tarball created at (2) + +4) Updating the upstream branch + + % git checkout upstream + % git fetch git://github.com/jwiegley/ledger.git + % git merge FETCH_HEAD + + -- David Bremner , Mon, 4 Mar 2013 22:35:06 -0400 diff --git a/changelog b/changelog new file mode 100644 index 00000000..c1a99f6a --- /dev/null +++ b/changelog @@ -0,0 +1,300 @@ +ledger (3.1.3-1) unstable; urgency=medium + + [ David Bremner ] + * New upstream release + * Don't build ledger-dbg (rely on auto generated -dbgsym package). + + [ Dimitri John Ledkov ] + * Finish dh'ification + * Drop python-ledger module, in preparation for python3 + transition. Closes: #945840 + * Drop obsolete inline patches for lisp (no longer exists), docs (using + separate build-dir anyway) and rpath (just shipping the shared library + globally now) + * Fixup shipping private library, privately without headers & dev + symlink. + + -- Dimitri John Ledkov Thu, 12 Dec 2019 14:17:21 +0000 + +ledger (3.1.2+dfsg1-2) unstable; urgency=medium + + * Drop build-depends on dh_elpa, no longer needed. + + -- David Bremner Wed, 06 Feb 2019 08:14:26 -0400 + +ledger (3.1.2+dfsg1-1) unstable; urgency=medium + + [ David Bremner ] + * Update Vcs-Git and Vcs-Browser for the move to salsa + * New upstream release. + * Bug fix: "CVE-2017-2808: Ledger CLI Account Directive Use-After-Free + Vulnerability", thanks to Salvatore Bonaccorso (Closes: #876659). + * Bug fix: "CVE-2017-2807: Ledger CLI Tags Parsing Code Execution + Vulnerability", thanks to Salvatore Bonaccorso (Closes: #876660). + * Bug fix: "CVE-2017-12481 CVE-2017-12482", thanks to Moritz Muehlenhoff + (Closes: #870900). + * Bug fix: "Upgrade to latest upstream git", thanks to Sampo Sorsa + (Closes: #913660). + * Bug fix: "[Mayhem] Bug report on ledger: ledger crashes with exit + status 139", thanks to Alexandre Rebert (Closes: #716086). + * Give up on Gnucash support (Closes: #740757). + * Bug fix: "Payee names containing angle brackets ("<>") + result in corrupti reports", thanks to Anonymous (Closes: #684603). + * Bug fix: "short option -f (for --file) missing in manpage", thanks to + Jonas Meurer (Closes: #839634). + * Drop binary packages elpa-ledger-mode and ledger-el, moving to new + source package ledger-mode. + + [ Ondřej Nový ] + * d/control: Deprecating priority extra as per policy 4.0.1 + * d/rules: Remove trailing whitespaces + + -- David Bremner Wed, 06 Feb 2019 06:48:57 -0400 + +ledger (3.1.2~pre1+g3a00e1c+dfsg1-5) unstable; urgency=medium + + * fix mangled patch exports (patching out deleted files) + + -- David Bremner Mon, 07 Aug 2017 09:21:56 -0400 + +ledger (3.1.2~pre1+g3a00e1c+dfsg1-4) unstable; urgency=medium + + * Add maintscripts to remove obsolete startup files. Bug fix: "emacs + startup provided in wrong package", thanks to bdale@gag.com (Bdale + Garbee); (Closes: #857585) + + -- David Bremner Sat, 05 Aug 2017 09:13:07 -0400 + +ledger (3.1.2~pre1+g3a00e1c+dfsg1-3) unstable; urgency=medium + + * Bug fix: "ledger deb package should suggest ledger-el", thanks to + Thierry (Closes: #843112). + * Swap uploader and maintainer to better direct bug reports + + -- David Bremner Wed, 19 Jul 2017 14:54:13 -0300 + +ledger (3.1.2~pre1+g3a00e1c+dfsg1-2) unstable; urgency=medium + + * convert ledger-el to elpa-ledger (using dh-elpa). Bug fix: "Fails to + install if emacs23 is still installed", thanks to Tollef Fog Heen + (Closes: #839682). + + -- David Bremner Wed, 05 Oct 2016 22:40:14 -0300 + +ledger (3.1.2~pre1+g3a00e1c+dfsg1-1) unstable; urgency=medium + + * New upstream release snapshot + * Cherry-pick upstream pull request 465. Bug fix: "FTBFS on buildds + after gcc6/icu57/boost1.61", thanks to Mattia Rizzolo (Closes: + #833753). + + -- David Bremner Tue, 13 Sep 2016 11:55:15 -0300 + +ledger (3.1.1+dfsg1-1) unstable; urgency=medium + + * New upstream release (Closes: #813780). + * Bug fix: "please add dependency “Suggests: ledger”", thanks to Ben + Finney (Closes: #766593). + * Bug fix: "Debian package does not include upstream commenting-out + keybinding (M-; )", thanks to gwern (Closes: #775440). + + [ Ben Finney ] + + * Update license grant text for ‘lisp/*’ to match upstream. + (Closes: Bug#809929) + + -- David Bremner Sun, 14 Feb 2016 22:28:14 -0400 + +ledger (3.1+dfsg1-7) unstable; urgency=medium + + * Bug fix: "FTBFS when built with dpkg-buildpackage -A (no binary + artifacts)", thanks to Santiago Vila (Closes: #805945). + + -- David Bremner Sat, 28 Nov 2015 13:40:32 -0400 + +ledger (3.1+dfsg1-6) unstable; urgency=medium + + * Add debug package, thanks to Adrian Lang + * Assert compliance with standards version 3.9.6 + + -- David Bremner Tue, 25 Aug 2015 13:10:54 -0300 + +ledger (3.1+dfsg1-5) unstable; urgency=medium + + * Cherry pick upstream commits bcaca2 and a1cb25 to fix FTBFS with boost + 1.58 + * Rebuild with gcc 5 + + -- David Bremner Wed, 12 Aug 2015 10:01:58 +0200 + +ledger (3.1+dfsg1-4) experimental; urgency=medium + + * Add python-ledger binary package (Closes: #704980). + + -- David Bremner Mon, 19 Jan 2015 08:31:23 +0100 + +ledger (3.1+dfsg1-2) unstable; urgency=medium + + * Cherry pick patches for Emacs ledger-mode (ledger-el) from + upstream (Closes: #769454). + - fix for completely broken M-p in Emacs ledger-mode + - fixes for syntax highlightling in ledger-mode + + -- David Bremner Wed, 03 Dec 2014 22:25:08 +0100 + +ledger (3.1+dfsg1-1) unstable; urgency=medium + + * New upstream release + + -- David Bremner Wed, 08 Oct 2014 19:20:38 +0200 + +ledger (3.0.0+dfsg4-2) unstable; urgency=high + + * Disable rpath for libledger.so (Closes: #760110). + + -- David Bremner Sun, 31 Aug 2014 17:54:00 -0700 + +ledger (3.0.0+dfsg4-1) unstable; urgency=medium + + * Don't embed libutfcpp + + -- David Bremner Thu, 28 Aug 2014 19:25:49 -0700 + +ledger (3.0.0+dfsg3-1) unstable; urgency=medium + + * Depend on default boost version + * Bug fix: "ledger-3 bundles non-dfsg software", thanks to + j@jamielinux.com; (Closes: #741740). The bundled sha1 + implementation is replaced by one from boost. + * Bug fix: "missing licenses in debian/copyright", thanks to Thorsten + Alteholz (Closes: #741246). + + -- David Bremner Sun, 13 Apr 2014 17:52:27 -0300 + +ledger (3.0.0+dfsg1-1) unstable; urgency=medium + + * New upstream tag v3.0. Only doc/test changes since last snapshot. + + -- David Bremner Thu, 13 Mar 2014 08:30:06 -0300 + +ledger (3.0.0~20140226+bedae81+dfsg1-2) unstable; urgency=low + + * Split emacs interface to separate binary package. Install in a (more) + debian-emacs-policy compliant way. + + -- David Bremner Mon, 03 Mar 2014 10:59:11 -0400 + +ledger (3.0.0~20140226+bedae81+dfsg1-1) unstable; urgency=low + + * Updated git snapshot, by request to unstable (Closes: #604116) + * Remove minified javascript files without corresponding source. These + are in the python bindings, so there should currently be no user + visible effects (Closes: #736738). + + -- David Bremner Sun, 02 Mar 2014 19:52:43 -0400 + +ledger (3.0.0~20130313+b608ed2-1) experimental; urgency=low + + * Updated git snapshot for experimental. + * Remove special handling for timeclock.el, no longer shipped upstream. + + -- David Bremner Sat, 16 Mar 2013 09:23:03 -0300 + +ledger (3.0.0~20130302+4810da9-1) experimental; urgency=low + + * Updated git snapshot for experimental (Closes: #701086). + * Build system switched (upstream) to CMake. + * Added trivial build-arch and build-indep targets + * No longer install /usr/share/doc/ledger/examples/tools; not of + interest to end-users. + * Add hardening flags + * Update Homepage, thanks to Ryan Kavanagh (Closes: #698239). + * Add Vcs-Browser and Vcs-Git fields. + * Use new emacs ledger mode (ldg-new.el) by default. + * Link with -Wl,--as-needed to avoid depending on 2 extra boost libs. + + -- David Bremner Tue, 05 Mar 2013 14:34:20 -0400 + +ledger (3.0.0~20120421+7cc550f-1) experimental; urgency=low + + * Updated git snapshot build for experimental. + * Bumped Standards-Version to 3.9.3; no changes required. + * Update debhelper compat to 8. + + -- Matt Palmer Sat, 21 Apr 2012 19:43:07 +1000 + +ledger (3.0.0~20100623+eda6cbd-1) experimental; urgency=low + + * Git snapshot build for experimental, to give people a look at ledger + 3.0, and bed down the packaging. Closes: #574090. + - Switch to 3.0 (quilt) format, for shits 'n giggles. + + -- Matt Palmer Fri, 10 Dec 2010 13:48:00 +1100 + +ledger (2.6.2-3) unstable; urgency=low + + * Ack NMU from Margarita Manterola, fixing #576660. + * Apply patch to ledger.el from David Maus to fix some sort of reconciling + buffer problem. Closes: #605429. + * Update docs to clarify that options in ~/.ledgerrc must be placed on + separate lines. Closes: #605810. + * Update docs to remove unwarranted optimism about the --file option + (It doesn't take a colon-separated list of files, and you can't specify + it more than once). Closes: #605807. + * Bump standards version; no changes required. + + -- Matt Palmer Fri, 10 Dec 2010 12:41:20 +1100 + +ledger (2.6.2-2.1) unstable; urgency=low + + * Non-maintainer upload to fix generation of unneeded dir.gz info file. + * Changed debian/rules to remove /usr/share/info/dir before building the + package. Closes: #576660. + + -- Margarita Manterola Sat, 24 Apr 2010 19:06:01 +0000 + +ledger (2.6.2-2) unstable; urgency=low + + * CONSTipate gnucash.cc, textual.cc, and option.cc, so it builds with GCC + 4.4. Thanks to Stefan Ebner for the patch. Closes: #547303. + * Install a man page (this will become redundant once 3.0 comes out, but + until then...). Thanks to Carl Worth for the manpage. Closes: #540409. + * Install emacs mode for ledger (ledger.el); untested, as I have no emacs + fu, but here's hoping... Thanks to Kevin Ryde for the patch. + Closes: #554040. + * Bumped standards version; no changes required. + + -- Matt Palmer Sat, 26 Dec 2009 10:38:50 +1100 + +ledger (2.6.2-1) unstable; urgency=low + + * New upstream release. + + -- Matt Palmer Tue, 30 Jun 2009 18:47:07 +1000 + +ledger (2.6.1-3) unstable; urgency=low + + * Put sample.dat and the scripts directory in examples. Closes: #524976. + * Build and install the HTML docs as well. Just Because. + + -- Matt Palmer Tue, 28 Apr 2009 19:52:52 +1000 + +ledger (2.6.1-2) unstable; urgency=low + + * Update README to point to new mailing list. Closes: #513414 + * Build and install the PDF documentation. Closes: #513416 + + -- Matt Palmer Sat, 31 Jan 2009 15:04:13 +1100 + +ledger (2.6.1-1) unstable; urgency=low + + * New upstream release. + + -- Matt Palmer Thu, 18 Sep 2008 10:11:27 +1000 + +ledger (2.6.0.90-1) unstable; urgency=low + + * Initial release (Closes: #405011) + + -- Matt Palmer Tue, 10 Jun 2008 18:44:40 +1000 + diff --git a/control b/control new file mode 100644 index 00000000..c58ba6a6 --- /dev/null +++ b/control @@ -0,0 +1,38 @@ +Source: ledger +Section: utils +Priority: optional +Maintainer: David Bremner +Uploaders: Matt Palmer +Build-Depends: debhelper-compat (= 12), cmake, libgmp3-dev, libpcre3-dev, + libexpat1-dev, libofx-dev, libmpfr-dev, libboost-regex-dev, + libboost-date-time-dev, libboost-filesystem-dev, + libboost-iostreams-dev, libboost-serialization-dev, + libboost-test-dev, man2html-base, python, python3-dev, texinfo, texlive, + libutfcpp-dev, libboost-python-dev, dh-python +Standards-Version: 4.4.1 +Homepage: http://ledger-cli.org +Vcs-Git: https://salsa.debian.org/debian/ledger.git +Vcs-Browser: https://salsa.debian.org/debian/ledger + +Package: ledger +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Suggests: elpa-ledger, +#python3-ledger +Description: command-line double-entry accounting program + Ledger is a powerful and flexible double-entry accounting system run + entirely from the command line. Your accounts ledger is stored in one or + more plain-text files with a very simple and readable format, and ledger + does the hard work of balancing your books and reporting. + +#Package: python3-ledger +#Architecture: any +#Section: python +#Depends: ${misc:Depends}, ${python3:Depends}, ${shlibs:Depends} +#Description: command-line double-entry accounting program (python3 extension) +# Ledger is a powerful and flexible double-entry accounting system run +# entirely from the command line. Your accounts ledger is stored in one or +# more plain-text files with a very simple and readable format, and ledger +# does the hard work of balancing your books and reporting. +# . +# This package provides the python3 module diff --git a/copyright b/copyright new file mode 100644 index 00000000..f3fff1cd --- /dev/null +++ b/copyright @@ -0,0 +1,139 @@ +This package was debianized by Matt Palmer on Tue, 10 +Jun 2008 18:44:40 +1000. + +It was downloaded from + + +Upstream Author: + + John Wiegley + +Copyright: + + Copyright (c) 2003-2013, John Wiegley. + +License: + + 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. + +The Debian packaging is (C) 2008 Matt Palmer and +is licensed under the GPL, see `/usr/share/common-licenses/GPL'. + + +Files: lisp/* + + 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., 51 Franklin Street, Fifth Floor, Boston, + MA 02110-1301 USA. + +Files: contrib/compilation-ledger.el + + compilation-ledger.el 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 3, or (at your + option) any later version. + + compilation-ledger.el 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 can get a copy of the GNU General Public License online at + . + +Files: src/strptime.cc + + Copyright 2009 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Files: src/wcwidth.cc + + Markus Kuhn -- 2007-05-26 (Unicode 5.0) + + Permission to use, copy, modify, and distribute this software + for any purpose and without fee is hereby granted. The author + disclaims all warranties with regard to this software. + + Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c + +Files: contrib/non-profit-audit-reports/* +Copyright: Tom Marble and Bradley Kugn, 2011-2013 + Contents under contrib/non-profit-audit-reports/ are licensed GPLv3-or-later. + + The GPLv3-or-later licensing of the contents of this directory does not, + to our knowledge and belief, impact the licensing of any other part of + Ledger. Parts of the files herein are likely derivative works of the rest + of Ledger, but these works are under this subdirectory are not, to our + knowledge, used, imported, included, copied, etc. into other parts of the + codebase. + + In short, this is a small application written to use Ledger like a + library, particularly via its Python API interface. It derives from + Ledger, but Ledger does not, to our knowledge, derive from it. + + We are not lawyers and this is not legal advice. + +Files: contrib/non-profit-audit-reports/ooolib2/* +Copyright: 2006-2009 Joseph Colton + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + diff --git a/dirs b/dirs new file mode 100644 index 00000000..a4d79c37 --- /dev/null +++ b/dirs @@ -0,0 +1,3 @@ +usr/bin +usr/share/emacs/site-lisp/ledger-el +usr/share/doc/ledger diff --git a/docs b/docs new file mode 100644 index 00000000..74d32467 --- /dev/null +++ b/docs @@ -0,0 +1 @@ +doc/NEWS.md diff --git a/elpa-ledger.elpa b/elpa-ledger.elpa new file mode 100644 index 00000000..2c3df1d4 --- /dev/null +++ b/elpa-ledger.elpa @@ -0,0 +1 @@ +lisp/*.el diff --git a/examples b/examples new file mode 100644 index 00000000..734b2c9c --- /dev/null +++ b/examples @@ -0,0 +1,2 @@ +test/input/sample.dat + diff --git a/get-orig-source.sh b/get-orig-source.sh new file mode 100644 index 00000000..a529b3a7 --- /dev/null +++ b/get-orig-source.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +UPSTREAM_VERSION=$1 +UPSTREAM_HASH=$2 + +git archive --prefix=ledger-${UPSTREAM_VERSION}/ ${UPSTREAM_HASH} | gzip -9 > ../ledger_${UPSTREAM_VERSION}.orig.tar.gz + diff --git a/ledger.dirs b/ledger.dirs new file mode 100644 index 00000000..343a75aa --- /dev/null +++ b/ledger.dirs @@ -0,0 +1,3 @@ +usr/lib/ledger +usr/share/info +usr/share/doc/ledger diff --git a/ledger.install b/ledger.install new file mode 100644 index 00000000..66457999 --- /dev/null +++ b/ledger.install @@ -0,0 +1,3 @@ +#usr/lib/*/libledger*.so* usr/lib/ledger +#usr/bin/ledger + diff --git a/ledger.maintscript b/ledger.maintscript new file mode 100644 index 00000000..0d4fc756 --- /dev/null +++ b/ledger.maintscript @@ -0,0 +1 @@ +rm_conffile /etc/emacs/site-start.d/50ledger.el diff --git a/python3-ledger.install b/python3-ledger.install new file mode 100644 index 00000000..607c0659 --- /dev/null +++ b/python3-ledger.install @@ -0,0 +1 @@ +usr/lib/python* diff --git a/rules b/rules new file mode 100755 index 00000000..ee94e816 --- /dev/null +++ b/rules @@ -0,0 +1,19 @@ +#!/usr/bin/make -f +%: + dh $@ + +override_dh_auto_configure: + dh_auto_configure -- -DCMAKE_INSTALL_LIBDIR=lib/ledger -DBUILD_DOCS=1 -DBUILD_WEB_DOCS=1 #-DUSE_PYTHON=ON + +override_dh_auto_build: + dh_auto_build -- all doc + +override_dh_auto_install: + # do not care about shipping convenience library + # as a public one, it's for private eyes only + dh_auto_install + rm -rf debian/*/usr/include/ + rm -rf debian/*/usr/lib/ledger/libledger.so + +override_dh_compress: + dh_compress -X.pdf diff --git a/source/format b/source/format new file mode 100644 index 00000000..163aaf8d --- /dev/null +++ b/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/watch b/watch new file mode 100644 index 00000000..61d2d271 --- /dev/null +++ b/watch @@ -0,0 +1,3 @@ +version=3 +opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/ledger-$1\.tar\.gz/ \ + https://github.com/ledger/ledger/tags .*/v?(\d\S*)\.tar\.gz -- cgit v1.2.3 From af6489afb69949ea706b6c3a1c1fedfd0fb0ee36 Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Thu, 12 Dec 2019 10:17:21 -0400 Subject: Import ledger_3.1.3.orig.tar.gz [dgit import orig ledger_3.1.3.orig.tar.gz] --- .dir-locals.el | 36 + .gitignore | 112 + .travis.yml | 111 + CMakeLists.txt | 331 + CONTRIBUTING.md | 138 + INSTALL.md | 175 + LICENSE.md | 28 + README.md | 184 + acprep | 1134 ++ appveyor.yml | 50 + cmake/FindUtfcpp.cmake | 31 + contrib/CSVReader.cs | 165 + contrib/Makefile | 4 + contrib/ParseCcStmt.cs | 184 + contrib/README | 4 + contrib/bal | 21 + contrib/bal-huquq | 21 + contrib/compilation-ledger.el | 97 + contrib/entry | 16 + contrib/getquote-uk.py | 23 + contrib/getquote.pl | 19 + contrib/iso4127-commodities/iso4217ledger.sh | 54 + contrib/iso4127-commodities/iso4217ledger.xsl | 138 + contrib/ledger-completion.bash | 91 + contrib/ledger-du | 49 + contrib/non-profit-audit-reports/GPLv3 | 674 ++ contrib/non-profit-audit-reports/LICENSE | 14 + contrib/non-profit-audit-reports/README | 100 + .../bank-reconcilation.plx | 232 + .../cash-receipts-and-disbursments-journals.plx | 203 + contrib/non-profit-audit-reports/csv2ods.py | 233 + contrib/non-profit-audit-reports/demo.sh | 45 + contrib/non-profit-audit-reports/fund-report.plx | 235 + .../general-ledger-report.plx | 224 + .../non-profit-audit-reports/ooolib2/__init__.py | 1987 ++++ contrib/non-profit-audit-reports/readcsv.py | 31 + .../non-profit-audit-reports/summary-reports.plx | 465 + .../tests/Financial/BankStuff/bank-statement.pdf | Bin 0 -> 3257 bytes .../tests/Financial/Invoices/Invoice20110510.pdf | Bin 0 -> 13890 bytes .../tests/Financial/Invoices/Invoice20110510.txt | 5 + .../Blah/Expenses/hosting/AprilHostingReceipt.pdf | 106 + .../Blah/Expenses/hosting/april-invoice.pdf | Bin 0 -> 3153 bytes .../Foo/Expenses/hosting/AprilHostingReceipt.pdf | Bin 0 -> 14813 bytes .../Foo/Expenses/hosting/AprilHostingReceipt.txt | 6 + .../Projects/Foo/Invoices/Invoice20100101.pdf | Bin 0 -> 14926 bytes .../tests/Projects/Foo/earmark-record.txt | 1 + .../tests/non-profit-test-data.ledger | 28 + .../tests/non-profit-test-data_MANIFEST | 10 + .../non-profit-test-data_chart-of-accounts.csv | 6 + .../tests/non-profit-test-data_general-ledger.ods | Bin 0 -> 5770 bytes .../unpaid-accruals-report.plx | 110 + contrib/raw/GenerateLatexExpeneseReport.pl | 429 + contrib/raw/MetadataExample.dat | 111 + contrib/raw/README | 5 + contrib/raw/VerifyImages.sh | 14 + contrib/raw/dotemacs.el | 201 + contrib/raw/ledger-matching.el | 342 + contrib/raw/ledger-shell-environment-functions | 90 + contrib/repl.sh | 13 + contrib/report | 21 + contrib/tc | 7 + contrib/ti | 5 + contrib/to | 3 + contrib/trend | 30 + default.nix | 49 + doc/CMakeLists.txt | 138 + doc/Doxyfile.in | 1877 ++++ doc/GLOSSARY.md | 151 + doc/NEWS.md | 1045 ++ doc/grammar.y | 221 + doc/ledger.1 | 1467 +++ doc/ledger3.texi | 10403 +++++++++++++++++++ doc/version.texi.in | 8 + lib/Makefile | 56 + lib/build-gcc.sh | 16 + lib/build-icc.sh | 17 + lib/build.sh | 17 + lib/utfcpp/v2_0/LICENSE | 23 + lib/utfcpp/v2_0/buildrelease.pl | 18 + lib/utfcpp/v2_0/samples/Makefile | 5 + lib/utfcpp/v2_0/samples/docsample.cpp | 52 + lib/utfcpp/v2_0/source/utf8.h | 34 + lib/utfcpp/v2_0/source/utf8/checked.h | 327 + lib/utfcpp/v2_0/source/utf8/core.h | 329 + lib/utfcpp/v2_0/source/utf8/unchecked.h | 228 + python/__init__.py | 0 python/demo.py | 293 + python/res/asc.gif | Bin 0 -> 54 bytes python/res/bg.gif | Bin 0 -> 64 bytes python/res/desc.gif | Bin 0 -> 54 bytes python/res/icons/first.png | Bin 0 -> 720 bytes python/res/icons/last.png | Bin 0 -> 737 bytes python/res/icons/next.png | Bin 0 -> 736 bytes python/res/icons/prev.png | Bin 0 -> 745 bytes python/res/jquery-latest.js | 32 + python/res/jquery.dimensions.min.js | 12 + python/res/jquery.metadata.js | 122 + python/res/jquery.tablesorter.min.js | 2 + python/res/jquery.tablesorter.pager.css | 25 + python/res/jquery.tablesorter.pager.js | 184 + python/res/style.css | 39 + python/server.py | 196 + src/CMakeLists.txt | 334 + src/account.cc | 779 ++ src/account.h | 307 + src/amount.cc | 1324 +++ src/amount.h | 790 ++ src/annotate.cc | 375 + src/annotate.h | 236 + src/balance.cc | 348 + src/balance.h | 608 ++ src/chain.cc | 268 + src/chain.h | 116 + src/commodity.cc | 571 + src/commodity.h | 303 + src/compare.cc | 132 + src/compare.h | 100 + src/context.h | 162 + src/convert.cc | 140 + src/convert.h | 55 + src/csv.cc | 289 + src/csv.h | 120 + src/draft.cc | 532 + src/draft.h | 132 + src/emacs.cc | 121 + src/emacs.h | 80 + src/error.cc | 117 + src/error.h | 105 + src/expr.cc | 319 + src/expr.h | 188 + src/exprbase.h | 245 + src/filters.cc | 1539 +++ src/filters.h | 1076 ++ src/flags.h | 187 + src/format.cc | 698 ++ src/format.h | 167 + src/generate.cc | 393 + src/generate.h | 130 + src/global.cc | 524 + src/global.h | 177 + src/history.cc | 599 ++ src/history.h | 96 + src/item.cc | 616 ++ src/item.h | 218 + src/iterators.cc | 274 + src/iterators.h | 329 + src/journal.cc | 580 ++ src/journal.h | 207 + src/lookup.cc | 287 + src/lookup.h | 57 + src/main.cc | 224 + src/mask.cc | 88 + src/mask.h | 140 + src/op.cc | 982 ++ src/op.h | 351 + src/option.cc | 253 + src/option.h | 301 + src/output.cc | 417 + src/output.h | 251 + src/parser.cc | 593 ++ src/parser.h | 126 + src/pool.cc | 393 + src/pool.h | 139 + src/post.cc | 756 ++ src/post.h | 262 + src/precmd.cc | 209 + src/precmd.h | 59 + src/predicate.h | 97 + src/print.cc | 334 + src/print.h | 91 + src/pstream.h | 114 + src/ptree.cc | 108 + src/ptree.h | 103 + src/py_account.cc | 260 + src/py_amount.cc | 322 + src/py_balance.cc | 244 + src/py_commodity.cc | 457 + src/py_expr.cc | 68 + src/py_format.cc | 68 + src/py_item.cc | 184 + src/py_journal.cc | 340 + src/py_post.cc | 195 + src/py_session.cc | 78 + src/py_times.cc | 253 + src/py_utils.cc | 255 + src/py_value.cc | 383 + src/py_xact.cc | 164 + src/pyfstream.h | 202 + src/pyinterp.cc | 601 ++ src/pyinterp.h | 150 + src/pyledger.cc | 52 + src/pyutils.h | 187 + src/query.cc | 561 + src/query.h | 353 + src/quotes.cc | 106 + src/quotes.h | 53 + src/report.cc | 1774 ++++ src/report.h | 1098 ++ src/scope.cc | 97 + src/scope.h | 620 ++ src/select.cc | 439 + src/select.h | 55 + src/session.cc | 425 + src/session.h | 195 + src/stats.cc | 119 + src/stats.h | 55 + src/stream.cc | 132 + src/stream.h | 142 + src/strptime.cc | 205 + src/strptime.h | 6 + src/system.hh.in | 230 + src/temps.cc | 146 + src/temps.h | 85 + src/textual.cc | 2022 ++++ src/timelog.cc | 199 + src/timelog.h | 111 + src/times.cc | 1783 ++++ src/times.h | 536 + src/token.cc | 611 ++ src/token.h | 138 + src/unistring.h | 159 + src/utils.cc | 856 ++ src/utils.h | 641 ++ src/value.cc | 2118 ++++ src/value.h | 1001 ++ src/views.cc | 259 + src/views.h | 457 + src/wcwidth.c.patch | 74 + src/wcwidth.cc | 319 + src/xact.cc | 859 ++ src/xact.h | 239 + test/CMakeLists.txt | 63 + test/CheckBaselineTests.py | 77 + test/CheckComments.py | 121 + test/CheckManpage.py | 45 + test/CheckOptions.py | 98 + test/CheckTexinfo.py | 116 + test/ConfirmTests.py | 94 + test/DocTests.py | 268 + test/GenerateTests.py | 154 + test/LedgerHarness.py | 154 + test/PyUnitTests.py | 13 + test/RegressTests.py | 231 + test/baseline/CMakeLists.txt | 1 + test/baseline/cmd-accounts.test | 38 + test/baseline/cmd-balance.test | 85 + test/baseline/cmd-budget.test | 65 + test/baseline/cmd-cleared.test | 36 + test/baseline/cmd-commodities.test | 30 + test/baseline/cmd-convert.test | 35 + test/baseline/cmd-convert1.dat | 3 + test/baseline/cmd-convert2.dat | 2 + test/baseline/cmd-convert3.dat | 2 + test/baseline/cmd-convert4.dat | 2 + test/baseline/cmd-csv.test | 53 + test/baseline/cmd-echo.test | 12 + test/baseline/cmd-entry.test | 43 + test/baseline/cmd-equity.test | 58 + test/baseline/cmd-payees.test | 48 + test/baseline/cmd-pricedb.test | 39 + test/baseline/cmd-pricemap.test | 36 + test/baseline/cmd-prices.test | 39 + test/baseline/cmd-print.test | 9 + test/baseline/cmd-register.test | 40 + test/baseline/cmd-script.test | 21 + test/baseline/cmd-select.test | 62 + test/baseline/cmd-source.test | 64 + test/baseline/cmd-stats.test | 31 + test/baseline/cmd-tags.test | 58 + test/baseline/cmd-xact.test | 43 + test/baseline/dir-account.test | 43 + test/baseline/dir-alias-fail.test | 10 + test/baseline/dir-alias.test | 22 + test/baseline/dir-apply.dat | 3 + test/baseline/dir-apply.test | 34 + test/baseline/dir-commodity-alias.test | 23 + test/baseline/dir-commodity-value.test | 24 + test/baseline/dir-commodity.test | 21 + test/baseline/dir-fixed.test | 12 + test/baseline/dir-import_py.test | 23 + test/baseline/dir-payee.test | 22 + test/baseline/dir-python_py.test | 28 + test/baseline/dir-tag.test | 21 + test/baseline/feat-annotations.test | 37 + test/baseline/feat-balance-assignments.test | 35 + test/baseline/feat-balance_assert-off.test | 18 + test/baseline/feat-balance_assert.test | 13 + test/baseline/feat-balance_assert_split.test | 60 + test/baseline/feat-check.test | 18 + test/baseline/feat-convert-with-directives.dat | 3 + test/baseline/feat-convert-with-directives.test | 28 + test/baseline/feat-fixated-prices.test | 24 + test/baseline/feat-import_py.test | 23 + test/baseline/feat-option_py.test | 14 + test/baseline/feat-value-expr.test | 99 + test/baseline/feat-value_py.test | 23 + test/baseline/featimport.py | 4 + test/baseline/featoption.py | 5 + test/baseline/opt-abbrev-len.test | 9 + test/baseline/opt-account-width.test | 9 + test/baseline/opt-account.test | 9 + test/baseline/opt-actual.test | 13 + test/baseline/opt-add-budget.test | 366 + test/baseline/opt-amount-data.test | 8 + test/baseline/opt-amount-width.test | 9 + test/baseline/opt-amount.test | 8 + test/baseline/opt-auto-match.dat | 4 + test/baseline/opt-auto-match.test | 71 + test/baseline/opt-aux-date.test | 20 + test/baseline/opt-average.test | 242 + test/baseline/opt-balance-format.test | 8 + test/baseline/opt-base.test | 26 + test/baseline/opt-basis.test | 10 + test/baseline/opt-begin.test | 238 + test/baseline/opt-bold-if.test | 16 + test/baseline/opt-budget-format.test | 21 + test/baseline/opt-budget.test | 319 + test/baseline/opt-budget_only.test | 22 + test/baseline/opt-budget_range.test | 111 + test/baseline/opt-by-payee.test | 218 + test/baseline/opt-check-payees.test | 37 + test/baseline/opt-cleared-format.test | 20 + test/baseline/opt-cleared.test | 204 + test/baseline/opt-code-as-account.test | 32 + test/baseline/opt-code-as-payee.test | 32 + test/baseline/opt-collapse-if-zero.test | 13 + test/baseline/opt-collapse.test | 11 + test/baseline/opt-collapse_reg.test | 44 + test/baseline/opt-color.test | 11 + test/baseline/opt-columns.test | 9 + test/baseline/opt-commodity-as-account.test | 9 + test/baseline/opt-commodity-as-payee.test | 9 + test/baseline/opt-cost.test | 10 + test/baseline/opt-count.test | 43 + test/baseline/opt-csv-format.test | 8 + test/baseline/opt-current.test | 15 + test/baseline/opt-daily.test | 54 + test/baseline/opt-date-format.test | 9 + test/baseline/opt-date-width.test | 9 + test/baseline/opt-date.test | 50 + test/baseline/opt-datetime-format.test | 22 + test/baseline/opt-day-break.test | 12 + test/baseline/opt-dc.test | 38 + test/baseline/opt-decimal-comma.test | 22 + test/baseline/opt-depth.test | 87 + test/baseline/opt-depth_flat.test | 62 + test/baseline/opt-deviation.test | 242 + test/baseline/opt-display-amount.test | 63 + test/baseline/opt-display-total.test | 63 + test/baseline/opt-display.test | 57 + test/baseline/opt-dow.test | 53 + test/baseline/opt-empty.test | 34 + test/baseline/opt-empty_bal.test | 39 + test/baseline/opt-empty_bal_flat.test | 33 + test/baseline/opt-empty_collapse.test | 26 + test/baseline/opt-end.test | 246 + test/baseline/opt-equity.test | 49 + test/baseline/opt-exact.test | 223 + test/baseline/opt-exchange.test | 136 + test/baseline/opt-explicit.test | 36 + test/baseline/opt-file.test | 12 + test/baseline/opt-file1.dat | 4 + test/baseline/opt-file2.dat | 4 + test/baseline/opt-flat.test | 32 + test/baseline/opt-force-color.test | 19 + test/baseline/opt-forecast-while.test | 283 + test/baseline/opt-forecast-years.test | 202 + test/baseline/opt-forecast_only.test | 64 + test/baseline/opt-format.test | 8 + test/baseline/opt-gain.test | 73 + test/baseline/opt-group-by.test | 114 + test/baseline/opt-group-title-format.test | 48 + test/baseline/opt-head.test | 218 + test/baseline/opt-historical.test | 250 + test/baseline/opt-immediate.test | 32 + test/baseline/opt-init-file.dat | 1 + test/baseline/opt-init-file.test | 10 + test/baseline/opt-inject.test | 10 + test/baseline/opt-input-date-format.test | 9 + test/baseline/opt-invert.test | 9 + test/baseline/opt-limit.test | 210 + test/baseline/opt-lot-dates.test | 4163 ++++++++ test/baseline/opt-lot-notes.test | 31 + test/baseline/opt-lot-prices.test | 2036 ++++ test/baseline/opt-lot-tags.test | 10 + test/baseline/opt-lots-actual.test | 22 + test/baseline/opt-lots.test | 5243 ++++++++++ test/baseline/opt-lots_basis.test | 874 ++ test/baseline/opt-lots_basis_base.test | 875 ++ test/baseline/opt-market.test | 64 + test/baseline/opt-master-account.test | 13 + test/baseline/opt-meta-width.test | 14 + test/baseline/opt-meta.test | 14 + test/baseline/opt-monthly.test | 218 + test/baseline/opt-no-aliases.test | 20 + test/baseline/opt-no-revalued.test | 58 + test/baseline/opt-no-rounding.test | 81 + test/baseline/opt-no-titles.test | 10 + test/baseline/opt-no-total.test | 8 + test/baseline/opt-now.test | 4 + test/baseline/opt-only.test | 232 + test/baseline/opt-output.test | 10 + test/baseline/opt-pager.test | 9 + test/baseline/opt-payee-as-account.test | 32 + test/baseline/opt-payee-width.test | 9 + test/baseline/opt-payee.test | 9 + test/baseline/opt-pedantic.test | 17 + test/baseline/opt-pending.test | 198 + test/baseline/opt-percent.test | 92 + test/baseline/opt-period.test | 284 + test/baseline/opt-permissive.test | 19 + test/baseline/opt-pivot.test | 148 + test/baseline/opt-plot-amount-format.test | 8 + test/baseline/opt-plot-total-format.test | 8 + test/baseline/opt-prepend-format.test | 22 + test/baseline/opt-prepend-width.test | 22 + test/baseline/opt-price-db.dat | 2 + test/baseline/opt-price-db.test | 8 + test/baseline/opt-price.test | 42 + test/baseline/opt-pricedb-format.test | 19 + test/baseline/opt-prices-format.test | 19 + test/baseline/opt-primary-date.test | 99 + test/baseline/opt-quantity.test | 9 + test/baseline/opt-quarterly.test | 202 + test/baseline/opt-raw.test | 9 + test/baseline/opt-real.test | 9 + test/baseline/opt-recursive-aliases.test | 12 + test/baseline/opt-register-format.test | 8 + test/baseline/opt-related-all.test | 12 + test/baseline/opt-related.test | 11 + test/baseline/opt-revalued-only.test | 57 + test/baseline/opt-revalued.test | 64 + test/baseline/opt-rich-data.test | 42 + test/baseline/opt-script.dat | 3 + test/baseline/opt-script.test | 15 + test/baseline/opt-seed.test.save | 432 + test/baseline/opt-sort-all.test | 119 + test/baseline/opt-sort-xacts.test | 173 + test/baseline/opt-sort.test | 237 + test/baseline/opt-start-of-week.test | 222 + test/baseline/opt-strict.test | 22 + test/baseline/opt-subtotal.test | 91 + test/baseline/opt-tail.test | 218 + test/baseline/opt-time-colon.test | 45 + test/baseline/opt-time-report.test | 44 + test/baseline/opt-total-data.test | 8 + test/baseline/opt-total-width.test | 9 + test/baseline/opt-total.test | 8 + test/baseline/opt-trace.test | 15 + test/baseline/opt-truncate.test | 140 + test/baseline/opt-unbudgeted.test | 294 + test/baseline/opt-uncleared.test | 284 + test/baseline/opt-unrealized-gains.test | 18 + test/baseline/opt-unrealized-losses.test | 18 + test/baseline/opt-unrealized.test | 26 + test/baseline/opt-unround.test | 92 + test/baseline/opt-value-expr.test | 48 + test/baseline/opt-values.test | 6 + test/baseline/opt-weekly.test | 223 + test/baseline/opt-weekly_empty.test | 299 + test/baseline/opt-wide.test | 9 + test/baseline/opt-yearly.test | 196 + test/convert.py | 211 + test/fullcheck.sh | 21 + test/input/demo.ledger | 63 + test/input/divzero.dat | 11 + test/input/drewr.dat | 63 + test/input/drewr3.dat | 72 + test/input/sample.dat | 56 + test/input/standard.dat | 5619 ++++++++++ test/input/transfer.dat | 220 + test/input/wow.dat | 550 + test/manual/CMakeLists.txt | 1 + test/manual/transaction-codes-1.test | 20 + test/manual/transaction-codes-2.test | 22 + test/manual/transaction-notes-1.test | 22 + test/manual/transaction-notes-2.test | 21 + test/manual/transaction-notes-3.test | 21 + test/manual/transaction-notes-4.test | 25 + test/manual/transaction-status-1.test | 17 + test/manual/transaction-status-2.test | 15 + test/manual/transaction-status-3.test | 16 + test/manual/transaction-status-4.test | 15 + test/python/JournalTest.py | 48 + test/python/PostingTest.py | 25 + test/python/TransactionTest.py | 25 + test/python/UnitTests.py | 12 + test/regress/012ADB60.test | 24 + test/regress/0161EB1E.test | 15 + test/regress/04C5E1CA.test | 16 + test/regress/04D86CD0.test | 11 + test/regress/0CA014F9.test | 11 + test/regress/0DDDEBC0.dat | 3 + test/regress/0DDDEBC0.test | 9 + test/regress/1036.test | 80 + test/regress/1038_1.test | 18 + test/regress/1038_2.test | 18 + test/regress/1038_3.test | 20 + test/regress/1046.test | 35 + test/regress/1050.test | 39 + test/regress/1055.test | 26 + test/regress/1057.test | 11 + test/regress/1072.test | 31 + test/regress/1074.test | 161 + test/regress/10D19C11.test | 37 + test/regress/1102.test | 8 + test/regress/1106.test | 11 + test/regress/1127.test | 15 + test/regress/1132.test | 16 + test/regress/1147-a.test | 12 + test/regress/1147-b.test | 10 + test/regress/1159.test | 29 + test/regress/1182_1.test | 12 + test/regress/1182_2.test | 17 + test/regress/1187_1.test | 19 + test/regress/1187_2.test | 18 + test/regress/1187_3.test | 36 + test/regress/1187_4.test | 32 + test/regress/1187_5.test | 36 + test/regress/1222.test | 7 + test/regress/1224.test | 9 + test/regress/1384C1D8.test | 27 + test/regress/13965214.test | 28 + test/regress/14DB77E7.test | 18 + test/regress/15230B79.test | 12 + test/regress/15A80F68.test | 15 + test/regress/1626.test | 28 + test/regress/1702.test | 17 + test/regress/1703.test | 11 + test/regress/1722.test | 12 + test/regress/1723.test | 5 + test/regress/1753.test | 12 + test/regress/178501DC.test | 21 + test/regress/1A546C4D.test | 13 + test/regress/1CF1EEC2.test | 25 + test/regress/1D275740.test | 175 + test/regress/1E192DF6.test | 57 + test/regress/25A099C9.dat | 793 ++ test/regress/25A099C9.test | 61 + test/regress/2CE7DADB.test | 13 + test/regress/2E3496BD.test | 19 + test/regress/370-budget_period_days.test | 27 + test/regress/370-budget_period_weeks.test | 29 + test/regress/370-forecast_period_days.test | 17 + test/regress/370-forecast_period_weeks.test | 16 + test/regress/370-period.test | 443 + test/regress/373540CC.test | 7 + test/regress/375.test | 36 + test/regress/383.test | 29 + test/regress/3AAB00ED.test | 23 + test/regress/3AB70168.test | 11 + test/regress/3FE26304.test | 74 + test/regress/4509F714.test | 25 + test/regress/461980A1.test | 11 + test/regress/47C579B8.test | 92 + test/regress/494-a.ledger | 10 + test/regress/494-b.ledger | 17 + test/regress/4D9288AE.dat | 4 + test/regress/4D9288AE.py | 4 + test/regress/4D9288AE_py.test | 3 + test/regress/53BCED29.test | 29 + test/regress/550-584.test | 73 + test/regress/55831A79.test | 24 + test/regress/56BBE69B.test | 17 + test/regress/5A03CFC3.test | 70 + test/regress/5D92A5EB.test | 34 + test/regress/5F1BAF17.test | 128 + test/regress/5FBF2ED8.test | 18 + test/regress/605A410D.test | 32 + test/regress/6188B0EC.test | 10 + test/regress/620F0674.test | 32 + test/regress/640D3205.test | 20 + test/regress/647D5DB9.test | 15 + test/regress/65FECA4D.test | 12 + test/regress/686.test | 33 + test/regress/68917252.dat | 2 + test/regress/68917252.test | 5 + test/regress/6D9066DD.test | 17 + test/regress/6DAB9FE3.test | 8 + test/regress/6E041C52.test | 8 + test/regress/6E7C2DF9.test | 24 + test/regress/712-a.test | 10 + test/regress/712-b.test | 22 + test/regress/713-a.test | 40 + test/regress/713-b.test | 48 + test/regress/727B2DF8.test | 79 + test/regress/730.test | 37 + test/regress/751B2357.test | 17 + test/regress/755.test | 90 + test/regress/785.test | 85 + test/regress/786A3DD0.test | 17 + test/regress/78AB4B87.dat | 14 + test/regress/78AB4B87.py | 27 + test/regress/78AB4B87_py.test | 15 + test/regress/793F6BF0.test | 50 + test/regress/7C44010B.test | 30 + test/regress/7F3650FD.test | 84 + test/regress/8254755E.test | 17 + test/regress/82763D86.test | 35 + test/regress/83B4A0E5.test | 43 + test/regress/854150DF.test | 25 + test/regress/86D2BDC4.test | 10 + test/regress/889BB167.test | 17 + test/regress/89233B6D-a.dat | 4 + test/regress/89233B6D-b.dat | 4 + test/regress/89233B6D.test | 13 + test/regress/8CE88DB4.test | 11 + test/regress/8EAF77C0.test | 17 + test/regress/9188F587.py | 27 + test/regress/9188F587_py.test | 15 + test/regress/95350193.test | 6 + test/regress/96A8E4A1.test | 10 + test/regress/981.test | 29 + test/regress/999-a.test | 6 + test/regress/999-b.test | 22 + test/regress/9E0E606D.test | 19 + test/regress/9EB10714.test | 43 + test/regress/A013A73B.test | 10 + test/regress/A28CF697.test | 9 + test/regress/A3FA7601.dat | 3 + test/regress/A3FA7601.test | 9 + test/regress/A560FDAD.test | 85 + test/regress/A8FCC765.dat | 2 + test/regress/A8FCC765.test | 8 + test/regress/AA2FF2B.test | 10 + test/regress/ACE05ECE.test | 6 + test/regress/AEDE9734.test | 12 + test/regress/AFAFB804.test | 57 + test/regress/B21BF389.py | 9 + test/regress/B21BF389_py.test | 8 + test/regress/B68FFB0D.test | 13 + test/regress/BBFA1759.test | 18 + test/regress/BF3C1F82-2.test | 12 + test/regress/BF3C1F82.test | 19 + test/regress/BFD3FBE1.test | 16 + test/regress/C0212EAC.test | 30 + test/regress/C19E4E9B.test | 18 + test/regress/C523E23F.test | 19 + test/regress/C927CFFE.test | 43 + test/regress/C9D593B3.test | 23 + test/regress/CAE63F5C-a.test | 17 + test/regress/CAE63F5C-b.test | 14 + test/regress/CAE63F5C-c.test | 14 + test/regress/CEECC0B0.test | 19 + test/regress/CFE5D8AA.test | 20 + test/regress/CMakeLists.txt | 1 + test/regress/D060256A.test | 14 + test/regress/D2829FC4.test | 62 + test/regress/D51BFF74.test | 24 + test/regress/D943AE0F.test | 13 + test/regress/D9C8EB08.test | 16 + test/regress/DB490507.test | 25 + test/regress/DDB54BB8.test | 18 + test/regress/DE17CCF1.test | 61 + test/regress/E2E479BC.test | 17 + test/regress/E4C9A8EA.test | 22 + test/regress/E627C594.test | 18 + test/regress/E9F130C5.test | 28 + test/regress/EA18D948.test | 14 + test/regress/F06D5554.test | 552 + test/regress/F524E251.test | 37 + test/regress/F559EC12.test | 41 + test/regress/FCE11C8D.test | 7 + test/regress/FDFBA165.test | 23 + test/regress/GH520.test | 33 + test/regress/error-in-include.dat | 5 + test/regress/error-in-include.test | 17 + .../fix-missing-trans-in-last-budget-period.test | 79 + test/regress/market-group-by.test | 26 + test/regress/total-1.test | 28 + test/regress/total-2.test | 30 + test/regress/xact_code.dat | 3 + test/regress/xact_code.py | 4 + test/regress/xact_code_py.test | 3 + test/unit/CMakeLists.txt | 23 + test/unit/t_amount.cc | 1393 +++ test/unit/t_balance.cc | 528 + test/unit/t_commodity.cc | 115 + test/unit/t_expr.cc | 334 + test/unit/t_times.cc | 169 + test/unit/t_value.cc | 715 ++ tools/average | 26 + tools/build.sh | 16 + tools/cleanup.sh | 5 + tools/excludes | 44 + tools/gendocs.sh | 29 + tools/genuuid | 28 + tools/nix-build.sh | 20 + tools/pre-commit | 69 + tools/prepare-commit-msg | 38 + tools/proof | 63 + tools/push | 39 + tools/rename.sh | 67 + tools/speed-test.sh | 16 + tools/speedcmp | 18 + tools/spellcheck.sh | 3 + tools/times.sh | 6 + tools/update_copyright_year.sh | 36 + 698 files changed, 118981 insertions(+) create mode 100644 .dir-locals.el create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CMakeLists.txt create mode 100644 CONTRIBUTING.md create mode 100644 INSTALL.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 acprep create mode 100644 appveyor.yml create mode 100644 cmake/FindUtfcpp.cmake create mode 100644 contrib/CSVReader.cs create mode 100644 contrib/Makefile create mode 100644 contrib/ParseCcStmt.cs create mode 100644 contrib/README create mode 100755 contrib/bal create mode 100755 contrib/bal-huquq create mode 100644 contrib/compilation-ledger.el create mode 100755 contrib/entry create mode 100755 contrib/getquote-uk.py create mode 100755 contrib/getquote.pl create mode 100755 contrib/iso4127-commodities/iso4217ledger.sh create mode 100644 contrib/iso4127-commodities/iso4217ledger.xsl create mode 100644 contrib/ledger-completion.bash create mode 100755 contrib/ledger-du create mode 100644 contrib/non-profit-audit-reports/GPLv3 create mode 100644 contrib/non-profit-audit-reports/LICENSE create mode 100644 contrib/non-profit-audit-reports/README create mode 100755 contrib/non-profit-audit-reports/bank-reconcilation.plx create mode 100755 contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx create mode 100755 contrib/non-profit-audit-reports/csv2ods.py create mode 100755 contrib/non-profit-audit-reports/demo.sh create mode 100755 contrib/non-profit-audit-reports/fund-report.plx create mode 100755 contrib/non-profit-audit-reports/general-ledger-report.plx create mode 100644 contrib/non-profit-audit-reports/ooolib2/__init__.py create mode 100755 contrib/non-profit-audit-reports/readcsv.py create mode 100755 contrib/non-profit-audit-reports/summary-reports.plx create mode 100644 contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf create mode 100644 contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf create mode 100644 contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt create mode 100644 contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf create mode 100644 contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf create mode 100644 contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf create mode 100644 contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt create mode 100644 contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf create mode 100644 contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt create mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger create mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST create mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv create mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods create mode 100755 contrib/non-profit-audit-reports/unpaid-accruals-report.plx create mode 100755 contrib/raw/GenerateLatexExpeneseReport.pl create mode 100644 contrib/raw/MetadataExample.dat create mode 100644 contrib/raw/README create mode 100755 contrib/raw/VerifyImages.sh create mode 100644 contrib/raw/dotemacs.el create mode 100644 contrib/raw/ledger-matching.el create mode 100644 contrib/raw/ledger-shell-environment-functions create mode 100755 contrib/repl.sh create mode 100755 contrib/report create mode 100755 contrib/tc create mode 100755 contrib/ti create mode 100755 contrib/to create mode 100755 contrib/trend create mode 100644 default.nix create mode 100644 doc/CMakeLists.txt create mode 100644 doc/Doxyfile.in create mode 100644 doc/GLOSSARY.md create mode 100644 doc/NEWS.md create mode 100644 doc/grammar.y create mode 100644 doc/ledger.1 create mode 100644 doc/ledger3.texi create mode 100644 doc/version.texi.in create mode 100644 lib/Makefile create mode 100755 lib/build-gcc.sh create mode 100755 lib/build-icc.sh create mode 100755 lib/build.sh create mode 100644 lib/utfcpp/v2_0/LICENSE create mode 100755 lib/utfcpp/v2_0/buildrelease.pl create mode 100644 lib/utfcpp/v2_0/samples/Makefile create mode 100644 lib/utfcpp/v2_0/samples/docsample.cpp create mode 100644 lib/utfcpp/v2_0/source/utf8.h create mode 100644 lib/utfcpp/v2_0/source/utf8/checked.h create mode 100644 lib/utfcpp/v2_0/source/utf8/core.h create mode 100644 lib/utfcpp/v2_0/source/utf8/unchecked.h create mode 100644 python/__init__.py create mode 100755 python/demo.py create mode 100644 python/res/asc.gif create mode 100644 python/res/bg.gif create mode 100644 python/res/desc.gif create mode 100644 python/res/icons/first.png create mode 100644 python/res/icons/last.png create mode 100644 python/res/icons/next.png create mode 100644 python/res/icons/prev.png create mode 100644 python/res/jquery-latest.js create mode 100644 python/res/jquery.dimensions.min.js create mode 100644 python/res/jquery.metadata.js create mode 100644 python/res/jquery.tablesorter.min.js create mode 100644 python/res/jquery.tablesorter.pager.css create mode 100644 python/res/jquery.tablesorter.pager.js create mode 100644 python/res/style.css create mode 100644 python/server.py create mode 100644 src/CMakeLists.txt create mode 100644 src/account.cc create mode 100644 src/account.h create mode 100644 src/amount.cc create mode 100644 src/amount.h create mode 100644 src/annotate.cc create mode 100644 src/annotate.h create mode 100644 src/balance.cc create mode 100644 src/balance.h create mode 100644 src/chain.cc create mode 100644 src/chain.h create mode 100644 src/commodity.cc create mode 100644 src/commodity.h create mode 100644 src/compare.cc create mode 100644 src/compare.h create mode 100644 src/context.h create mode 100644 src/convert.cc create mode 100644 src/convert.h create mode 100644 src/csv.cc create mode 100644 src/csv.h create mode 100644 src/draft.cc create mode 100644 src/draft.h create mode 100644 src/emacs.cc create mode 100644 src/emacs.h create mode 100644 src/error.cc create mode 100644 src/error.h create mode 100644 src/expr.cc create mode 100644 src/expr.h create mode 100644 src/exprbase.h create mode 100644 src/filters.cc create mode 100644 src/filters.h create mode 100644 src/flags.h create mode 100644 src/format.cc create mode 100644 src/format.h create mode 100644 src/generate.cc create mode 100644 src/generate.h create mode 100644 src/global.cc create mode 100644 src/global.h create mode 100644 src/history.cc create mode 100644 src/history.h create mode 100644 src/item.cc create mode 100644 src/item.h create mode 100644 src/iterators.cc create mode 100644 src/iterators.h create mode 100644 src/journal.cc create mode 100644 src/journal.h create mode 100644 src/lookup.cc create mode 100644 src/lookup.h create mode 100644 src/main.cc create mode 100644 src/mask.cc create mode 100644 src/mask.h create mode 100644 src/op.cc create mode 100644 src/op.h create mode 100644 src/option.cc create mode 100644 src/option.h create mode 100644 src/output.cc create mode 100644 src/output.h create mode 100644 src/parser.cc create mode 100644 src/parser.h create mode 100644 src/pool.cc create mode 100644 src/pool.h create mode 100644 src/post.cc create mode 100644 src/post.h create mode 100644 src/precmd.cc create mode 100644 src/precmd.h create mode 100644 src/predicate.h create mode 100644 src/print.cc create mode 100644 src/print.h create mode 100644 src/pstream.h create mode 100644 src/ptree.cc create mode 100644 src/ptree.h create mode 100644 src/py_account.cc create mode 100644 src/py_amount.cc create mode 100644 src/py_balance.cc create mode 100644 src/py_commodity.cc create mode 100644 src/py_expr.cc create mode 100644 src/py_format.cc create mode 100644 src/py_item.cc create mode 100644 src/py_journal.cc create mode 100644 src/py_post.cc create mode 100644 src/py_session.cc create mode 100644 src/py_times.cc create mode 100644 src/py_utils.cc create mode 100644 src/py_value.cc create mode 100644 src/py_xact.cc create mode 100644 src/pyfstream.h create mode 100644 src/pyinterp.cc create mode 100644 src/pyinterp.h create mode 100644 src/pyledger.cc create mode 100644 src/pyutils.h create mode 100644 src/query.cc create mode 100644 src/query.h create mode 100644 src/quotes.cc create mode 100644 src/quotes.h create mode 100644 src/report.cc create mode 100644 src/report.h create mode 100644 src/scope.cc create mode 100644 src/scope.h create mode 100644 src/select.cc create mode 100644 src/select.h create mode 100644 src/session.cc create mode 100644 src/session.h create mode 100644 src/stats.cc create mode 100644 src/stats.h create mode 100644 src/stream.cc create mode 100644 src/stream.h create mode 100644 src/strptime.cc create mode 100644 src/strptime.h create mode 100644 src/system.hh.in create mode 100644 src/temps.cc create mode 100644 src/temps.h create mode 100644 src/textual.cc create mode 100644 src/timelog.cc create mode 100644 src/timelog.h create mode 100644 src/times.cc create mode 100644 src/times.h create mode 100644 src/token.cc create mode 100644 src/token.h create mode 100644 src/unistring.h create mode 100644 src/utils.cc create mode 100644 src/utils.h create mode 100644 src/value.cc create mode 100644 src/value.h create mode 100644 src/views.cc create mode 100644 src/views.h create mode 100644 src/wcwidth.c.patch create mode 100644 src/wcwidth.cc create mode 100644 src/xact.cc create mode 100644 src/xact.h create mode 100644 test/CMakeLists.txt create mode 100755 test/CheckBaselineTests.py create mode 100644 test/CheckComments.py create mode 100755 test/CheckManpage.py create mode 100755 test/CheckOptions.py create mode 100755 test/CheckTexinfo.py create mode 100755 test/ConfirmTests.py create mode 100755 test/DocTests.py create mode 100755 test/GenerateTests.py create mode 100755 test/LedgerHarness.py create mode 100755 test/PyUnitTests.py create mode 100755 test/RegressTests.py create mode 100644 test/baseline/CMakeLists.txt create mode 100644 test/baseline/cmd-accounts.test create mode 100644 test/baseline/cmd-balance.test create mode 100644 test/baseline/cmd-budget.test create mode 100644 test/baseline/cmd-cleared.test create mode 100644 test/baseline/cmd-commodities.test create mode 100644 test/baseline/cmd-convert.test create mode 100644 test/baseline/cmd-convert1.dat create mode 100644 test/baseline/cmd-convert2.dat create mode 100644 test/baseline/cmd-convert3.dat create mode 100644 test/baseline/cmd-convert4.dat create mode 100644 test/baseline/cmd-csv.test create mode 100644 test/baseline/cmd-echo.test create mode 100644 test/baseline/cmd-entry.test create mode 100644 test/baseline/cmd-equity.test create mode 100644 test/baseline/cmd-payees.test create mode 100644 test/baseline/cmd-pricedb.test create mode 100644 test/baseline/cmd-pricemap.test create mode 100644 test/baseline/cmd-prices.test create mode 100644 test/baseline/cmd-print.test create mode 100644 test/baseline/cmd-register.test create mode 100644 test/baseline/cmd-script.test create mode 100644 test/baseline/cmd-select.test create mode 100644 test/baseline/cmd-source.test create mode 100644 test/baseline/cmd-stats.test create mode 100644 test/baseline/cmd-tags.test create mode 100644 test/baseline/cmd-xact.test create mode 100644 test/baseline/dir-account.test create mode 100644 test/baseline/dir-alias-fail.test create mode 100644 test/baseline/dir-alias.test create mode 100644 test/baseline/dir-apply.dat create mode 100644 test/baseline/dir-apply.test create mode 100644 test/baseline/dir-commodity-alias.test create mode 100644 test/baseline/dir-commodity-value.test create mode 100644 test/baseline/dir-commodity.test create mode 100644 test/baseline/dir-fixed.test create mode 100644 test/baseline/dir-import_py.test create mode 100644 test/baseline/dir-payee.test create mode 100644 test/baseline/dir-python_py.test create mode 100644 test/baseline/dir-tag.test create mode 100644 test/baseline/feat-annotations.test create mode 100644 test/baseline/feat-balance-assignments.test create mode 100644 test/baseline/feat-balance_assert-off.test create mode 100644 test/baseline/feat-balance_assert.test create mode 100644 test/baseline/feat-balance_assert_split.test create mode 100644 test/baseline/feat-check.test create mode 100644 test/baseline/feat-convert-with-directives.dat create mode 100644 test/baseline/feat-convert-with-directives.test create mode 100644 test/baseline/feat-fixated-prices.test create mode 100644 test/baseline/feat-import_py.test create mode 100644 test/baseline/feat-option_py.test create mode 100644 test/baseline/feat-value-expr.test create mode 100644 test/baseline/feat-value_py.test create mode 100644 test/baseline/featimport.py create mode 100644 test/baseline/featoption.py create mode 100644 test/baseline/opt-abbrev-len.test create mode 100644 test/baseline/opt-account-width.test create mode 100644 test/baseline/opt-account.test create mode 100644 test/baseline/opt-actual.test create mode 100644 test/baseline/opt-add-budget.test create mode 100644 test/baseline/opt-amount-data.test create mode 100644 test/baseline/opt-amount-width.test create mode 100644 test/baseline/opt-amount.test create mode 100644 test/baseline/opt-auto-match.dat create mode 100644 test/baseline/opt-auto-match.test create mode 100644 test/baseline/opt-aux-date.test create mode 100644 test/baseline/opt-average.test create mode 100644 test/baseline/opt-balance-format.test create mode 100644 test/baseline/opt-base.test create mode 100644 test/baseline/opt-basis.test create mode 100644 test/baseline/opt-begin.test create mode 100644 test/baseline/opt-bold-if.test create mode 100644 test/baseline/opt-budget-format.test create mode 100644 test/baseline/opt-budget.test create mode 100644 test/baseline/opt-budget_only.test create mode 100644 test/baseline/opt-budget_range.test create mode 100644 test/baseline/opt-by-payee.test create mode 100644 test/baseline/opt-check-payees.test create mode 100644 test/baseline/opt-cleared-format.test create mode 100644 test/baseline/opt-cleared.test create mode 100644 test/baseline/opt-code-as-account.test create mode 100644 test/baseline/opt-code-as-payee.test create mode 100644 test/baseline/opt-collapse-if-zero.test create mode 100644 test/baseline/opt-collapse.test create mode 100644 test/baseline/opt-collapse_reg.test create mode 100644 test/baseline/opt-color.test create mode 100644 test/baseline/opt-columns.test create mode 100644 test/baseline/opt-commodity-as-account.test create mode 100644 test/baseline/opt-commodity-as-payee.test create mode 100644 test/baseline/opt-cost.test create mode 100644 test/baseline/opt-count.test create mode 100644 test/baseline/opt-csv-format.test create mode 100644 test/baseline/opt-current.test create mode 100644 test/baseline/opt-daily.test create mode 100644 test/baseline/opt-date-format.test create mode 100644 test/baseline/opt-date-width.test create mode 100644 test/baseline/opt-date.test create mode 100644 test/baseline/opt-datetime-format.test create mode 100644 test/baseline/opt-day-break.test create mode 100644 test/baseline/opt-dc.test create mode 100644 test/baseline/opt-decimal-comma.test create mode 100644 test/baseline/opt-depth.test create mode 100644 test/baseline/opt-depth_flat.test create mode 100644 test/baseline/opt-deviation.test create mode 100644 test/baseline/opt-display-amount.test create mode 100644 test/baseline/opt-display-total.test create mode 100644 test/baseline/opt-display.test create mode 100644 test/baseline/opt-dow.test create mode 100644 test/baseline/opt-empty.test create mode 100644 test/baseline/opt-empty_bal.test create mode 100644 test/baseline/opt-empty_bal_flat.test create mode 100644 test/baseline/opt-empty_collapse.test create mode 100644 test/baseline/opt-end.test create mode 100644 test/baseline/opt-equity.test create mode 100644 test/baseline/opt-exact.test create mode 100644 test/baseline/opt-exchange.test create mode 100644 test/baseline/opt-explicit.test create mode 100644 test/baseline/opt-file.test create mode 100644 test/baseline/opt-file1.dat create mode 100644 test/baseline/opt-file2.dat create mode 100644 test/baseline/opt-flat.test create mode 100644 test/baseline/opt-force-color.test create mode 100644 test/baseline/opt-forecast-while.test create mode 100644 test/baseline/opt-forecast-years.test create mode 100644 test/baseline/opt-forecast_only.test create mode 100644 test/baseline/opt-format.test create mode 100644 test/baseline/opt-gain.test create mode 100644 test/baseline/opt-group-by.test create mode 100644 test/baseline/opt-group-title-format.test create mode 100644 test/baseline/opt-head.test create mode 100644 test/baseline/opt-historical.test create mode 100644 test/baseline/opt-immediate.test create mode 100644 test/baseline/opt-init-file.dat create mode 100644 test/baseline/opt-init-file.test create mode 100644 test/baseline/opt-inject.test create mode 100644 test/baseline/opt-input-date-format.test create mode 100644 test/baseline/opt-invert.test create mode 100644 test/baseline/opt-limit.test create mode 100644 test/baseline/opt-lot-dates.test create mode 100644 test/baseline/opt-lot-notes.test create mode 100644 test/baseline/opt-lot-prices.test create mode 100644 test/baseline/opt-lot-tags.test create mode 100644 test/baseline/opt-lots-actual.test create mode 100644 test/baseline/opt-lots.test create mode 100644 test/baseline/opt-lots_basis.test create mode 100644 test/baseline/opt-lots_basis_base.test create mode 100644 test/baseline/opt-market.test create mode 100644 test/baseline/opt-master-account.test create mode 100644 test/baseline/opt-meta-width.test create mode 100644 test/baseline/opt-meta.test create mode 100644 test/baseline/opt-monthly.test create mode 100644 test/baseline/opt-no-aliases.test create mode 100644 test/baseline/opt-no-revalued.test create mode 100644 test/baseline/opt-no-rounding.test create mode 100644 test/baseline/opt-no-titles.test create mode 100644 test/baseline/opt-no-total.test create mode 100644 test/baseline/opt-now.test create mode 100644 test/baseline/opt-only.test create mode 100644 test/baseline/opt-output.test create mode 100644 test/baseline/opt-pager.test create mode 100644 test/baseline/opt-payee-as-account.test create mode 100644 test/baseline/opt-payee-width.test create mode 100644 test/baseline/opt-payee.test create mode 100644 test/baseline/opt-pedantic.test create mode 100644 test/baseline/opt-pending.test create mode 100644 test/baseline/opt-percent.test create mode 100644 test/baseline/opt-period.test create mode 100644 test/baseline/opt-permissive.test create mode 100644 test/baseline/opt-pivot.test create mode 100644 test/baseline/opt-plot-amount-format.test create mode 100644 test/baseline/opt-plot-total-format.test create mode 100644 test/baseline/opt-prepend-format.test create mode 100644 test/baseline/opt-prepend-width.test create mode 100644 test/baseline/opt-price-db.dat create mode 100644 test/baseline/opt-price-db.test create mode 100644 test/baseline/opt-price.test create mode 100644 test/baseline/opt-pricedb-format.test create mode 100644 test/baseline/opt-prices-format.test create mode 100644 test/baseline/opt-primary-date.test create mode 100644 test/baseline/opt-quantity.test create mode 100644 test/baseline/opt-quarterly.test create mode 100644 test/baseline/opt-raw.test create mode 100644 test/baseline/opt-real.test create mode 100644 test/baseline/opt-recursive-aliases.test create mode 100644 test/baseline/opt-register-format.test create mode 100644 test/baseline/opt-related-all.test create mode 100644 test/baseline/opt-related.test create mode 100644 test/baseline/opt-revalued-only.test create mode 100644 test/baseline/opt-revalued.test create mode 100644 test/baseline/opt-rich-data.test create mode 100644 test/baseline/opt-script.dat create mode 100644 test/baseline/opt-script.test create mode 100644 test/baseline/opt-seed.test.save create mode 100644 test/baseline/opt-sort-all.test create mode 100644 test/baseline/opt-sort-xacts.test create mode 100644 test/baseline/opt-sort.test create mode 100644 test/baseline/opt-start-of-week.test create mode 100644 test/baseline/opt-strict.test create mode 100644 test/baseline/opt-subtotal.test create mode 100644 test/baseline/opt-tail.test create mode 100644 test/baseline/opt-time-colon.test create mode 100644 test/baseline/opt-time-report.test create mode 100644 test/baseline/opt-total-data.test create mode 100644 test/baseline/opt-total-width.test create mode 100644 test/baseline/opt-total.test create mode 100644 test/baseline/opt-trace.test create mode 100644 test/baseline/opt-truncate.test create mode 100644 test/baseline/opt-unbudgeted.test create mode 100644 test/baseline/opt-uncleared.test create mode 100644 test/baseline/opt-unrealized-gains.test create mode 100644 test/baseline/opt-unrealized-losses.test create mode 100644 test/baseline/opt-unrealized.test create mode 100644 test/baseline/opt-unround.test create mode 100644 test/baseline/opt-value-expr.test create mode 100644 test/baseline/opt-values.test create mode 100644 test/baseline/opt-weekly.test create mode 100644 test/baseline/opt-weekly_empty.test create mode 100644 test/baseline/opt-wide.test create mode 100644 test/baseline/opt-yearly.test create mode 100755 test/convert.py create mode 100755 test/fullcheck.sh create mode 100644 test/input/demo.ledger create mode 100644 test/input/divzero.dat create mode 100644 test/input/drewr.dat create mode 100644 test/input/drewr3.dat create mode 100644 test/input/sample.dat create mode 100644 test/input/standard.dat create mode 100644 test/input/transfer.dat create mode 100644 test/input/wow.dat create mode 100644 test/manual/CMakeLists.txt create mode 100644 test/manual/transaction-codes-1.test create mode 100644 test/manual/transaction-codes-2.test create mode 100644 test/manual/transaction-notes-1.test create mode 100644 test/manual/transaction-notes-2.test create mode 100644 test/manual/transaction-notes-3.test create mode 100644 test/manual/transaction-notes-4.test create mode 100644 test/manual/transaction-status-1.test create mode 100644 test/manual/transaction-status-2.test create mode 100644 test/manual/transaction-status-3.test create mode 100644 test/manual/transaction-status-4.test create mode 100644 test/python/JournalTest.py create mode 100644 test/python/PostingTest.py create mode 100644 test/python/TransactionTest.py create mode 100644 test/python/UnitTests.py create mode 100644 test/regress/012ADB60.test create mode 100644 test/regress/0161EB1E.test create mode 100644 test/regress/04C5E1CA.test create mode 100644 test/regress/04D86CD0.test create mode 100644 test/regress/0CA014F9.test create mode 100644 test/regress/0DDDEBC0.dat create mode 100644 test/regress/0DDDEBC0.test create mode 100644 test/regress/1036.test create mode 100644 test/regress/1038_1.test create mode 100644 test/regress/1038_2.test create mode 100644 test/regress/1038_3.test create mode 100644 test/regress/1046.test create mode 100644 test/regress/1050.test create mode 100644 test/regress/1055.test create mode 100644 test/regress/1057.test create mode 100644 test/regress/1072.test create mode 100644 test/regress/1074.test create mode 100644 test/regress/10D19C11.test create mode 100644 test/regress/1102.test create mode 100644 test/regress/1106.test create mode 100644 test/regress/1127.test create mode 100644 test/regress/1132.test create mode 100644 test/regress/1147-a.test create mode 100644 test/regress/1147-b.test create mode 100644 test/regress/1159.test create mode 100644 test/regress/1182_1.test create mode 100644 test/regress/1182_2.test create mode 100644 test/regress/1187_1.test create mode 100644 test/regress/1187_2.test create mode 100644 test/regress/1187_3.test create mode 100644 test/regress/1187_4.test create mode 100644 test/regress/1187_5.test create mode 100644 test/regress/1222.test create mode 100644 test/regress/1224.test create mode 100644 test/regress/1384C1D8.test create mode 100644 test/regress/13965214.test create mode 100644 test/regress/14DB77E7.test create mode 100644 test/regress/15230B79.test create mode 100644 test/regress/15A80F68.test create mode 100644 test/regress/1626.test create mode 100644 test/regress/1702.test create mode 100644 test/regress/1703.test create mode 100644 test/regress/1722.test create mode 100644 test/regress/1723.test create mode 100644 test/regress/1753.test create mode 100644 test/regress/178501DC.test create mode 100644 test/regress/1A546C4D.test create mode 100644 test/regress/1CF1EEC2.test create mode 100644 test/regress/1D275740.test create mode 100644 test/regress/1E192DF6.test create mode 100644 test/regress/25A099C9.dat create mode 100644 test/regress/25A099C9.test create mode 100644 test/regress/2CE7DADB.test create mode 100644 test/regress/2E3496BD.test create mode 100644 test/regress/370-budget_period_days.test create mode 100644 test/regress/370-budget_period_weeks.test create mode 100644 test/regress/370-forecast_period_days.test create mode 100644 test/regress/370-forecast_period_weeks.test create mode 100644 test/regress/370-period.test create mode 100644 test/regress/373540CC.test create mode 100644 test/regress/375.test create mode 100644 test/regress/383.test create mode 100644 test/regress/3AAB00ED.test create mode 100644 test/regress/3AB70168.test create mode 100644 test/regress/3FE26304.test create mode 100644 test/regress/4509F714.test create mode 100644 test/regress/461980A1.test create mode 100644 test/regress/47C579B8.test create mode 100644 test/regress/494-a.ledger create mode 100644 test/regress/494-b.ledger create mode 100644 test/regress/4D9288AE.dat create mode 100644 test/regress/4D9288AE.py create mode 100644 test/regress/4D9288AE_py.test create mode 100644 test/regress/53BCED29.test create mode 100644 test/regress/550-584.test create mode 100644 test/regress/55831A79.test create mode 100644 test/regress/56BBE69B.test create mode 100644 test/regress/5A03CFC3.test create mode 100644 test/regress/5D92A5EB.test create mode 100644 test/regress/5F1BAF17.test create mode 100644 test/regress/5FBF2ED8.test create mode 100644 test/regress/605A410D.test create mode 100644 test/regress/6188B0EC.test create mode 100644 test/regress/620F0674.test create mode 100644 test/regress/640D3205.test create mode 100644 test/regress/647D5DB9.test create mode 100644 test/regress/65FECA4D.test create mode 100644 test/regress/686.test create mode 100644 test/regress/68917252.dat create mode 100644 test/regress/68917252.test create mode 100644 test/regress/6D9066DD.test create mode 100644 test/regress/6DAB9FE3.test create mode 100644 test/regress/6E041C52.test create mode 100644 test/regress/6E7C2DF9.test create mode 100644 test/regress/712-a.test create mode 100644 test/regress/712-b.test create mode 100644 test/regress/713-a.test create mode 100644 test/regress/713-b.test create mode 100644 test/regress/727B2DF8.test create mode 100644 test/regress/730.test create mode 100644 test/regress/751B2357.test create mode 100644 test/regress/755.test create mode 100644 test/regress/785.test create mode 100644 test/regress/786A3DD0.test create mode 100644 test/regress/78AB4B87.dat create mode 100644 test/regress/78AB4B87.py create mode 100644 test/regress/78AB4B87_py.test create mode 100644 test/regress/793F6BF0.test create mode 100644 test/regress/7C44010B.test create mode 100644 test/regress/7F3650FD.test create mode 100644 test/regress/8254755E.test create mode 100644 test/regress/82763D86.test create mode 100644 test/regress/83B4A0E5.test create mode 100644 test/regress/854150DF.test create mode 100644 test/regress/86D2BDC4.test create mode 100644 test/regress/889BB167.test create mode 100644 test/regress/89233B6D-a.dat create mode 100644 test/regress/89233B6D-b.dat create mode 100644 test/regress/89233B6D.test create mode 100644 test/regress/8CE88DB4.test create mode 100644 test/regress/8EAF77C0.test create mode 100644 test/regress/9188F587.py create mode 100644 test/regress/9188F587_py.test create mode 100644 test/regress/95350193.test create mode 100644 test/regress/96A8E4A1.test create mode 100644 test/regress/981.test create mode 100644 test/regress/999-a.test create mode 100644 test/regress/999-b.test create mode 100644 test/regress/9E0E606D.test create mode 100644 test/regress/9EB10714.test create mode 100644 test/regress/A013A73B.test create mode 100644 test/regress/A28CF697.test create mode 100644 test/regress/A3FA7601.dat create mode 100644 test/regress/A3FA7601.test create mode 100644 test/regress/A560FDAD.test create mode 100644 test/regress/A8FCC765.dat create mode 100644 test/regress/A8FCC765.test create mode 100644 test/regress/AA2FF2B.test create mode 100644 test/regress/ACE05ECE.test create mode 100644 test/regress/AEDE9734.test create mode 100644 test/regress/AFAFB804.test create mode 100644 test/regress/B21BF389.py create mode 100644 test/regress/B21BF389_py.test create mode 100644 test/regress/B68FFB0D.test create mode 100644 test/regress/BBFA1759.test create mode 100644 test/regress/BF3C1F82-2.test create mode 100644 test/regress/BF3C1F82.test create mode 100644 test/regress/BFD3FBE1.test create mode 100644 test/regress/C0212EAC.test create mode 100644 test/regress/C19E4E9B.test create mode 100644 test/regress/C523E23F.test create mode 100644 test/regress/C927CFFE.test create mode 100644 test/regress/C9D593B3.test create mode 100644 test/regress/CAE63F5C-a.test create mode 100644 test/regress/CAE63F5C-b.test create mode 100644 test/regress/CAE63F5C-c.test create mode 100644 test/regress/CEECC0B0.test create mode 100644 test/regress/CFE5D8AA.test create mode 100644 test/regress/CMakeLists.txt create mode 100644 test/regress/D060256A.test create mode 100644 test/regress/D2829FC4.test create mode 100644 test/regress/D51BFF74.test create mode 100644 test/regress/D943AE0F.test create mode 100644 test/regress/D9C8EB08.test create mode 100644 test/regress/DB490507.test create mode 100644 test/regress/DDB54BB8.test create mode 100644 test/regress/DE17CCF1.test create mode 100644 test/regress/E2E479BC.test create mode 100644 test/regress/E4C9A8EA.test create mode 100644 test/regress/E627C594.test create mode 100644 test/regress/E9F130C5.test create mode 100644 test/regress/EA18D948.test create mode 100644 test/regress/F06D5554.test create mode 100644 test/regress/F524E251.test create mode 100644 test/regress/F559EC12.test create mode 100644 test/regress/FCE11C8D.test create mode 100644 test/regress/FDFBA165.test create mode 100644 test/regress/GH520.test create mode 100644 test/regress/error-in-include.dat create mode 100644 test/regress/error-in-include.test create mode 100644 test/regress/fix-missing-trans-in-last-budget-period.test create mode 100644 test/regress/market-group-by.test create mode 100644 test/regress/total-1.test create mode 100644 test/regress/total-2.test create mode 100644 test/regress/xact_code.dat create mode 100644 test/regress/xact_code.py create mode 100644 test/regress/xact_code_py.test create mode 100644 test/unit/CMakeLists.txt create mode 100644 test/unit/t_amount.cc create mode 100644 test/unit/t_balance.cc create mode 100644 test/unit/t_commodity.cc create mode 100644 test/unit/t_expr.cc create mode 100644 test/unit/t_times.cc create mode 100644 test/unit/t_value.cc create mode 100755 tools/average create mode 100755 tools/build.sh create mode 100755 tools/cleanup.sh create mode 100644 tools/excludes create mode 100755 tools/gendocs.sh create mode 100755 tools/genuuid create mode 100755 tools/nix-build.sh create mode 100755 tools/pre-commit create mode 100755 tools/prepare-commit-msg create mode 100755 tools/proof create mode 100755 tools/push create mode 100755 tools/rename.sh create mode 100755 tools/speed-test.sh create mode 100755 tools/speedcmp create mode 100755 tools/spellcheck.sh create mode 100755 tools/times.sh create mode 100755 tools/update_copyright_year.sh diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 00000000..34761815 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,36 @@ +;;; Directory Local Variables +;;; For more information see (info "(emacs) Directory Variables") + +((nil + (tab-width . 2) + (sentence-end-double-space . t) + (bug-reference-url-format . "https://github.com/ledger/ledger/issues/%s")) + (c-mode + (c-file-style . "ledger") + (c-style-alist + ("ledger" + (indent-tabs-mode) + (c-basic-offset . 2) + (c-comment-only-line-offset 0 . 0) + (c-hanging-braces-alist + (substatement-open before after) + (arglist-cont-nonempty)) + (c-offsets-alist + (statement-block-intro . +) + (knr-argdecl-intro . 5) + (substatement-open . 0) + (substatement-label . 0) + (label . 0) + (case-label . 0) + (statement-case-open . 0) + (statement-cont . +) + (arglist-intro . +) + (arglist-close . +) + (inline-open . 0) + (brace-list-open . 0) + (topmost-intro-cont first c-lineup-topmost-intro-cont c-lineup-gnu-DEFUN-intro-cont)) + (c-special-indent-hook . c-gnu-impose-minimum) + (c-block-comment-prefix . "")))) + (emacs-lisp-mode + (indent-tabs-mode . nil))) + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3a5fc0de --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +*.[oa] +*.so +*.so.* +*.dylib +*.backup +*.elc +*.gcov +*.l[oa] +*.pyc +*.sw[p-z] +*~ +.timestamp +*.tar.bz2 +*.tar.gz +.deps/ +.libs/ +ABOUT-NLS +Makefile +Makefile.am +Makefile.in +TAGS +acconf.h.in +aclocal.m4 +autogen.sh +autom4te.cache/ +config.guess +config.h +config.h.in +config.log +config.rpath +config.status +config.sub +configure +configure.ac +depcomp +doc/Doxyfile +doc/*.aux +doc/*.cp +doc/*.cps +doc/*.fn +doc/*.fns +doc/*.html +doc/*.info +doc/*.info-* +doc/*.ky +doc/*.kys +doc/*.log +doc/*.pdf +doc/*.pg +doc/*.toc +doc/*.tp +doc/*.vr +doc/*.vrs +doc/.dirstamp +doc/html/ +doc/latex/ +doc/refman.pdf +doc/report/ +doc/version.texi +install-sh +intl/ +ledger +libtool +ltmain.sh +m4/ +make.sh +missing +mkinstalldirs +po/ +py-compile +shave +shave-libtool +stamp-h1 +texinfo.tex +tmpcvs*/ +tmpwrk*/ +dist/win/vc9/Debug/ +dist/win/vc9/gen-mpir.exe +dist/win/vc9/gen-mpir.ilk +dist/win/vc9/gen-mpir.pdb +dist/win/vc9/ledger.ncb +dist/win/vc9/ledger.vcproj.*.user +dist/win/vc9/ledger.suo +dist/win/vc9/lib/Win32/Debug/ +src/TAGS +CMakeCache.txt +CPackConfig.cmake +CPackSourceConfig.cmake +CMakeFiles/ +CTestTestfile.cmake +_CPack_Packages/ +cmake_install.cmake +install_manifest.txt +system.hh +system.hh.[gp]ch* +Ledger*.dmg +Ledger*.sh +# Files that generated by running ./demo.sh in contrib/non-profit-audit-reports +contrib/non-profit-audit-reports/tests/chart-of-accounts.txt +contrib/non-profit-audit-reports/tests/general-ledger.csv +contrib/non-profit-audit-reports/tests/general-ledger.ods +contrib/non-profit-audit-reports/tests/general-ledger.txt +contrib/non-profit-audit-reports/tests/MANIFEST +contrib/non-profit-audit-reports/general-ledger.zip +/wiki/ +.ninja_deps +.ninja_log +build.ninja +rules.ninja +test/Testing +/MathTests +/UtilTests diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..825ad6d7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,111 @@ +# NOTE: Please validate this file after editing it using +# Travis WebLint https://lint.travis-ci.org/ +# or travis-lint https://github.com/travis-ci/travis-lint + +language: cpp +compiler: + - gcc +os: + - linux + - osx +sudo: false +cache: + apt: true + +env: + global: + # List of required boost libraries to build + - BOOST_LIBS="date_time,filesystem,iostreams,python,regex,system,test" + # Encrypted COVERITY_SCAN_TOKEN + - secure: "mYNxD1B8WNSvUeKzInehZ7syi2g1jH2ymeSQxoeKKD2duq3pvNWPdZdc4o9MlWQcAqcz58rhFZRIpuEWCnP0LbbJaG+MyuemMn9uAmg9Y4gFpMsBPHuTdf8pO3rDex+tkrr9puEJFgL+QV/TehxO6NDDpx7UdYvJb+4aZD/auYI=" + matrix: + # Boost version to build against; an empty string means the + # distribution's default. + - BOOST_VERSION="" + - BOOST_VERSION="1.61.0" + +# The configuration for macOS only works with Boost installed by +# homebrew, so exclude the other combinations. +matrix: + exclude: + - os: linux + env: BOOST_VERSION="" + - os: osx + env: BOOST_VERSION="1.61.0" + +addons: + coverity_scan: + project: + name: "ledger/ledger" + description: "Build submitted via Travis CI" + build_command_prepend: "cmake . -DUSE_PYTHON=ON -DBUILD_DEBUG=ON -DCLANG_GCOV=ON" + build_command: "make" + branch_pattern: coverity + apt: + packages: + - libgmp-dev + - libmpfr-dev + - libedit-dev + - libboost-dev + - libboost-test-dev + - libboost-regex-dev + - libboost-python-dev + - libboost-system-dev + - libboost-date-time-dev + - libboost-iostreams-dev + - libboost-filesystem-dev + - libboost-serialization-dev + homebrew: + packages: + - boost + - boost-python + - gmp + - mpfr + +before_install: + - | + if [ -n "${BOOST_VERSION}" ]; then + BOOST_SOURCE="$(mktemp -d)" + BOOST_SOURCE_URL="https://sourceforge.net/projects/boost/files/boost/${BOOST_VERSION}/boost_${BOOST_VERSION//./_}.tar.bz2/download" + curl -Ls "$BOOST_SOURCE_URL" | + tar jx -C "${BOOST_SOURCE}" --strip-components 1 + fi + +install: + - | + if [ -n "${BOOST_VERSION}" ]; then + export BOOST_ROOT="${HOME}/boost" + pushd "${BOOST_SOURCE}" + ./bootstrap.sh --with-libraries="${BOOST_LIBS}" + ./b2 threading=multi -d0 --prefix="${BOOST_ROOT}" install + popd + rm -Rf "${BOOST_SOURCE}" + fi + +before_script: + # On macOS, CMake finds the Boost.Python installed by homebrew only + # with the component name "python27". Also, precompiling system.hh + # does not work. + - if [ "$TRAVIS_OS_NAME" = osx -a -z "$BOOST_VERSION" ]; then EXTRA_CMAKE_ARGS="-DPRECOMPILE_SYSTEM_HH=OFF -DUSE_PYTHON27_COMPONENT=ON"; fi + - cmake . -DUSE_PYTHON=ON -DBUILD_DEBUG=ON $EXTRA_CMAKE_ARGS + - make VERBOSE=1 + +script: + - ctest --output-on-failure + - PYTHONPATH=. python python/demo.py + +notifications: + email: + on_success: change + on_failure: change + irc: + channels: [ "chat.freenode.net#ledger" ] + on_success: change + on_failure: change + webhooks: + urls: + - https://webhooks.gitter.im/e/0050d91909a8cde39e35 + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: never # options: [always|never|change] default: always + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..9a6ceae0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,331 @@ +cmake_minimum_required(VERSION 2.8.5) +if(POLICY CMP0042) + # CMP0042 is only known to CMake 3.0 and above + cmake_policy(SET CMP0042 OLD) +endif(POLICY CMP0042) + +PROJECT(ledger) + +set(Ledger_VERSION_MAJOR 3) +set(Ledger_VERSION_MINOR 1) +set(Ledger_VERSION_PATCH 3) +set(Ledger_VERSION_PRERELEASE "") +set(Ledger_VERSION_DATE 20190331) + +set(Ledger_TEST_TIMEZONE "America/Chicago") + +# Point CMake at any custom modules we may ship +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +enable_testing() + +add_definitions(-std=c++11) +if (CYGWIN) + add_definitions(-U__STRICT_ANSI__) +endif() + +######################################################################## + +option(USE_PYTHON "Build support for the Python scripting bridge" OFF) +OPTION(USE_PYTHON27_COMPONENT "Use python27 as name of Boost.Python component" OFF) +option(USE_DOXYGEN "Build reference documentation using Doxygen" OFF) + +option(DISABLE_ASSERTS "Build without any internal consistency checks" OFF) +option(BUILD_DEBUG "Build support for runtime debugging" OFF) +option(PRECOMPILE_SYSTEM_HH "Precompile system.hh" ON) + +option(BUILD_LIBRARY "Build and install Ledger as a library" ON) +option(BUILD_DOCS "Build and install documentation" OFF) +option(BUILD_WEB_DOCS "Build version of documentation suitable for viewing online" OFF) + +if (BUILD_DEBUG) + set(CMAKE_BUILD_TYPE Debug) + set(DEBUG_MODE 1) +else() + set(CMAKE_BUILD_TYPE Release) + set(DEBUG_MODE 0) +endif() + +if (DISABLE_ASSERTS) + set(NO_ASSERTS 1) +else() + set(NO_ASSERTS 0) +endif() + +if (CLANG_GCOV) + set(PROFILE_LIBS profile_rt) + set(CMAKE_REQUIRED_LIBRARIES ${PROFILE_LIBS}) +endif() + +######################################################################## + +set(Python_ADDITIONAL_VERSIONS 2.7 2.6) +find_package(PythonInterp) # Used for running tests + +if (USE_PYTHON) + if (NOT BUILD_LIBRARY) + message(ERROR "Building the python module requires BUILD_LIBRARY=ON.") + endif() + + find_package(PythonLibs) + if (PYTHONLIBS_FOUND) + if(USE_PYTHON27_COMPONENT) + set(BOOST_PYTHON "python27") + else() + set(BOOST_PYTHON "python") + endif() + set(HAVE_BOOST_PYTHON 1) + include_directories(SYSTEM ${PYTHON_INCLUDE_DIRS}) + else() + set(HAVE_BOOST_PYTHON 0) + message("Could not find a Python library to use with Boost.Python") + endif() +else() + set(HAVE_BOOST_PYTHON 0) +endif() + +# Set BOOST_ROOT to help CMake to find the right Boost version +find_package(Boost 1.49.0 + REQUIRED date_time filesystem system iostreams regex unit_test_framework + ${BOOST_PYTHON}) + +include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) +link_directories(${Boost_LIBRARY_DIRS}) + +######################################################################## + +include(CheckIncludeFiles) +include(CheckLibraryExists) +include(CheckFunctionExists) +include(CheckCSourceCompiles) +include(CheckCXXSourceCompiles) +include(CheckCXXSourceRuns) +include(CMakePushCheckState) + +check_function_exists(getpwuid HAVE_GETPWUID) +check_function_exists(getpwnam HAVE_GETPWNAM) +check_function_exists(ioctl HAVE_IOCTL) +check_function_exists(isatty HAVE_ISATTY) + +check_c_source_compiles(" +#include +#include +#include +#include +#include +#include + +int main() { + int status, pfd[2]; + status = pipe(pfd); + status = fork(); + if (status < 0) { + ; + } else if (status == 0) { + char *arg0 = NULL; + + status = dup2(pfd[0], STDIN_FILENO); + + close(pfd[1]); + close(pfd[0]); + + execlp(\"\", arg0, (char *)0); + perror(\"execl\"); + exit(1); + } else { + close(pfd[0]); + } + return 0; +}" UNIX_PIPES_COMPILES) + +if (UNIX_PIPES_COMPILES) + set(HAVE_UNIX_PIPES 1) +else() + set(HAVE_UNIX_PIPES 0) +endif() + +cmake_push_check_state() + +set(CMAKE_REQUIRED_INCLUDES ${CMAKE_INCLUDE_PATH} ${Boost_INCLUDE_DIRS}) +set(CMAKE_REQUIRED_LIBRARIES ${Boost_LIBRARIES} icuuc ${PROFILE_LIBS}) + +check_cxx_source_runs(" +#include + +using namespace boost; + +int main() { + std::string text = \"Активы\"; + u32regex r = make_u32regex(\"активы\", regex::perl | regex::icase); + return u32regex_search(text, r) ? 0 : 1; +}" BOOST_REGEX_UNICODE_RUNS) + +if (BOOST_REGEX_UNICODE_RUNS) + set(HAVE_BOOST_REGEX_UNICODE 1) +else() + set(HAVE_BOOST_REGEX_UNICODE 0) +endif() + +cmake_pop_check_state() + +# Check if fix for https://github.com/boostorg/python/issues/39 is needed +if (HAVE_BOOST_PYTHON) +cmake_push_check_state() + +set(CMAKE_REQUIRED_INCLUDES + ${CMAKE_INCLUDE_PATH} ${Boost_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}) +set(CMAKE_REQUIRED_LIBRARIES + ${Boost_LIBRARIES} ${PYTHON_LIBRARIES} ${PROFILE_LIBS}) + +check_cxx_source_compiles(" +#include + +struct X { int y; }; + +int main() +{ + boost::python::make_setter(&X::y); + return 0; +}" BOOST_MAKE_SETTER_COMPILES) + +if (BOOST_MAKE_SETTER_COMPILES) + set(HAVE_BOOST_159_ISSUE_39 0) +else() + set(HAVE_BOOST_159_ISSUE_39 1) +endif() + +cmake_pop_check_state() +endif() + +######################################################################## + +include_directories(${CMAKE_INCLUDE_PATH}) + +macro(find_opt_library_and_header _header_var _header _lib_var _lib _have_var) + if (${_have_var}) + find_path(${_header_var} ${_header}) + if (NOT ${_header_var}) + set(${_have_var} 0) + else() + find_library(${_lib_var} ${_lib}) + if (NOT ${_lib_var}) + set(${_have_var} 0) + else() + include_directories(SYSTEM "${${_header_var}}") + set(${_have_var} 1) + endif() + endif() + else() + set(${_have_var} 0) + endif() +endmacro(find_opt_library_and_header _header_var _header _lib_var _lib _have_var) + +macro(find_req_library_and_header _header_var _header _lib_var _lib) + find_path(${_header_var} ${_header}) + if (NOT ${_header_var}) + message(SEND_ERROR "Could not find ${_header} on your system") + else() + include_directories(SYSTEM "${${_header_var}}") + find_library(${_lib_var} ${_lib}) + if (NOT ${_lib_var}) + message(SEND_ERROR "Could not find library ${_lib} on your system") + endif() + endif() +endmacro(find_req_library_and_header _header_var _header _lib_var _lib) + +find_req_library_and_header(GMP_PATH gmp.h GMP_LIB gmp) +find_req_library_and_header(MPFR_PATH mpfr.h MPFR_LIB mpfr) + +check_library_exists(edit readline "" HAVE_EDIT) +find_opt_library_and_header(EDIT_PATH histedit.h EDIT_LIB edit HAVE_EDIT) + +#find_package(Gettext) # Used for running tests + +#if (GETTEXT_FOUND) +# set(HAVE_GETTEXT 1) +#else() + set(HAVE_GETTEXT 0) +#endif() + +#find_path(INTL_PATH libintl.h) +#find_library(INTL_LIB intl) +#include_directories(SYSTEM "${INTL_PATH}") + +######################################################################## + +macro(add_ledger_library_dependencies _target) + target_link_libraries(${_target} ${MPFR_LIB}) + target_link_libraries(${_target} ${GMP_LIB}) + if (HAVE_EDIT) + target_link_libraries(${_target} ${EDIT_LIB}) + endif() + if (HAVE_GETTEXT) + target_link_libraries(${_target} ${INTL_LIB}) + endif() + if (HAVE_BOOST_PYTHON) + if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + # Don't link directly to a Python framework on macOS, to avoid segfaults + # when the module is imported from a different interpreter + target_link_libraries(${_target} ${Boost_LIBRARIES}) + set_target_properties(${_target} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") + else() + target_link_libraries(${_target} ${Boost_LIBRARIES} ${PYTHON_LIBRARIES}) + endif() + else() + target_link_libraries(${_target} ${Boost_LIBRARIES}) + endif() + if (HAVE_BOOST_REGEX_UNICODE) + target_link_libraries(${_target} icuuc) + endif() + target_link_libraries(${_target} ${PROFILE_LIBS}) +endmacro(add_ledger_library_dependencies _target) + +######################################################################## + +include(FindUtfcpp) +if (UTFCPP_FOUND) + include_directories("${UTFCPP_INCLUDE_DIR}") +else() + message(FATAL_ERROR "Missing required header file: utf8.h\n" + "Define UTFCPP_PATH or install utfcpp locally into the source tree below lib/utfcpp/." + ) +endif() + +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) + +# add the binary tree to the search path for include files so that we will +# find system.hh +include_directories("${PROJECT_BINARY_DIR}") + +configure_file( + ${PROJECT_SOURCE_DIR}/src/system.hh.in + ${PROJECT_BINARY_DIR}/system.hh) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "-isystem ") +endif() + +add_subdirectory(src) +add_subdirectory(doc) +add_subdirectory(test) + +######################################################################## + +# build a CPack driven installer package +include (InstallRequiredSystemLibraries) + +set (CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.md") +set (CPACK_PACKAGE_VERSION_MAJOR "${Ledger_VERSION_MAJOR}") +set (CPACK_PACKAGE_VERSION_MINOR "${Ledger_VERSION_MINOR}") +set (CPACK_PACKAGE_VERSION_PATCH "${Ledger_VERSION_PATCH}${Ledger_VERSION_PRERELEASE}") + +set (CPACK_GENERATOR "TBZ2") +set (CPACK_SOURCE_GENERATOR "TBZ2") +set (CPACK_SOURCE_PACKAGE_FILE_NAME + "${CMAKE_PROJECT_NAME}-${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") +set (CPACK_SOURCE_IGNORE_FILES "/.git*;/.dir-locals.el;~$;/doc/website/;/doc/wiki/;/lib/*.sh;/lib/Makefile;/tools/;${CPACK_SOURCE_IGNORE_FILES}") + +include (CPack) + +### CMakeLists.txt ends here diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..80a413f3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,138 @@ +Tips for contributors +--------------------- + +* Please **make pull requests against `next`, not `master`**. Ledger follows + a [git-flow] branching model, in which development happens on the `next` + branch and is subsequently merged into `master` for releases. +* If you're making **changes to files for which the Travis build is not + relevant**, please **add `[ci skip]` to the end of the commit message**. +* Report bugs using [GitHub Issues]. + +GLOSSARY +---- + +Developing the Ledger software uses a number different tools, not all of +which will be familiar to all developers. + +**[Boost]**: a standard set of C++ libraries. Most +Boost libraries consist of inline functions and templates in header files. + +**[Boost.Python]**: C++ library which enables seamless interoperability +between C++ and the Python programming language. + +**[Cheetah]**: a Python templating engine, used by `./python/server.py`. + +**[CMake]**: A cross platform system for building from source code. It uses +the `CMakeLists.txt` files. + +**[Doxygen]**: generates programming documentation from +source code files. Primarily used on C++ sources, but works on all. Uses +the `doc/Doxyfile.in` file. + +**[GCC]**: Gnu Compiler Collection, which includes the +*gcc* compiler and *gcov* coverage/profiler tool. + +**[clang]**: C language family frontend for LLVM, which +includes the *clang* compiler. + +**[GMP]**: Gnu Multiple Precision Arithmetic Library +provides arbitrary precision math. + +**[MPFR]**: Gnu Multiple Precision Floating-point Library +with correct rounding. + +**[Markdown]**: A typesetter +format that produces *html* files from *.md* files. Note that GitHub +automatically renders *.md* files. + +**[SHA1]**: a marginally secure cryptographic hash function, used only for +signing the license file. + +**[Texinfo]**: Gnu documentation +typesetter that produces *html* and *pdf* files from the `doc/\*.texi` files. + +**[Travis CI]**: a hosted continuous integration + service that builds and runs tests each commit posted to GitHub. Each + build creates a [log], updates a [small badge] at + the top left of the main project's + [README.md], and + emails the author of the commit if any tests fail. + +**[utfcpp]**: a library for handling utf-8 in a variety of C++ versions. + + +Orientation +--- + +The source tree can be confusing to a new developer. Here is a selective +orientation: + +**./acprep**: a custom thousand-line script to install dependencies, grab + updates, and build. It also creates `\*.cmake`, + `./CmakeFiles/` and other CMake temporary files. Use `./acprep --help` + for more information. + +**./README.md**: user readme file in markdown format, also used as the project + description on GitHub. + +**./contrib/**: contributed scripts of random quality and completion. They + usually require editing to run. + +**./doc/**: documentation, licenses, and + tools for generating documents such as the *pdf* manual. + +**./lib/**: a couple of libraries used in development. + +**./python/**: samples using the Python ledger module. + +**./src/**: the C++ header and source files in a flat directory. + +**./test/**: a testing harness with subdirectories full of tests + +**./tools/**: an accretion of tools, mostly small scripts, to aid development + + +Building +--- + +If you are going to be working on Ledger, you'll want to enable both debug +builds (which are the default, using `acprep`), and also the use of +pre-compiled headers. To do this, specify your compiler as either `clang++` +or `g++` as follows: + + mkdir build + ./acprep --compiler=clang++ + cd build + make + +This will set up a debug build using clang++ (and pre-compiled headers, which +is enabled by the combination of those two), and then start a build. + +For even quicker rebuilds, try the Ninja build tool, which is very fast at +determining what to rebuild, and automatically takes advantage of multiple +cores: + + mkdir build + ./acprep --compiler=clang++ --ninja + cd build + ninja + +[Boost]: http://boost.org +[Boost.Python]: http://www.boost.org/libs/python/ +[GitHub Issues]: https://github.com/ledger/ledger/issues +[GMP]: http://gmplib.org/ +[MPFR]: http://www.mpfr.org/ +[Cheetah]: http://www.cheetahtemplate.org +[CMake]: http://www.cmake.org +[Doxygen]: http://doxygen.org +[Markdown]: https://daringfireball.net/projects/markdown/ +[SHA1]: http://en.wikipedia.org/wiki/SHA-1 +[Texinfo]: http://www.gnu.org/software/texinfo/ +[Travis CI]: https://travis-ci.org +[GCC]: http://gcc.gnu.org +[utfcpp]: http://utfcpp.sourceforge.net +[log]: https://travis-ci.org/ledger/ledger +[small badge]: https://img.shields.io/travis/ledger/ledger/master.svg?&style=flat +[git-flow]: http://nvie.com/posts/a-successful-git-branching-model/ +[README.md]: https://github.com/ledger/ledger/blob/master/README.md +[clang]: http://clang.llvm.org diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..bf4a63bb --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,175 @@ +# INSTALL + +To build this code after doing a Git clone, run: + + $ ./acprep dependencies + $ ./acprep update + +If anything goes wrong, see "COMMON CONFIGURE/BUILD PROBLEMS" below. + +If you try to configure and build without running acprep first, you are +almost certainly going to run into problems. In future, you can run +`acprep update` again and again, and it will keep you updated to the +very latest version. + +Now install it: + + $ sudo make install + + +## COMMON CONFIGURE / BUILD PROBLEMS + +To build and install Ledger requires several dependencies on various +platforms. You can install these dependencies very simply for most of them +using: + + $ ./acprep dependencies + +The first order of business if acprep update doesn't work is to find out where +things went wrong. So follow these steps to produce a bug report I can track +down easily: + + $ ./acprep --debug update # shows what acprep was thinking + $ $EDITOR CMakeCache.txt # shows what cmake was thinking + +With the contents of config.log, and the output from acprep --debug update, +it's usually fairly obvious where things have gone astray. + + +## F.A.Q. + + +Q: The build fails saying it can't find `utf8.h` + +A: You didn't run `./acprep update`. + +---------------------------------------------------------------------- + +Q: `./acprep update` gives errors or `./acprep dependencies` fails + +A: You're probably missing some dependency libraries. If you tried + `./acprep dependencies` already and that didn't solve the problem, + then you may need to install dependencies by hand. On a Debian + GNU/Linux system (or Debian-based system such as Ubuntu), something + like this should work (as root): + + $ sudo apt-get install build-essential cmake texinfo python-dev \ + zlib1g-dev libbz2-dev libgmp3-dev gettext libmpfr-dev \ + libboost-date-time-dev libboost-filesystem-dev \ + libboost-graph-dev libboost-iostreams-dev \ + libboost-python-dev libboost-regex-dev libboost-test-dev \ + doxygen libedit-dev libmpc-dev tzdata + +---------------------------------------------------------------------- + +Q: Configure fails saying it can't find boost_regex + +A: Look in config.log and search for "boost_regex", then scroll down a bit + until you see the exact compile error. Usually it's failing because + your include directory is different from anything acprep is expecting to + see. It could also be failing because your Boost libraries have a + custom "suffix" on them. + + Let's say your Boost was installed in ~/boost, and every library has the + suffix `-xgcc42`. This is what you would run: + + $ CPPFLAGS=-I$HOME/boost acprep --boost=xgcc42 update + +---------------------------------------------------------------------- + +Q: Configure fails saying it can't find MPFR + +A: You need MPFR version 2.4.0 or higher. This version does not come with + most Debian distributions, so you will need to build it. The + relevant packages are `libmpfr-dev` and `libmpfr-dbg`. See also + the question above about what to do if `./acprep update` gives + errors or `./acprep dependencies` fails. + +---------------------------------------------------------------------- + +Q: I'm seeing a segfault deep inside the boost_regex code! + +A: Actually, the real segfault is in libstdc++'s facet code. It's being + caused by using a debug Boost with a non-debug build of Ledger, or + vice-versa. + +---------------------------------------------------------------------- + +Q: Something else fails, or Ledger crashes on startup + +A: This, I am most interested in hearing about. Please file a bug at the + Ledger Issue Tracker, https://github.com/ledger/ledger/issues. The more + details you can provide, the better. Also, if Ledger is crashing, try + running it under gdb like so: + + $ gdb ledger + (gdb) run + ... runs till crash ... + (gdb) bt + + Put that backtrace output, and the output from `ledger --version` + in the bug report. + +---------------------------------------------------------------------- + +Q: Whenever I try to use the Python support, I get a segfault + +A: Make sure that the boost_python library you linked against is using the + exact same Python as the Ledger executable. In particular I see this + bug on macOS systems where boost_python is linked against the default + Python, while Ledger is linked against the version provided by MacPorts. + Or vice versa. + + Solution: Use one or the other. If you prefer the system Python, run + `port deactivate -f python26`, to get MacPorts' version out of the way. + You'll then need to delete the Ledger binary and run `make` to relink + it. + +---------------------------------------------------------------------- + +Q: When I run `make check`, the Python unit tests always crash + +A: This can happen for the same reason as above. It can also happen if you + have ICU support enabled. This is a bug I'm still trying to track down. + +---------------------------------------------------------------------- + +Q: My distribution has versions of Boost and/or CMake that are too old for + Ledger. How do I build my own Boost and/or CMake binaries that will + work properly with Ledger? Thereafter, how do I configure Ledger + properly to use those newly built verisons of Boost and/or CMake? + +A: Here's commands that one user used to make this work, for Boost 1.51.0 + on Debian GNU/Linux 6.0.x (aka Debian squeeze). It's likely to work ok + for other versions of Boost as well. YMMV on other distributions and/or + other Debian distribution versions, though. + + - Preparing and building Boost + + $ export BOOST_VERSION=1.57.0 + $ cd /somewhere/you/want/to/build/boost + $ wget -N http://iweb.dl.sourceforge.net/project/boost/boost/$BOOST_VERSION/boost_${BOOST_VERSION//./_}.tar.bz2 + $ tar xvf boost_${BOOST_VERSION//./_}.tar.bz2 + $ cd boost_${BOOST_VERSION//./_} + $ ./bootstrap.sh + $ ./b2 --build-type=complete --layout=tagged --prefix=/where/you/want/boost/installed + $ ./b2 --build-type=complete --layout=tagged --prefix=/where/you/want/boost/installed install + + - Preparing and building CMake + + $ export CMAKE_VERSION=3.1.0 + $ cd /somewhere/you/want/to/build/cmake + $ wget -N http://www.cmake.org/files/v${CMAKE_VERSION:0:3}/cmake-${CMAKE_VERSION}.tar.gz + $ tar xvf cmake-${CMAKE_VERSION}.tar.gz + $ cd cmake-${CMAKE_VERSION} + $ ./configure --prefix=/where/you/want/cmake/installed/ + $ make + $ make install + + - Building Ledger using the CMake and/or Boost as installed above + + $ cd /path/to/ledger/sources + $ env PATH=/where/you/want/cmake/installed/bin:$PATH BOOST_ROOT=/where/you/want/boost/installed PREFIX=/where/you/want/ledger/installed $SHELL + $ ./acprep --prefix=$PREFIX --debug --python config + $ ./acprep --prefix=$PREFIX --debug --python make + $ ./acprep --prefix=$PREFIX --debug --python install diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..36dc7cfd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,28 @@ +Copyright (c) 2003-2019, John Wiegley. 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..d6b047ca --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +[![Join the chat at https://gitter.im/use-package/Lobby](https://badges.gitter.im/use-package/Lobby.svg)](https://gitter.im/use-package/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status master](https://img.shields.io/travis/ledger/ledger/master.svg?label=master&style=flat)](https://travis-ci.org/ledger/ledger) +[![Build Status next](https://img.shields.io/travis/ledger/ledger/next.svg?label=next&style=flat)](https://travis-ci.org/ledger/ledger) +[![Status](https://img.shields.io/badge/status-active-brightgreen.svg?style=flat)](https://github.com/ledger/ledger/pulse/monthly) +[![License](https://img.shields.io/badge/license-BSD-blue.svg?style=flat)](http://opensource.org/licenses/BSD-3-Clause) +[![GitHub release](https://img.shields.io/github/release/ledger/ledger.svg?style=flat)](https://github.com/ledger/ledger/releases) + +# Ledger: Command-Line Accounting + +Ledger is a powerful, double-entry accounting system that is accessed from the +UNIX command-line. This may put off some users, since there is no flashy UI, +but for those who want unparalleled reporting access to their data there are +few alternatives. + +Ledger uses text files for input. It reads the files and generates reports; +there is no other database or stored state. To use Ledger, you create a +file of your account names and transactions, run from the command line with +some options to specify input and requested reports, and get output. +The output is generally plain text, though you could generate a graph or +html instead. Ledger is simple in concept, surprisingly rich in ability, +and easy to use. + + +## For the Impatient + +I know, you just want to build and play. If you have all the [dependencies](#dependencies) +installed, then simply do this: + + $ git clone git://github.com/ledger/ledger.git + $ cd ledger && ./acprep update # Update to the latest, configure, make + +Now try your first ledger command: + + $ ./ledger -f test/input/sample.dat reg + +For help on keeping your journal have a look at the +[documentation] and the [wiki][] (Also see the “Resources” section at the +end of this file). An Emacs mode for Ledger files can be found in the +[ledger/ledger-mode repository] and a vim plugin is located in the +[ledger/vim-ledger repository]. + +## Docker version + +If you have Docker installed on your computer or server, you can use a [Docker version](https://hub.docker.com/r/dcycle/ledger/) of this software, without installing any further dependencies: + + $ docker run --rm -v "$PWD"/test/input:/data dcycle/ledger:1 -f /data/sample.dat reg + +## Dependencies + +If you wish to proceed in this venture, you'll need a few dependencies. The +easiest way to get them for your platform is to run this handy Python +script: + + $ ./acprep dependencies + +If that doesn't completely work, here are the dependencies for building the +current `master` branch: + +Dependency | Version (or greater) +-----------|--------------------- +[Boost] | 1.49 +[GMP] | 4.2.2 +[MPFR] | 2.4.0 +[utfcpp] | 2.3.4 +[gettext] | 0.17 _optional_ +[libedit] | 20090111-3.0 _optional_ +[Python] | 2.4 _optional_ +[doxygen] | 1.5.7.1 _optional_, for `make docs` +[graphviz] | 2.20.3 _optional_, for `make docs` +[texinfo] | 4.13 _optional_, for `make docs` +[lcov] | 1.6 _optional_, for `make report`, used with `/./acprep gcov` +[sloccount] | 2.26 _optional_, for `make sloc` + +### macOS + +You can use [Homebrew] or [MacPorts] to install Ledger easily on macOS. + +#### 1. Homebrew + +You can see the parameters you can pass while installing with brew by the command `brew options ledger`. To install ledger, simply type the following command: + + $ brew install ledger + +If you to want to startup python, use the following command: + + $ ledger python + + +#### 2. MacPorts + +If you build stuff using MacPorts on macOS, as I do, here is what you would +run: + + $ sudo port install -f cmake python26 \ + libiconv +universal zlib +universal gmp +universal \ + mpfr +universal ncurses +universal ncursesw +universal \ + gettext +universal libedit +universal boost-jam \ + boost +st+python26+icu texlive doxygen graphviz \ + texinfo lcov sloccount + +### Ubuntu + +If you're going to build on Ubuntu, `sudo apt-get install ...` the +following packages (current as of Ubuntu 18.04): + + $ sudo apt-get install build-essential cmake doxygen \ + libboost-system-dev libboost-dev python-dev gettext git \ + libboost-date-time-dev libboost-filesystem-dev \ + libboost-iostreams-dev libboost-python-dev libboost-regex-dev \ + libboost-test-dev libedit-dev libgmp3-dev libmpfr-dev texinfo tzdata + +### Debian + +Debian 9 (stretch), Debian 10 (buster), Debian testing and Debian unstable +(sid) contain all components needed to build ledger. You can install all +required build dependencies using the following command: + + $ sudo apt-get install build-essential cmake autopoint texinfo python-dev \ + zlib1g-dev libbz2-dev libgmp3-dev gettext libmpfr-dev \ + libboost-date-time-dev libboost-filesystem-dev \ + libboost-graph-dev libboost-iostreams-dev \ + libboost-python-dev libboost-regex-dev libboost-test-dev + +## Building + +The next step is preparing your environment for building. While you can use +`cmake .` and make, I've prepared a script that does a lot more of the +footwork for you: + + $ ./acprep update + # or, if you want to use the Boost libraries with suffix -mt, install in + # $HOME/local and build with 2 processes in parallel + $ ./acprep update --boost-suffix=-mt --prefix=$HOME/local -j2 + +Please read the contents of `CMakeFiles/CMakeOutput.log` and +`CMakeFiles/CMakeError.log` if the configure step fails. Also, +see the `help` subcommand to `acprep`, which explains some of its many +options. It's pretty much the only command I run for configuring, building +and testing Ledger. + +You can run `make check` to confirm the result, and `make install` to install. + +## Resources + +Now that you're up and running, here are a few resources to keep in mind: + + - [Homepage] + - [Documentation] + - [IRC channel][IRC] + - [Mailing List / Forum][mailing list] + - [GitHub project page][github] + - [Code analysis][openhub] + +If you have ideas you'd like to share, the best way is either to e-mail me a +patch (I prefer attachments over pasted text), or to get an account on GitHub. +Once you do, fork the [Ledger project][github], +hack as much as you like, then send me a pull request via GitHub. + +[Homepage]: http://ledger-cli.org/ +[documentation]: http://www.ledger-cli.org/docs.html +[mailing list]: http://list.ledger-cli.org/ +[wiki]: http://wiki.ledger-cli.org/ +[IRC]: irc://irc.freenode.net/ledger +[github]: http://github.com/ledger/ledger +[ledger/vim-ledger repository]: https://github.com/ledger/vim-ledger +[Homebrew]: http://brew.sh/ +[MacPorts]: https://www.macports.org/ +[Boost]: http://boost.org +[GMP]: http://gmplib.org/ +[MPFR]: http://www.mpfr.org/ +[utfcpp]: http://utfcpp.sourceforge.net +[gettext]: https://www.gnu.org/software/gettext/ +[libedit]: http://thrysoee.dk/editline/ +[Python]: http://python.org +[doxygen]: http://www.doxygen.org/ +[graphviz]: http://graphviz.org/ +[texinfo]: http://www.gnu.org/software/texinfo/ +[lcov]: http://ltp.sourceforge.net/coverage/lcov.php +[sloccount]: http://www.dwheeler.com/sloccount/ +[pcre]: http://www.pcre.org/ +[libofx]: http://libofx.sourceforge.net +[expat]: http://www.libexpat.org +[libxml2]: http://xmlsoft.org +[openhub]: https://www.openhub.net/p/ledger diff --git a/acprep b/acprep new file mode 100755 index 00000000..c5b4c203 --- /dev/null +++ b/acprep @@ -0,0 +1,1134 @@ +#!/usr/bin/env python + +# acprep, version 3.1 +# +# This script simply sets up the compiler and linker flags for all the various +# build permutations I use for testing and profiling. + +import inspect +import logging +import logging.handlers +import optparse +import os +import re +import shutil +import sys + +try: + import hashlib +except: + import md5 + +from os.path import * +from stat import * +from subprocess import Popen, PIPE, call + +LEVELS = {'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL} + +def which(program): + def is_exe(fpath): + return os.path.exists(fpath) and os.access(fpath, os.X_OK) + + def ext_candidates(fpath): + yield fpath + for ext in os.environ.get("PATHEXT", "").split(os.pathsep): + yield fpath + ext + + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + for candidate in ext_candidates(exe_file): + if is_exe(candidate): + return candidate + + return None + +class BoostInfo(object): + def dependencies(self, system): + if system in ['darwin-homebrew']: + return [ 'boost' ] + + if system in ['darwin-macports']: + return [ 'boost-jam', 'boost', '+python27+universal' ] + + if system in ['centos']: + return [ 'boost-devel' ] + + elif system in ['ubuntu-bionic', 'ubuntu-xenial', + 'ubuntu-trusty', 'ubuntu-cosmic']: + return [ 'libboost-dev', + 'libboost-date-time-dev', + 'libboost-filesystem-dev', + 'libboost-iostreams-dev', + 'libboost-python-dev', + 'libboost-regex-dev', + 'libboost-system-dev', + 'libboost-test-dev', + 'tzdata' ] + + elif system in [ 'ubuntu-saucy', 'ubuntu-precise']: + return [ 'autopoint', + 'libboost-dev', + 'libboost-test-dev', + 'libboost-regex-dev', + 'libboost-date-time-dev', + 'libboost-filesystem-dev', + 'libboost-iostreams-dev', + 'libboost-python-dev' ] + + elif system in ['ubuntu-lucid']: + return [ 'bjam', 'autopoint', + 'libboost-dev', + 'libboost-regex-dev', + 'libboost-date-time-dev', + 'libboost-filesystem-dev', + 'libboost-iostreams-dev', + 'libboost-python-dev' ] + + +class CommandLineApp(object): + "Base class for building command line applications." + + force_exit = True # If true, always ends run() with sys.exit() + log_handler = None + boost_major = "1_52" + + def __init__(self): + "Initialize CommandLineApp." + # Create the logger + self.log = logging.getLogger(os.path.basename(sys.argv[0])) + ch = logging.StreamHandler() + formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") + ch.setFormatter(formatter) + self.log.addHandler(ch) + self.log_handler = ch + + # Setup the options parser + usage = 'usage: %prog [OPTIONS...] [ARGS...]' + op = self.option_parser = optparse.OptionParser(usage = usage, + conflict_handler = 'resolve') + op.add_option('', '--debug', + action='store_true', dest='debug', + default=False, help='show debug messages and pass exceptions') + op.add_option('-v', '--verbose', + action='store_true', dest='verbose', + default=False, help='show informational messages') + op.add_option('-q', '--quiet', + action='store_true', dest='quiet', + default=False, help='do not show log messages on console') + op.add_option('', '--log', metavar='FILE', + type='string', action='store', dest='logfile', + default=False, help='append logging data to FILE') + op.add_option('', '--loglevel', metavar='LEVEL', + type='string', action='store', dest='loglevel', + default=False, help='set log level: DEBUG, INFO, WARNING, ERROR, CRITICAL') + + self.options = op.get_default_values() + + def main(self, *args): + """Main body of your application. + + This is the main portion of the app, and is run after all of the + arguments are processed. Override this method to implment the primary + processing section of your application.""" + pass + + def handleInterrupt(self): + """Called when the program is interrupted via Control-C or SIGINT. + Returns exit code.""" + self.log.error('Canceled by user.') + return 1 + + def handleMainException(self): + "Invoked when there is an error in the main() method." + if not self.options.debug: + self.log.exception('Caught exception') + return 1 + + ## INTERNALS (Subclasses should not need to override these methods) + + def run(self): + """Entry point. + + Process options and execute callback functions as needed. This method + should not need to be overridden, if the main() method is defined.""" + # Process the options supported and given + self.options, main_args = self.option_parser.parse_args(values=self.options) + + if self.options.logfile: + fh = logging.handlers.RotatingFileHandler(self.options.logfile, + maxBytes = (1024 * 1024), + backupCount = 5) + formatter = logging.Formatter("%(asctime)s - %(levelname)s: %(message)s") + fh.setFormatter(formatter) + self.log.addHandler(fh) + + if self.options.quiet: + self.log.removeHandler(self.log_handler) + ch = logging.handlers.SysLogHandler() + formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") + ch.setFormatter(formatter) + self.log.addHandler(ch) + self.log_handler = ch + + if self.options.loglevel: + self.log.setLevel(LEVELS[self.options.loglevel]) + elif self.options.debug: + self.log.setLevel(logging.DEBUG) + elif self.options.verbose: + self.log.setLevel(logging.INFO) + + exit_code = 0 + try: + # We could just call main() and catch a TypeError, but that would + # not let us differentiate between application errors and a case + # where the user has not passed us enough arguments. So, we check + # the argument count ourself. + argspec = inspect.getargspec(self.main) + expected_arg_count = len(argspec[0]) - 1 + + if len(main_args) >= expected_arg_count: + exit_code = self.main(*main_args) + else: + self.log.debug('Incorrect argument count (expected %d, got %d)' % + (expected_arg_count, len(main_args))) + self.option_parser.print_help() + exit_code = 1 + + except KeyboardInterrupt: + exit_code = self.handleInterrupt() + + except SystemExit as msg: + exit_code = msg.args[0] + + except Exception: + exit_code = self.handleMainException() + if self.options.debug: + raise + + if self.force_exit: + sys.exit(exit_code) + return exit_code + + +class PrepareBuild(CommandLineApp): + ######################################################################### + # Initialization routines # + ######################################################################### + + def initialize(self): + self.log.debug('Initializing all state variables') + + self.should_clean = False + self.configured = False + self.current_ver = None + #self.current_flavor = 'default' + self.current_flavor = 'debug' + self.products_dir = None + self.configure_args = [] + self.CXXFLAGS = [] + self.LDFLAGS = [] + + self.envvars = { + 'CXX': 'g++', + 'CXXFLAGS': '', + 'LDFLAGS': '', + } + + for varname in self.envvars.keys(): + if varname in os.environ: + self.envvars[varname] = os.environ[varname] + + if varname.endswith('FLAGS'): + self.__dict__[varname] = str.split(os.environ[varname]) + self.envvars[varname] = '' + + # If ~/Products/ or build/ exists, use them instead of the source tree + # for building + products = self.default_products_directory() + if (exists(products) and isdir(products)) or \ + (exists('build') and isdir('build')): + self.options.build_dir = None + + def __init__(self): + CommandLineApp.__init__(self) + self.log.setLevel(logging.INFO) + + self.source_dir = os.getcwd() + + op = self.option_parser + + op.add_option('', '--help', action="callback", + callback=self.option_help, + help='Show this help text') + op.add_option('-j', '--jobs', metavar='N', + type='int', action='store', dest='jobs', + default=1, help='Allow N make jobs at once') + + op.add_option('', '--boost', metavar='BOOST_ROOT', + action="store", dest="boost_root", + help='Set Boost library root (ex: "--boost=/usr/local")') + op.add_option('', '--boost-suffix', metavar='BOOST_SUFFIX', + action="store", dest="boost_suffix", + help='Set Boost library suffix (ex: "--boost-suffix=-mt")') + op.add_option('', '--boost-include', metavar='BOOST_INCLUDE', + action="store", dest="boost_include", + help='Set Boost include path (ex: "--boost-include=DIR")') + + op.add_option('', '--compiler', metavar='COMPILER', + action="store", dest="compiler", + help='Use the Clang C++ compiler') + op.add_option('', '--cxx', metavar='COMPILER', + action="store", dest="compiler", + help='Use the Clang C++ compiler') + + op.add_option('-N', '--ninja', action='store_true', dest='use_ninja', + default=False, + help='Use ninja to build, rather than make') + op.add_option('', '--no-git', action='store_true', dest='no_git', + default=False, + help='Do not call out to Git; useful for offline builds') + op.add_option('', '--doxygen', action='store_true', + dest='enable_doxygen', default=False, + help='Enable use of Doxygen to build ref manual ("make docs")') + op.add_option('', '--python', action='store_true', dest='python', + default=False, + help='Enable Python support') + op.add_option('', '--no-python', action='store_false', dest='python', + help='Disable python support (default)') + op.add_option('', '--prefix', metavar='DIR', action="store", + dest="prefix_dir", help='Use custom installation prefix') + op.add_option('', '--products', metavar='DIR', action="store", + dest="option_products", + help='Collect all build products in this directory') + op.add_option('', '--output', metavar='DIR', action="store", + default=self.source_dir, + dest="build_dir", help='Build in the specified directory') + op.add_option('', '--local', action="callback", + callback=self.option_local, + help='Build directly within the source tree (default)') + + self.options = op.get_default_values() + + self.initialize() + + def main(self, *args): + if args and args[0] in ['default', 'debug', 'opt', 'gcov', 'gprof']: + self.current_flavor = args[0] + args = args[1:] + + if args: + cmd = args[0] + if 'phase_' + cmd not in PrepareBuild.__dict__: + self.log.error("Unknown build phase: " + cmd + "\n") + sys.exit(1) + else: + args = args[1:] + else: + cmd = 'config' + + self.log.info('Invoking primary phase: ' + cmd) + PrepareBuild.__dict__['phase_' + cmd](self, *args) + + ######################################################################### + # General utility code # + ######################################################################### + + def execute(self, *args): + try: + self.log.debug('Executing command: ' + ' '.join(args)) + + retcode = call(args, shell=False) + if retcode < 0: + self.log.error("Child was terminated by signal", -retcode) + sys.exit(1) + elif retcode != 0: + self.log.error("Execution failed: " + ' '.join(args)) + sys.exit(1) + except OSError as e: + self.log.error("Execution failed: " + e) + sys.exit(1) + + def get_stdout(self, *args): + try: + self.log.debug('Executing command: ' + ' '.join(args)) + + proc = Popen(args, shell=False, stdout=PIPE) + stdout = proc.stdout.read() + retcode = proc.wait() + if retcode < 0: + self.log.error("Child was terminated by signal", + -retcode) + sys.exit(1) + elif retcode != 0: + self.log.error("Execution failed: " + ' '.join(args)) + sys.exit(1) + return stdout[:-1] + except OSError as e: + self.log.error("Execution failed:" + e) + sys.exit(1) + + def isnewer(self, file1, file2): + "Check if file1 is newer than file2." + if not exists(file2): + return True + return os.stat(file1)[ST_MTIME] > os.stat(file2)[ST_MTIME] + + ######################################################################### + # Determine information about the surroundings # + ######################################################################### + + def prefix_directory(self): + if self.options.prefix_dir: + return self.options.prefix_dir + else: + return None + + def default_products_directory(self): + return join(os.environ['HOME'], "Products") + + def products_directory(self): + if not self.products_dir: + products = self.default_products_directory() + + if not exists(products) or not isdir(products): + products = join(self.source_dir, 'build') + + products = join(products, basename(self.source_dir)) + + self.products_dir = products + + return self.products_dir + + def build_directory(self): + if not self.options.build_dir: + self.options.build_dir = join(self.products_directory(), + self.current_flavor) + return self.options.build_dir + + def ensure(self, dirname): + if not exists(dirname): + self.log.info('Making directory: ' + dirname) + os.makedirs(dirname) + elif not isdir(dirname): + self.log.error('Directory is not a directory: ' + dirname) + sys.exit(1) + return dirname + + def git_working_tree(self): + return exists('.git') and isdir('.git') and not self.options.no_git + + def current_version(self): + if not self.current_ver: + major, minor, patch, date = None, None, None, None + + version_m4 = open('CMakeLists.txt', 'r') + for line in version_m4.readlines(): + match = re.match('^set\(Ledger_VERSION_MAJOR ([0-9]+)\)', line) + if match: + major = match.group(1) + match = re.match('^set\(Ledger_VERSION_MINOR ([0-9]+)\)', line) + if match: + minor = match.group(1) + match = re.match('^set\(Ledger_VERSION_PATCH ([0-9]+)\)', line) + if match: + patch = match.group(1) + match = re.match('^set\(Ledger_VERSION_DATE ([0-9]+)\)', line) + if match: + date = match.group(1) + break + self.current_ver = "%s.%s.%s%s" % (major, minor, patch, + "-%s" % date if date else "") + version_m4.close() + return self.current_ver + + def phase_products(self, *args): + self.log.info('Executing phase: products') + print(self.products_directory()) + + def phase_info(self, *args): + self.log.info('Executing phase: info') + + environ, conf_args = self.configure_environment() + + self.log.info("Current version => " + self.current_version()) + self.log.info("Current flavor => " + self.current_flavor) + self.log.info("Source directory => " + self.source_dir) + if self.prefix_directory(): + self.log.info("Installation prefix => " + self.prefix_directory()) + self.log.info("Products directory => " + self.products_directory()) + self.log.info("Build directory => " + self.build_directory()) + + self.log.debug('CMake environment =>') + + keys = environ.keys() + keys.sort() + for key in keys: + if key in ['PATH', 'CXX'] or key.endswith('FLAGS'): + self.log.debug(' %s=%s' % (key, environ[key])) + + self.log.debug('CMake arguments =>') + + for arg in conf_args + list(args): + self.log.debug(' %s' % arg) + + def phase_sloc(self, *args): + self.log.info('Executing phase: sloc') + self.execute('sloccount', 'src', 'python', 'test') + + ######################################################################### + # Update local files with the latest information # + ######################################################################### + + def phase_pull(self, *args): + self.log.info('Executing phase: pull') + if self.git_working_tree(): + self.execute('git', 'pull') + + ######################################################################### + # Automatic installation of build dependencies # + ######################################################################### + + def phase_dependencies(self, *args): + self.log.info('Executing phase: dependencies') + + self.log.info("Installing Ledger's build dependencies ...") + + system = self.get_stdout('uname', '-s').decode('utf8') + + if system == 'Darwin': + if exists('/opt/local/bin/port'): + self.log.info('Looks like you are using MacPorts on macOS') + packages = [ + 'sudo', 'port', 'install', '-f', + 'automake', 'autoconf', 'libtool', + 'python27', '+universal', + 'libiconv', '+universal', + 'zlib', '+universal', + 'gmp' ,'+universal', 'mpfr', '+universal', + 'ncurses', '+universal', + 'gettext' ,'+universal', + 'libedit' ,'+universal', + 'texlive-xetex', 'doxygen', 'graphviz', 'texinfo', + 'lcov', 'sloccount' + ] + BoostInfo().dependencies('darwin-macports') + self.log.info('Executing: ' + ' '.join(packages)) + self.execute(*packages) + elif exists('/usr/local/bin/brew') or exists('/opt/local/bin/brew'): + self.log.info('Looks like you are using Homebrew on macOS') + packages = [ + 'brew', 'install', + 'cmake', 'ninja', + 'mpfr', 'gmp', + ] + BoostInfo().dependencies('darwin-homebrew') + self.log.info('Executing: ' + ' '.join(packages)) + self.execute(*packages) + elif exists('/sw/bin/fink'): + self.log.info('Looks like you are using Fink on macOS') + self.log.error("I don't know the package names for Fink yet!") + sys.exit(1) + + elif system == 'Linux': + if exists('/etc/issue'): + issue = open('/etc/issue') + if issue.readline().startswith('Ubuntu'): + info = dict([line.strip().split('=', 1) + for line in open('/etc/lsb-release')]) + release = info['DISTRIB_CODENAME'] + self.log.info('Looks like you are using APT on Ubuntu ' + release) + packages = [ + 'sudo', 'apt-get', 'install', + 'build-essential', + ] + + if release == 'bionic': + packages.extend([ + 'doxygen', + 'cmake', + 'ninja-build', + 'zlib1g-dev', + 'libbz2-dev', + 'python-dev', + 'libgmp3-dev', + 'libmpfr-dev', + 'gettext', + 'libedit-dev', + 'texinfo', + 'lcov', + 'libutfcpp-dev', + 'sloccount' + ]) + elif release == 'trusty': + packages.extend([ + 'doxygen', + 'cmake', + 'ninja-build', + 'zlib1g-dev', + 'libbz2-dev', + 'python-dev', + 'libgmp3-dev', + 'libmpfr-dev', + 'gettext', + 'libedit-dev', + 'texinfo', + 'lcov', + 'libutfcpp-dev', + 'sloccount' + ]) + elif release == 'xenial': + packages.extend([ + 'doxygen', + 'cmake', + 'ninja-build', + 'zlib1g-dev', + 'libbz2-dev', + 'python-dev', + 'libgmp3-dev', + 'libmpfr-dev', + 'gettext', + 'libedit-dev', + 'texinfo', + 'lcov', + 'libutfcpp-dev', + 'sloccount' + ]) + elif release == 'saucy': + packages.extend([ + 'doxygen', + 'cmake', + 'ninja-build', + 'zlib1g-dev', + 'libbz2-dev', + 'python-dev', + 'libgmp-dev', + 'libmpfr-dev', + 'gettext', + 'libedit-dev', + 'texinfo', + 'lcov', + 'sloccount' + ]) + elif release == 'precise': + packages.extend([ + 'libtool', + 'cmake', + 'zlib1g-dev', + 'libbz2-dev', + 'python-dev', + 'libgmp-dev', + 'libmpfr-dev', + 'gettext', + 'libedit-dev', + 'texinfo', + 'lcov', + 'libutfcpp-dev', + 'sloccount' + ]) + else: + self.log.info('I do not recognize your version of Ubuntu!') + packages = None + if packages: + packages.extend( + BoostInfo().dependencies('ubuntu-' + release)) + self.log.info('Executing: ' + ' '.join(packages)) + self.execute(*packages) + + if exists('/etc/redhat-release'): + release = open('/etc/redhat-release').readline() + if release.startswith('CentOS'): + self.log.info('Looks like you are using YUM on CentOS') + packages = [ + 'sudo', 'yum', 'install', + 'gcc', + 'gcc-c++', + 'compat-gcc-*', + 'make', + 'libtool', + 'autoconf', + 'automake', + 'zlib-devel', + 'bzip2-devel', + 'python-devel', + 'gmp-devel', + 'gettext-devel', + #'mpfr-devel' + 'libedit-devel', + #'texlive-full', + #'doxygen', + #'graphviz', + 'texinfo', + #'lcov', + #'sloccount' + ] + self.log.info('Executing: ' + ' '.join(packages)) + self.execute(*packages) + elif release.startswith('Fedora release 20'): + self.log.info('Looks like you are using YUM on Fedora 20') + packages = [ + 'sudo', 'yum', 'install', + 'boost-devel', + 'bzip2-devel', + 'cmake', + 'doxygen', + 'gcc', + 'gcc-c++', + 'gettext', + 'gettext-devel', + 'gmp-devel', + 'lcov', + 'libedit-devel', + 'mpfr-devel', + 'ninja-build', + 'python-devel', + 'sloccount', + 'texinfo', + 'zlib-devel' + ] + self.log.info('Executing: ' + ' '.join(packages)) + self.execute(*packages) + + elif system.startswith('CYGWIN'): + self.log.info('Looks like you are using Cygwin') + self.log.info('Please install the dependencies manually.') + + ######################################################################### + # Determine the system's basic configuration # + ######################################################################### + + def setup_for_johnw(self): + self.configure_args.append('-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=ON') + + if not self.options.compiler: + self.configure_args.append('-DCMAKE_CXX_COMPILER:PATH=/usr/local/bin/clang++') + + if self.current_flavor == 'opt': + self.configure_args.append('-DCMAKE_CXX_FLAGS_RELEASE:STRING=-O3') + self.configure_args.append('-DCMAKE_EXE_LINKER_FLAGS:STRING=-O3') + self.configure_args.append('-DCMAKE_SHARED_LINKER_FLAGS:STRING=-O3') + self.configure_args.append('-DCMAKE_MODULE_LINKER_FLAGS:STRING=-O3') + #else: + # self.CXXFLAGS.append('-g -O1 -faddress-sanitizer') + # self.LDFLAGS.append('-g -O1 -faddress-sanitizer') + + self.configure_args.append(self.source_dir) + + else: + self.configure_args.append('-DCMAKE_CXX_COMPILER:PATH=' + self.options.compiler) + self.configure_args.append('-DCMAKE_INCLUDE_PATH:STRING=/usr/local/include') + self.configure_args.append('-DCMAKE_LIBRARY_PATH:STRING=/usr/local/lib') + self.configure_args.append('-DBOOST_ROOT=/usr/local') + self.configure_args.append(self.source_dir) + + def setup_for_system(self): + system = str(self.get_stdout('uname', '-s')) + self.log.info('System type is => ' + system) + + if self.options.enable_doxygen: + self.configure_args.append('-DUSE_DOXYGEN=1') + if self.options.python: + self.configure_args.append('-DUSE_PYTHON=1') + + if system.startswith('CYGWIN'): + self.configure_args.append('-G') + self.configure_args.append('Unix Makefiles') + elif self.options.use_ninja: + self.configure_args.append('-GNinja') + + if exists('/Users/johnw/Projects/ledger/plan/TODO'): + self.setup_for_johnw() + + def setup_flavor(self): + self.setup_for_system() + + if 'setup_flavor_' + self.current_flavor not in PrepareBuild.__dict__: + self.log.error('Unknown build flavor "%s"' % self.current_flavor) + sys.exit(1) + + self.log.info('Setting up build flavor => ' + self.current_flavor) + PrepareBuild.__dict__['setup_flavor_' + self.current_flavor](self) + + def escape_string(self, data): + return re.sub('(["\\\\])', '\\\\\\1', data) + + def finalize_config(self): + self.setup_flavor() + + for var in ('CXXFLAGS', 'LDFLAGS'): + value = self.__dict__[var] + if value: + first = not self.envvars[var] + for member in value: + #escaped = self.escape_string(member) + #if member != escaped: + # member = escaped + if first: + first = False + else: + self.envvars[var] += ' ' + self.envvars[var] += member + self.log.debug('Final value of %s: %s' % + (var, self.envvars[var])) + + elif var in self.envvars: + del self.envvars[var] + + ######################################################################### + # Options that can modify any build flavor # + ######################################################################### + + def option_local(self, option=None, opt_str=None, value=None, parser=None): + self.log.debug('Saw option --local') + self.options.build_dir = self.source_dir + + def option_help(self, option=None, opt_str=None, value=None, parser=None): + self.phase_help() + + ######################################################################### + # The various build flavors # + ######################################################################### + + def setup_flavor_default(self): + pass + + def setup_flavor_debug(self): + self.configure_args.append('-DBUILD_DEBUG=1') + + def setup_flavor_opt(self): + self.configure_args.append('-DNO_ASSERTS=1') + + def setup_flavor_gcov(self): + # NO_ASSERTS is set so that branch coverage ignores the never-taken + # else branch inside assert statements. + self.configure_args.append('-DBUILD_DEBUG=1') + self.configure_args.append('-DNO_ASSERTS=1') + self.configure_args.append('-DCLANG_GCOV=1') + + self.CXXFLAGS.append('-fprofile-arcs') + self.CXXFLAGS.append('-ftest-coverage') + self.LDFLAGS.append('-fprofile-arcs') + self.LDFLAGS.append('-ftest-coverage') + + if not self.options.compiler or self.options.compiler == "clang-3.1": + self.LDFLAGS.append('-lgcov') + + def setup_flavor_gprof(self): + self.configure_args.append('-DBUILD_DEBUG=1') + + self.CXXFLAGS.append('-pg') + self.LDFLAGS.append('-pg') + + ######################################################################### + # Configure build tree using CMake # + ######################################################################### + + def configure_environment(self): + self.finalize_config() + + environ = dict(os.environ) + for key, value in self.envvars.items(): + if value: + environ[key] = value + + if self.build_directory() == self.source_dir: + conf_args = ['cmake'] + else: + conf_args = ['cmake', self.source_dir] + + if not which('cmake'): + self.log.error("Cannot find CMake, please check your PATH") + sys.exit(1) + + for var in ('CXX', 'CXXFLAGS', 'LDFLAGS'): + if self.envvars.get(var) and (var.endswith('FLAGS') + or exists(self.envvars[var])): + if var == 'CXX': + conf_args.append('-DCMAKE_CXX_COMPILER=%s' % + self.envvars[var]) + elif var == 'CXXFLAGS': + conf_args.append('-DCMAKE_CXX_FLAGS=%s' % + self.envvars[var]) + elif var == 'LDFLAGS': + conf_args.append('-DCMAKE_EXE_LINKER_FLAGS=%s' % + self.envvars[var]) + + if self.options.boost_root: + conf_args.append('-DBOOST_ROOT=%s' % + self.options.boost_root) + conf_args.append('-DBoost_NO_SYSTEM_PATHS=TRUE') + if self.options.boost_suffix: + conf_args.append('-DBoost_COMPILER=%s' % + self.options.boost_suffix) + if self.options.boost_include: + conf_args.append('-DBOOST_INCLUDEDIR=%s' % + self.options.boost_include) + + if self.prefix_directory(): + conf_args.append('-DCMAKE_INSTALL_PREFIX=%s' % self.prefix_directory()) + + return (environ, conf_args + self.configure_args) + + def phase_configure(self, *args): + self.log.info('Executing phase: configure') + + self.configured = True + + environ, conf_args = self.configure_environment() + for arg in args: + if arg: conf_args.append(arg) + + build_dir = self.ensure(self.build_directory()) + try: + os.chdir(build_dir) + + need_to_config = not isfile('rules.ninja' if self.options.use_ninja else 'Makefile') + if need_to_config: + self.log.debug('Source => ' + self.source_dir) + self.log.debug('Build => ' + build_dir) + self.log.debug('configure env => ' + str(environ)) + self.log.debug('configure args => ' + str(conf_args)) + + configure = Popen(conf_args, shell=False, env=environ) + retcode = configure.wait() + if retcode < 0: + self.log.error("Child was terminated by signal", -retcode) + sys.exit(1) + elif retcode != 0: + self.log.error("Execution failed: " + ' '.join(conf_args)) + sys.exit(1) + else: + self.log.debug('configure does not need to be run') + + finally: + os.chdir(self.source_dir) + + def phase_config(self, *args): + self.log.info('Executing phase: config') + self.phase_configure(*args) + if self.should_clean: + self.phase_clean() + + ######################################################################### + # Builds products from the sources # + ######################################################################### + + def phase_make(self, *args): + self.log.info('Executing phase: make') + + config_args = [] + make_args = [] + + for arg in args: + if arg.startswith('--') or arg.startswith('-D'): + config_args.append(arg) + else: + make_args.append(arg) + + if self.options.jobs > 1 and self.current_flavor != 'gcov': + make_args.append('-j%d' % self.options.jobs) + + if self.options.verbose: + make_args.append('-v' if self.options.use_ninja else 'VERBOSE=1') + + self.log.debug('Configure arguments => ' + str(config_args)) + self.log.debug('Makefile arguments => ' + str(make_args)) + + if not self.configured: + self.phase_config(*config_args) + + build_dir = self.ensure(self.build_directory()) + try: + self.log.debug('Changing directory to ' + build_dir) + os.chdir(build_dir) + + self.execute(*(['ninja' if self.options.use_ninja else 'make'] + + make_args)) + finally: + os.chdir(self.source_dir) + + def phase_check(self, *args): + self.log.info('Executing phase: check') + build_dir = self.ensure(self.build_directory()) + try: + self.log.debug('Changing directory to ' + build_dir) + os.chdir(build_dir) + + make_args = list(args) + if self.options.jobs > 1: + make_args.append('-j%d' % self.options.jobs) + + self.execute(*(['ctest'] + list(make_args))) + finally: + os.chdir(self.source_dir) + + def phase_update(self, *args): + self.log.info('Executing phase: update') + self.phase_pull() + self.phase_make(*args) + + ######################################################################### + # Build directory cleaning phases # + ######################################################################### + + def phase_clean(self, *args): + self.log.info('Executing phase: clean') + self.phase_make('clean') + + def phase_gitclean(self, *args): + self.log.info('Executing phase: gitclean') + if self.git_working_tree(): + self.execute('git', 'clean', '-dfx') + + ######################################################################### + # Other build phases # + ######################################################################### + + def configure_flavor(self, flavor, reset=True): + self.initialize() # reset everything + self.current_flavor = flavor + self.options.build_dir = None # use the build/ tree + self.options.prefix_dir = None + + if reset and exists(self.build_directory()) and \ + isdir(self.build_directory()): + self.log.info('=== Wiping build directory %s ===' % + self.build_directory()) + try: + shutil.rmtree(self.build_directory()) + except: + self.execute('chmod', '-R', 'u+w', self.build_directory()) + self.execute('rm', '-fr', self.build_directory()) + + def phase_rsync(self, *args): + self.log.info('Executing phase: rsync') + + proof_dir = 'ledger-proof' + + if self.options.python: + proof_dir += "-python" + if self.options.compiler: + proof_dir += "-" + basename(self.options.compiler) + + source_copy_dir = join(self.ensure(self.products_directory()), proof_dir) + + self.execute('rsync', '-a', '--delete', '--exclude=/dist/', + '--exclude=.git/', '--exclude=b/', + '--exclude=/lib/boost-release/', + '--exclude=/archive/', '--exclude=/build/', + '%s/' % self.source_dir, '%s/' % source_copy_dir) + + self.source_dir = source_copy_dir + + def phase_proof(self, *args): + self.log.info('Executing phase: proof') + + self.log.info('=== Copying source tree ===') + self.phase_rsync() + + self.phase_makeall(reset=True, *args) + + self.configure_flavor('opt', reset=False) + self.log.info('=== Testing opt ===') + # jww (2012-05-20): Can't use fullcheck yet + #self.phase_make('fullcheck') + self.phase_make('test') + + self.configure_flavor('gcov', reset=False) + self.log.info('=== Testing gcov ===') + #self.phase_make('check') + self.phase_make('test') + + self.configure_flavor('default', reset=False) + self.log.info('=== Testing default ===') + #self.phase_make('fullcheck') + self.phase_make('test') + # jww (2012-05-20): docs are not working yet + #self.phase_make('docs') + + self.configure_flavor('debug', reset=False) + self.log.info('=== Testing debug ===') + #self.phase_make('fullcheck') + self.phase_make('test') + + def phase_makeall(self, reset=False, *args): + self.log.info('Executing phase: makeall') + + self.configure_flavor('opt', reset) + self.log.info('=== Building opt ===') + self.phase_make(*args) + + self.configure_flavor('gcov', reset) + self.log.info('=== Building gcov ===') + self.phase_make(*args) + + self.configure_flavor('default', reset) + self.log.info('=== Building default ===') + self.phase_make(*args) + + self.configure_flavor('debug', reset) + self.log.info('=== Building debug ===') + self.phase_make(*args) + + ######################################################################### + # Help # + ######################################################################### + + def phase_help(self, *args): + self.option_parser.print_help() + + print(""" +Of the optional ARGS, the first is an optional build FLAVOR, with the default +being 'debug': + + default Regular autoconf settings + debug Debugging and --verify support (default) + opt Full optimizations + gcov Coverage analysis + gprof Code profiling (for macOS, just use: 'shark -i ledger ...') + +Next is the optional build PHASE, with 'config' being the default: + + clean Runs 'make clean' in the build directory + config Configure the environment for building + dependencies Automatically install all necessary build dependencies + gitclean Runs 'git clean -dfx', which *really* cleans things + help Displays this help text + info Show information about the build environment + make Do a make in the build directory + proof Proves Ledger by building and testing every flavor + pull Pulls the latest, and updates local config if need be + update Does it all, updates your environment and re-make's + +There are many other build phases, though most are not of interest to the +typical user: + + configure Runs just cmake + do_all Runs makeall followed by proof + gettext Initialize gettext support + makeall Build every flavor there is + products Report the products directory path + rsync Rsync a copy of the source tree into Products + sloc Report total Source Lines Of Code + version Output current HEAD version to version.m4 + +NOTE: If you wish to pass options to CMake or make, add "--" followed by +your options. Those starting with "-D" or "--" will be passed on to CMake, +positional arguments and other options will be passed to make. +For the 'config' and 'configure' phase everything will be passed to CMake. + +Here are some real-world examples: + + ./acprep + ./acprep --python + ./acprep opt make + ./acprep make doc -- -DBUILD_WEB_DOCS=1""") + sys.exit(0) + +PrepareBuild().run() diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..3b2e7267 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,50 @@ +version: build-{build} + +image: Visual Studio 2013 + +shallow_clone: true # don't download repo history + +install: +- time /t +- tzutil /s "Eastern Standard Time_dstoff" +- time /t + +branches: + only: + - master + - next + +build_script: +- C:\msys64\usr\bin\bash -lc "pacman --noconfirm -Sy pacman" +- C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors" +- C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -Syu" +- C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-i686-boost" +- C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-i686-mpfr" +- C:\msys64\usr\bin\bash -lc "pacman --needed --noconfirm -S mingw-w64-i686-cmake" +- C:\msys64\usr\bin\bash -lc "export PATH=/mingw32/bin:$PATH && cd $APPVEYOR_BUILD_FOLDER && + cmake -G 'MSYS Makefiles'" +- C:\msys64\usr\bin\bash -lc "export PATH=/mingw32/bin:$PATH && cd $APPVEYOR_BUILD_FOLDER && + make -j2" + +after_build: +- set LIB_DIR=C:\msys64\mingw32\bin +- 7z a ledger-win.zip %APPVEYOR_BUILD_FOLDER%\ledger.exe + %LIB_DIR%\libboost_filesystem-mt.dll + %LIB_DIR%\libboost_regex-mt.dll + %LIB_DIR%\libboost_system-mt.dll + %LIB_DIR%\libgcc_s_dw2-1.dll + %LIB_DIR%\libgmp-10.dll + %LIB_DIR%\libicudt57.dll + %LIB_DIR%\libicuuc57.dll + %LIB_DIR%\libstdc++-6.dll + %LIB_DIR%\libwinpthread-1.dll + %APPVEYOR_BUILD_FOLDER%\libledger.dll + +artifacts: + - path: ledger-win.zip + name: Ledger Win32 binaries + +test_script: +- C:\msys64\usr\bin\bash -lc "export MINGW_PREFIX=C:/msys64/mingw32/ CTEST_OUTPUT_ON_FAILURE=1 + PATH=/mingw32/bin:$PATH && cd $APPVEYOR_BUILD_FOLDER && + make test || echo Errors from tests were ignored" diff --git a/cmake/FindUtfcpp.cmake b/cmake/FindUtfcpp.cmake new file mode 100644 index 00000000..93828ef4 --- /dev/null +++ b/cmake/FindUtfcpp.cmake @@ -0,0 +1,31 @@ +# - Try to find utfcpp +# Once done, this will define +# +# UTFCPP_FOUND - system has utfcpp's utf8.h +# UTFCPP_PATH - the utfcpp include directories + +include(CheckCXXSourceCompiles) + +set(UTFCPP_FOUND FALSE) + +find_path(UTFCPP_INCLUDE_DIR + NAMES utf8.h + HINTS "${UTFCPP_PATH}" + PATHS "${PROJECT_SOURCE_DIR}/lib/utfcpp/v2_0/source" +) + +if (UTFCPP_INCLUDE_DIR) + set(CMAKE_REQUIRED_INCLUDES "${UTFCPP_INCLUDE_DIR}") + set(UTFCPP_FOUND TRUE) +endif() + +check_cxx_source_compiles(" +#include +#include \"utf8.h\" + +int main(int argc, char** argv) { + std::string input = std::string(\"utfcpp\"); + const char * p = input.c_str(); + std::size_t len = input.length(); + utf8::is_valid(p, p + len); +}" HAVE_WORKING_UTFCPP) diff --git a/contrib/CSVReader.cs b/contrib/CSVReader.cs new file mode 100644 index 00000000..a22eab06 --- /dev/null +++ b/contrib/CSVReader.cs @@ -0,0 +1,165 @@ +// This code is in the public domain. I can't remember where I found it on the Web, but it +// didn't come with any license. + +using System; +using System.Collections; +using System.IO; +using System.Text; + +namespace CSVReader { + + /// + /// A data-reader style interface for reading CSV files. + /// + public class CSVReader : IDisposable { + + #region Private variables + + private Stream stream; + private StreamReader reader; + + #endregion + + /// + /// Create a new reader for the given stream. + /// + /// The stream to read the CSV from. + public CSVReader(Stream s) : this(s, null) { } + + /// + /// Create a new reader for the given stream and encoding. + /// + /// The stream to read the CSV from. + /// The encoding used. + public CSVReader(Stream s, Encoding enc) { + + this.stream = s; + if (!s.CanRead) { + throw new CSVReaderException("Could not read the given CSV stream!"); + } + reader = (enc != null) ? new StreamReader(s, enc) : new StreamReader(s); + } + + /// + /// Creates a new reader for the given text file path. + /// + /// The name of the file to be read. + public CSVReader(string filename) : this(filename, null) { } + + /// + /// Creates a new reader for the given text file path and encoding. + /// + /// The name of the file to be read. + /// The encoding used. + public CSVReader(string filename, Encoding enc) + : this(new FileStream(filename, FileMode.Open), enc) { } + + /// + /// Returns the fields for the next row of CSV data (or null if at eof) + /// + /// A string array of fields or null if at the end of file. + public string[] GetCSVLine() { + + string data = reader.ReadLine(); + if (data == null) return null; + if (data.Length == 0) return new string[0]; + + ArrayList result = new ArrayList(); + + ParseCSVFields(result, data); + + return (string[])result.ToArray(typeof(string)); + } + + // Parses the CSV fields and pushes the fields into the result arraylist + private void ParseCSVFields(ArrayList result, string data) { + + int pos = -1; + while (pos < data.Length) + result.Add(ParseCSVField(data, ref pos)); + } + + // Parses the field at the given position of the data, modified pos to match + // the first unparsed position and returns the parsed field + private string ParseCSVField(string data, ref int startSeparatorPosition) { + + if (startSeparatorPosition == data.Length-1) { + startSeparatorPosition++; + // The last field is empty + return ""; + } + + int fromPos = startSeparatorPosition + 1; + + // Determine if this is a quoted field + if (data[fromPos] == '"') { + // If we're at the end of the string, let's consider this a field that + // only contains the quote + if (fromPos == data.Length-1) { + fromPos++; + return "\""; + } + + // Otherwise, return a string of appropriate length with double quotes collapsed + // Note that FSQ returns data.Length if no single quote was found + int nextSingleQuote = FindSingleQuote(data, fromPos+1); + startSeparatorPosition = nextSingleQuote+1; + return data.Substring(fromPos+1, nextSingleQuote-fromPos-1).Replace("\"\"", "\""); + } + + // The field ends in the next comma or EOL + int nextComma = data.IndexOf(',', fromPos); + if (nextComma == -1) { + startSeparatorPosition = data.Length; + return data.Substring(fromPos); + } + else { + startSeparatorPosition = nextComma; + return data.Substring(fromPos, nextComma-fromPos); + } + } + + // Returns the index of the next single quote mark in the string + // (starting from startFrom) + private int FindSingleQuote(string data, int startFrom) { + + int i = startFrom-1; + while (++i < data.Length) + if (data[i] == '"') { + // If this is a double quote, bypass the chars + if (i < data.Length-1 && data[i+1] == '"') { + i++; + continue; + } + else + return i; + } + // If no quote found, return the end value of i (data.Length) + return i; + } + + /// + /// Disposes the CSVReader. The underlying stream is closed. + /// + public void Dispose() { + // Closing the reader closes the underlying stream, too + if (reader != null) reader.Close(); + else if (stream != null) + stream.Close(); // In case we failed before the reader was constructed + GC.SuppressFinalize(this); + } + } + + + /// + /// Exception class for CSVReader exceptions. + /// + public class CSVReaderException : ApplicationException { + + /// + /// Constructs a new exception object with the given message. + /// + /// The exception message. + public CSVReaderException(string message) : base(message) { } + } +} diff --git a/contrib/Makefile b/contrib/Makefile new file mode 100644 index 00000000..6e4d367a --- /dev/null +++ b/contrib/Makefile @@ -0,0 +1,4 @@ +all: ParseCcStmt.exe + +ParseCcStmt.exe: ParseCcStmt.cs CSVReader.cs + gmcs -out:ParseCcStmt.exe ParseCcStmt.cs CSVReader.cs diff --git a/contrib/ParseCcStmt.cs b/contrib/ParseCcStmt.cs new file mode 100644 index 00000000..52fc6f64 --- /dev/null +++ b/contrib/ParseCcStmt.cs @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +using CSVReader; + +/** + * @file ParseCcStmt.cs + * + * @brief Provides a .NET way to turn a CSV report into Ledger transactions. + * + * I use this code for converting the statements from my own credit card + * issuer. I realize it's strange for this to be in C#, but I wrote it + * during a phase of C# contracting. The code is solid enough now -- + * and the Mono project is portable enough -- that I haven't seen the + * need to rewrite it into another language like Python. + */ + +namespace JohnWiegley +{ + public class Posting + { + public DateTime Date; + public DateTime PostedDate; + public string Code; + public string Payee; + public Decimal Amount; + } + + public interface IStatementConverter + { + List ConvertRecords(Stream s); + } + + public class ConvertGoldMasterCardStatement : IStatementConverter + { + public List ConvertRecords(Stream s) + { + List posts = new List(); + + using (CSVReader.CSVReader csv = new CSVReader.CSVReader(s)) { + string[] fields; + while ((fields = csv.GetCSVLine()) != null) { + if (fields[0] == "POSTING DATE") + continue; + + Posting post = new Posting(); + + post.Date = DateTime.ParseEpost(fields[0], "mm/dd/yy", null); + post.PostedDate = DateTime.ParseEpost(fields[1], "mm/dd/yy", null); + post.Payee = fields[2].Trim(); + post.Code = fields[3].Trim(); + post.Amount = Convert.ToDecimal(fields[4].Trim()); + + if (post.Code.Length == 0) + post.Code = null; + + posts.Add(post); + } + } + return posts; + } + } + + public class ConvertMastercardStatement : IStatementConverter + { + public List ConvertRecords(Stream s) + { + List posts = new List(); + + using (CSVReader.CSVReader csv = new CSVReader.CSVReader(s)) { + string[] fields; + while ((fields = csv.GetCSVLine()) != null) { + Posting post = new Posting(); + + post.Date = DateTime.ParseEpost(fields[0], "m/dd/yyyy", null); + post.Payee = fields[2].Trim(); + post.Code = fields[3].Trim(); + post.Amount = - Convert.ToDecimal(fields[4].Trim()); + + if (post.Code.Length == 0) + post.Code = null; + + posts.Add(post); + } + } + return posts; + } + } + + public class PrintPostings + { + public string DefaultAccount(Posting post) { + if (Regex.IsMatch(post.Payee, "IGA")) + return "Expenses:Food"; + return "Expenses:Food"; + } + + public void Print(string AccountName, string PayAccountName, + List posts) + { + foreach (Posting post in posts) { + if (post.Amount < 0) { + Console.WriteLine("{0} * {1}{2}", post.Date.ToString("yyyy/mm/dd"), + post.Code != null ? "(" + post.Code + ") " : "", + post.Payee); + Console.WriteLine(" {0,-36}{1,12}", AccountName, + "$" + (- post.Amount).ToString()); + Console.WriteLine(" {0}", PayAccountName); + } else { + Console.WriteLine("{0} {1}{2}", post.Date.ToString("yyyy/mm/dd"), + post.Code != null ? "(" + post.Code + ") " : "", + post.Payee); + Console.WriteLine(" {0,-36}{1,12}", DefaultAccount(post), + "$" + post.Amount.ToString()); + Console.WriteLine(" * {0}", AccountName); + } + Console.WriteLine(); + } + } + } + + public class ParseCcStmt + { + public static int Main(string[] args) + { + StreamReader reader = new StreamReader(args[0]); + string firstLine = reader.ReadLine(); + + string CardAccount = args[1]; + string BankAccount = args[2]; + + IStatementConverter converter; + + if (firstLine.StartsWith("POSTING DATE")) { + converter = new ConvertGoldMasterCardStatement(); + } else { + converter = new ConvertMastercardStatement(); + } + + reader = new StreamReader(args[0]); + List posts = converter.ConvertRecords(reader.BaseStream); + + PrintPostings printer = new PrintPostings(); + printer.Print(CardAccount, BankAccount, posts); + + return 0; + } + } +} diff --git a/contrib/README b/contrib/README new file mode 100644 index 00000000..6108afbf --- /dev/null +++ b/contrib/README @@ -0,0 +1,4 @@ +This scripts are provided just to give some ideas. They probably need +to be modified to better suit your environment. Beware! + +John diff --git a/contrib/bal b/contrib/bal new file mode 100755 index 00000000..423e3e41 --- /dev/null +++ b/contrib/bal @@ -0,0 +1,21 @@ +#!/bin/sh + +switch="-c" +limit="-t (/Liabilities/?a<0:Ua>100)&a" + +if [ "$1" = "-C" -o "$1" = "-U" ]; then + switch="$1" + shift +elif [ "$1" = "-b" -o "$1" = "-e" -o "$1" = "-p" ]; then + switch="$1 $2" + shift 2 +fi + +accts="$@" +if [ -z "$accts" ]; then + accts="-Equity -Income -Expenses" +else + limit="" +fi + +ledger -VQ $switch $limit -s -S "-UT" balance $accts diff --git a/contrib/bal-huquq b/contrib/bal-huquq new file mode 100755 index 00000000..fad2854a --- /dev/null +++ b/contrib/bal-huquq @@ -0,0 +1,21 @@ +#!/bin/sh + +switch="-c" +limit="-t (/Liabilities/?(/Huquq/?a/P{2.22AU}<={-1.0}:a<0):Ua>100)&a" + +if [ "$1" = "-C" -o "$1" = "-U" ]; then + switch="$1" + shift +elif [ "$1" = "-b" -o "$1" = "-e" -o "$1" = "-p" ]; then + switch="$1 $2" + shift 2 +fi + +accts="$@" +if [ -z "$accts" ]; then + accts="-Equity -Income -Expenses" +else + limit="" +fi + +ledger -VQ $switch $limit -s -S "-UT" balance $accts diff --git a/contrib/compilation-ledger.el b/contrib/compilation-ledger.el new file mode 100644 index 00000000..0dedc894 --- /dev/null +++ b/contrib/compilation-ledger.el @@ -0,0 +1,97 @@ +;;; compilation-ledger.el --- error regexps for ledger + +;; Copyright 2009, 2010, 2011 Kevin Ryde + +;; Author: Kevin Ryde +;; Version: 1 +;; Keywords: processes +;; URL: http://user42.tuxfamily.org/compilation-ledger/index.html +;; EmacsWiki: CompilationMode + +;; compilation-ledger.el 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 3, or (at your +;; option) any later version. +;; +;; compilation-ledger.el 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 can get a copy of the GNU General Public License online at +;; . + + +;;; Commentary: + +;; This spot of code adds a `compilation-error-regexp-alist' pattern to +;; recognise error messages from the "ledger" program, +;; +;; http://newartisans.com/software/ledger.html +;; +;; such as +;; +;; Error: "foo.ledger", line 1656: Invalid date string: foo +;; +;; This is only for running ledger from M-x compile. ledger.el shows +;; reports with its own `ledger-report-mode' which has more features and +;; isn't based on `compilation-mode'. + +;;; Install: + +;; Put compilation-ledger.el in one of your `load-path' directories, +;; and in your .emacs add +;; +;; (eval-after-load "compile" '(require 'compilation-ledger)) +;; +;; There's an autoload cookie below for this, if you know how to use +;; `update-file-autoloads' and friends. + +;;; Emacsen: + +;; Designed for Emacs 20 up, works in XEmacs 21 too. + +;;; History: + +;; Version 1 - the first version + +;;; Code: + +;;;###autoload (eval-after-load "compile" '(require 'compilation-ledger)) + +(require 'compile) + +(let ((symbol 'compilation-ledger) + (pattern '("^Error: \"\\([^\"\n]+?\\)\", line \\([0-9]+\\):" 1 2))) + (cond ((eval-when-compile (boundp 'compilation-error-regexp-systems-list)) + ;; xemacs21 + (add-to-list 'compilation-error-regexp-alist-alist + (list symbol pattern)) + (compilation-build-compilation-error-regexp-alist)) + ((eval-when-compile (boundp 'compilation-error-regexp-alist-alist)) + ;; emacs22 up + (add-to-list 'compilation-error-regexp-alist symbol) + (add-to-list 'compilation-error-regexp-alist-alist + (cons symbol pattern))) + (t + ;; emacs21 + (add-to-list 'compilation-error-regexp-alist pattern)))) + +(defun compilation-ledger-unload-function () + "Remove compilation-ledger regexps on `unload-feature'." + (setq compilation-error-regexp-alist + (remove 'compilation-ledger compilation-error-regexp-alist)) + (setq compilation-error-regexp-alist-alist + (remove (assq 'compilation-ledger + compilation-error-regexp-alist-alist) + compilation-error-regexp-alist-alist)) + (when (eval-when-compile + (fboundp 'compilation-build-compilation-error-regexp-alist)) + (compilation-build-compilation-error-regexp-alist)) + nil) ;; and normal unload-feature actions + +;; LocalWords: http newartisans html el + +(provide 'compilation-ledger) + +;;; compilation-ledger.el ends here diff --git a/contrib/entry b/contrib/entry new file mode 100755 index 00000000..ef1869da --- /dev/null +++ b/contrib/entry @@ -0,0 +1,16 @@ +#!/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 xact "$@" > /tmp/xact; then + cat /tmp/xact >> $LEDGER +else + echo "$@" >> $LEDGER +fi +rm /tmp/xact + +vi +$line $LEDGER diff --git a/contrib/getquote-uk.py b/contrib/getquote-uk.py new file mode 100755 index 00000000..a69d4e7d --- /dev/null +++ b/contrib/getquote-uk.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import urllib, string, sys + +def download(sym): + url = "http://uk.old.finance.yahoo.com/d/quotes.csv?s=" + url += sym + "&f=sl1d1t1c1ohgv&e=.csv" + f = urllib.urlopen(url, proxies={}) + info = f.read() + f.close() + fields = string.split(info, ',') + result = float(fields[1])/100 + return result + + +sym = sys.argv[1] +sym = sym.replace('_', '.') +if sym == '£': + print '£1.00' +else: + try: print "£" +str(download(sym)) + except: pass diff --git a/contrib/getquote.pl b/contrib/getquote.pl new file mode 100755 index 00000000..8e3bb678 --- /dev/null +++ b/contrib/getquote.pl @@ -0,0 +1,19 @@ +#!/usr/bin/env perl + +$timeout = 60; + +use Finance::Quote; +use POSIX qw(strftime localtime time); + +$q = Finance::Quote->new; +$q->timeout($timeout); +$q->require_labels(qw/price/); + +%quotes = $q->fetch("nasdaq", $ARGV[0]); +if ($quotes{$ARGV[0], "price"}) { + print strftime('%Y/%m/%d %H:%M:%S', localtime(time())); + print " ", $ARGV[0], " "; + print "\$", $quotes{$ARGV[0], "price"}, "\n"; +} else { + exit 1; +} diff --git a/contrib/iso4127-commodities/iso4217ledger.sh b/contrib/iso4127-commodities/iso4217ledger.sh new file mode 100755 index 00000000..a7815f4a --- /dev/null +++ b/contrib/iso4127-commodities/iso4217ledger.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# iso4217ledger.sh - Convert ISO 4217 currencies to ledger commodities +# +# This script will download the latest XML for ISO 4217 Table A.1 +# and print the contained currency & funds code list as ledger +# commodity definitions to stdout. + +# Copyright (c) 2014 Alexis Hildebrandt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +xml_url="http://www.currency-iso.org/dam/downloads/lists/list_one.xml" +xsl_file="$(dirname $0)/iso4217ledger.xsl" + +xsltproc="$(which xsltproc)" +if [ ! -f "$xsltproc" -o ! -x "$xsltproc" ]; then + echo "Can't find xsltproc" + exit 1 +fi + +download_command="$(which curl)" +if [ -f "$download_command" \ + -a -x "$download_command" ]; then + download_options="--silent" +else + download_command="$(which wget)" + if [ -n "$download_command" \ + -a -f "$download_command" \ + -a -x "$download_command" ]; then + download_options="--quiet --output-document -" + else + echo "Can't find curl or wget." + exit 1 + fi +fi + +$download_command $download_options "$xml_url" | $xsltproc "$xsl_file" - diff --git a/contrib/iso4127-commodities/iso4217ledger.xsl b/contrib/iso4127-commodities/iso4217ledger.xsl new file mode 100644 index 00000000..cd70d2d6 --- /dev/null +++ b/contrib/iso4127-commodities/iso4217ledger.xsl @@ -0,0 +1,138 @@ + + + + + + + + + + + , + + + + + + + + + + + ; Ledger commodity declarations +; Generated from ISO 4217 Table A.1 XML ( + +) using iso4217ledger.xsl + + + + + + + + + + + + + + ¤ + + + + + commodity + + + + note + + - + + + ( + + ) + + + + format + + 0000 + + + + + + + + + + + + + + + + + + + nomarket + + + + + + + + + + 0 + + + + + + + diff --git a/contrib/ledger-completion.bash b/contrib/ledger-completion.bash new file mode 100644 index 00000000..2da44274 --- /dev/null +++ b/contrib/ledger-completion.bash @@ -0,0 +1,91 @@ +### Assumption +# +# bash-completion package is installed and enabled +# +### Just want to try it? +# +# $ source ledger-completion.bash +# +### How to install? +# +#### For local user +# +# $ cat <>~/.bash_completion +# . ~/.bash_completion.d/ledger +# EOF +# +# $ cp ledger-completion.bash ~/.bash_completion.d/ledger +# +#### For all users +# +# $ sudo cp ledger-completion.bash /etc/bash_completion.d/ledger +# + +_ledger() +{ + local cur prev command options + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # COMMANDS + # + # Commands are found in source code: + # report.cc::lookup "case symbol_t::COMMAND" + # report.cc::lookupcase "case symbol_t::PRECOMMAND" : these are debug commands and they have been filtered out here + # + commands="accounts balance budget cleared commodities convert csv draft echo emacs entry equity lisp org payees pricemap prices pricesdb print register reload select source stats tags xact xml" + + # OPTIONS + # + # Options are found in source code: + # global.cc::lookup_option + # report.cc::lookup_option + # session.cc::lookup_option + # + options="--abbrev-len= --account-width= --account= --actual --actual-dates --add-budget --amount-data --amount-width= --amount= --anon --ansi --args-only --auto-match --aux-date --average --balance-format= --base --basis --begin= --bold-if= --budget --budget-format= --by-payee --cache= --change --check-payees --cleared --cleared-format= --collapse --collapse-if-zero --color --columns= --cost --count --csv-format= --current --daily --date-format= --date-width= --date= --datetime-format= --day-break --days-of-week --dc --debug= --decimal-comma --depth= --detail --deviation --display-amount= --display-total= --display= --dow --download --effective --empty --end= --equity --exact --exchange= --explicit --file= --first= --flat --force-color --force-pager --forecast-while= --forecast-years= --forecast= --format= --full-help --gain --generated --group-by= --group-title-format= --head= --help --help-calc --help-comm --help-disp --historical --immediate --init-file= --inject= --input-date-format= --invert --last= --leeway= --limit= --lot-dates --lot-notes --lot-prices --lot-tags --lots --lots-actual --market --master-account= --meta-width= --meta= --monthly --no-aliases --no-color --no-pager --no-rounding --no-titles --no-total --now= --only= --options --output= --pager= --payee-width= --payee= --pedantic --pending --percent --period-sort= --period= --permissive --pivot= --plot-amount-format= --plot-total-format= --prepend-format= --prepend-width= --price --price-db= --price-exp= --pricedb-format= --prices-format= --primary-date --quantity --quarterly --raw --real --recursive-aliases --register-format= --related --related-all --revalued --revalued-only --revalued-total= --rich-data --script= --seed= --sort-all= --sort-xacts= --sort= --start-of-week= --strict --subtotal --tail= --time-colon --time-report --total-data --total-width= --total= --trace= --truncate= --unbudgeted --uncleared --unrealized --unrealized-gains= --unrealized-losses= --unround --value --value-expr= --values --verbose --verify --verify-memory --version --weekly --wide --yearly" + + # Bash FAQ E13 http://tiswww.case.edu/php/chet/bash/FAQ + # + COMP_WORDBREAKS=${COMP_WORDBREAKS//:} + + # ACCOUNTS + # + # Accounts are generated with bash command: + # $ ledger accounts>/tmp/accounts; for i in {1..5}; do cut -d : -f $i- /tmp/accounts;cut -d : -f -$i /tmp/accounts; done|sort -u|xargs + # + # Warning: this is working badly if there are spaces in account names + # + accounts="Assets Liabilities Equity Revenue Expenses" + + case $prev in + --@(cache|file|init-file|output|pager|price-db|script)|-@(f|i|o)) + _filedir + return 0 + ;; + @(balance|equity|print|register)) + COMPREPLY=( $(compgen -W "${accounts}" -- ${cur}) ) + return 0 + ;; + + esac + + if [[ ${cur} == -* ]] ; then + COMPREPLY=( $(compgen -W "${options}" -- ${cur}) ) +# elif [[ ${cur} == [A-Z]* ]] ; then +# COMPREPLY=( $(compgen -W "${accounts}" -- ${cur}) ) + else + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + fi + + return 0 +} +complete -F _ledger ledger + +# Local variables: +# mode: shell-script +# sh-basic-offset: 4 +# sh-indent-comment: t +# indent-tabs-mode: nil +# End: +# ex: ts=4 sw=4 et filetype=sh diff --git a/contrib/ledger-du b/contrib/ledger-du new file mode 100755 index 00000000..580e916e --- /dev/null +++ b/contrib/ledger-du @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import string +import sys +import os +import time + +from stat import * +from os.path import * + +def report_file(path): + dir_elems = string.split(dirname(path), os.sep) + if dir_elems[0] == "." or dir_elems[0] == "": + dir_elems = dir_elems[1 :] + account = string.join(dir_elems, ":") + + info = os.stat(path) + print time.strftime("%Y/%m/%d", time.localtime(info[ST_MTIME])), + + print basename(path) + print " ", account, " ", info[ST_SIZE], "b" + print " Equity:Files" + print + +def find_files(path): + xacts = os.listdir(path) + for xact in xacts: + xact = join(path, xact) + if not islink(xact): + if isdir(xact) and xact != "/proc": + find_files(xact) + else: + report_file(xact) + +args = sys.argv[1:] +if len(args): + paths = args +else: + paths = ["."] + +print """ +C 1.00 Kb = 1024 b +C 1.00 Mb = 1024 Kb +C 1.00 Gb = 1024 Mb +C 1.00 Tb = 1024 Gb +""" + +for path in paths: + find_files(path) diff --git a/contrib/non-profit-audit-reports/GPLv3 b/contrib/non-profit-audit-reports/GPLv3 new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/contrib/non-profit-audit-reports/GPLv3 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/contrib/non-profit-audit-reports/LICENSE b/contrib/non-profit-audit-reports/LICENSE new file mode 100644 index 00000000..c8c63924 --- /dev/null +++ b/contrib/non-profit-audit-reports/LICENSE @@ -0,0 +1,14 @@ +Contents under contrib/non-profit-audit-reports/ are licensed GPLv3-or-later. + +The GPLv3-or-later licensing of the contents of this directory does not, +to our knowledge and belief, impact the licensing of any other part of +Ledger. Parts of the files herein are likely derivative works of the rest +of Ledger, but these works are under this subdirectory are not, to our +knowledge, used, imported, included, copied, etc. into other parts of the +codebase. + +In short, this is a small application written to use Ledger like a +library, particularly via its Python API interface. It derives from +Ledger, but Ledger does not, to our knowledge, derive from it. + +We are not lawyers and this is not legal advice. diff --git a/contrib/non-profit-audit-reports/README b/contrib/non-profit-audit-reports/README new file mode 100644 index 00000000..b4897f21 --- /dev/null +++ b/contrib/non-profit-audit-reports/README @@ -0,0 +1,100 @@ +README + +This document provides backround on the enclosed example + +Demo +---- +To run the demo do +./demo.sh + +Which should generate the following files in tests/ + chart-of-accounts.txt + general-ledger.txt + general-ledger.csv + general-ledger.ods + +And a final, "portable" zip file with the spreadsheet in + general-ledger.zip + +It *should* be possible to copy general-ledger.zip to another system, +unzip it, open general-ledger.ods in Libre Office and have the relative +links resolve correctly. + +NOTE: Export to PDF should also work. + + +Known Dependencies +------------------ +ledger (3.0) +python (2.x) +zip +libdate-manip-perl +libmath-gmp-perl + + +Temporary Hacks +--------------- +Due to an urgent project deadline the ooolib2 directory +represents some fixes to: + http://ooolib.sourceforge.net/ + +The proper version of this library can be installed on Debian systems with +# apt-get install python-ooolib + +Compare the deltas to the current version with +# diff -u /usr/share/pyshared/ooolib/__init__.py ooolib2/__init__.py + +Note also that the csv2ods.py treats columns 4 and 5 (numbering from 1) of the csv +magically. If column 4 contains a non-empty string which is not 'Receipt' +then it is interpreted as a relative path of an artifact to link to. +Similary for column 5 and 'Invoice'. + + +Sample PDF files +---------------- +The sample PDF files were created as follows: + +paps --font="Courier 12" --paper letter --top-margin=18 tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt | ps2pdf - tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf + +paps --font="Courier 12" --paper letter --top-margin=18 tests/Financial/Invoices/Invoice20110510.txt | ps2pdf - tests/Financial/Invoices/Invoice20110510.pdf + + +Resources +--------- +ooolib + http://ooolib.sourceforge.net/ + +LIBPF + probably does not replace ooolib + http://wp.libpf.com/?p=82 + +Libre Office Calc Guide (contains function reference) + https://www.libreoffice.org/get-help/documentation/ + +Libre Office API + http://api.libreoffice.org/examples/examples.html + http://api.libreoffice.org/examples/DevelopersGuide/examples.html + +OpenOffice Developers Guide + http://wiki.openoffice.org/wiki/Documentation/DevGuide/OpenOffice.org_Developers_Guide + +Spreadsheet Documents + http://wiki.openoffice.org/wiki/Documentation/DevGuide/Spreadsheets/Spreadsheet_Documents + +How to correctly create ODF documents using zip +(Do NOT do this, use ooolib instead) + http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/ + +Line Breaks + fo:break-before="page" + http://books.evc-cit.info/oobook/ch03.html#page-content-section + +ODF Validator + http://opendocumentfellowship.com/validator + +Editing Hyperlinks + http://help.libreoffice.org/Common/Editing_Hyperlinks + +Perl OODoc +NOTE: a replacement for POD, not ooolib + http://search.cpan.org/dist/OpenOffice-OODoc/ diff --git a/contrib/non-profit-audit-reports/bank-reconcilation.plx b/contrib/non-profit-audit-reports/bank-reconcilation.plx new file mode 100755 index 00000000..7a8da911 --- /dev/null +++ b/contrib/non-profit-audit-reports/bank-reconcilation.plx @@ -0,0 +1,232 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; +use Data::PowerSet; + +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); +my $ONE_HUNDRED = Math::BigFloat->new("100.00"); + +my $VERBOSE = 1; +my $DEBUG = 0; + +my $LEDGER_BIN = "/usr/local/bin/ledger"; + +###################################################################### +sub BruteForceSubSetSumSolver ($$$) { + my($numberList, $totalSought, $extractNumber) = @_; + + my($P, $N) = (0, 0); + my $size = scalar(@{$numberList}); + my %Q; + my(@L) = + map { { val => &$extractNumber($_), obj => $_ } } @{$numberList}; + + my $powerset = Data::PowerSet->new(@L); + + while (my $set = $powerset->next) { + my $total = $ZERO; + foreach my $ee (@{$set}) { + $total += $ee->{val}; + } + if ($totalSought == $total) { + my(@list) = map { $_->{obj} } @{$set}; + return (1, \@list); + } + } + return (0, []); +} +###################################################################### +sub DynamicProgrammingSubSetSumSolver ($$$) { + my($numberList, $totalSought, $extractNumber) = @_; + + my($P, $N) = (0, 0); + my $size = scalar(@{$numberList}); + my %Q; + my(@L) = + map { { val => &$extractNumber($_), obj => $_ } } @{$numberList}; + + print STDERR " TotalSought:", $totalSought if $VERBOSE; + print STDERR " L in this iteration:\n [" if $VERBOSE; + + foreach my $ee (@L) { + if ($ee->{val} < 0) { + $N += $ee->{val} + } else { + $P += $ee->{val}; + } + print STDERR $ee->{val}, ", " if $VERBOSE; + } + print STDERR "]\n P = $P, N = $N\n" if ($VERBOSE); + + for (my $ii = 0 ; $ii <= $size ; $ii++ ) { + $Q{$ii}{0}{value} = 1; + $Q{$ii}{0}{list} = []; + } + for (my $jj = $N; $jj <= $P ; $jj++) { + $Q{0}{$jj}{value} = ($L[0]{val} == $jj); + $Q{0}{$jj}{list} = $Q{0}{$jj}{value} ? [ $L[0]{obj} ] : []; + } + for (my $ii = 1; $ii <= $size ; $ii++ ) { + for (my $jj = $N; $jj <= $P ; $jj++) { + if ($Q{$ii-1}{$jj}{value}) { + $Q{$ii}{$jj}{value} = 1; + + $Q{$ii}{$jj}{list} = [] unless defined $Q{$ii}{$jj}{list}; + push(@{$Q{$ii}{$jj}{list}}, @{$Q{$ii-1}{$jj}{list}}); + + } elsif ($L[$ii]{val} == $jj) { + $Q{$ii}{$jj}{value} = 1; + + $Q{$ii}{$jj}{list} = [] unless defined $Q{$ii}{$jj}{list}; + push(@{$Q{$ii}{$jj}{list}}, $jj); + } elsif ($Q{$ii-1}{$jj - $L[$ii]{val}}{value}) { + $Q{$ii}{$jj}{value} = 1; + $Q{$ii}{$jj}{list} = [] unless defined $Q{$ii}{$jj}{list}; + push(@{$Q{$ii}{$jj}{list}}, $L[$ii]{obj}, @{$Q{$ii-1}{$jj - $L[$ii]{val}}{list}}); + } else { + $Q{$ii}{$jj}{value} = 0; + $Q{$ii}{$jj}{list} = []; + } + } + } + foreach (my $ii = 0; $ii <= $size; $ii++) { + foreach (my $jj = $N; $jj <= $P; $jj++) { + print "Q($ii, $jj) == $Q{$ii}{$jj}{value} with List of ", join(", ", @{$Q{$ii}{$jj}{list}}), "\n"; + } + } + return [ $Q{$size}{$totalSought}{value}, \@{$Q{$size}{$totalSought}{list}}]; +} +###################################################################### +sub Commify ($) { + my $text = reverse $_[0]; + $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; + return scalar reverse $text; +} +###################################################################### +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} +###################################################################### +sub ConvertTwoDigitPrecisionToInteger ($) { + return sprintf("%d", $_[0] * $ONE_HUNDRED); +} +###################################################################### +sub ConvertTwoDigitPrecisionToIntegerInEntry ($) { + return ConvertTwoDigitPrecisionToInteger($_[0]->{amount}); +} +###################################################################### +my $firstArg = shift @ARGV; + +my $solver = \&BruteForceSubSetSumSolver; + +if (@ARGV < 7) { + print STDERR "usage: $0 [-d] <ACCOUNT_REGEX> <END_DATE> <START_SEARCH_FROM_DATE> <END_SEARCH_TO_DATE> <BANK_STATEMENT_BALANCE> <LEDGER_OPTIONS>\n"; + exit 1; +} +if ($firstArg eq '-d') { + $solver = \&DynamicProgrammingSubSetSumSolver; +} else { + unshift(@ARGV, $firstArg); +} +my($title, $account, $endDate, $startSearchFromDate, $endSearchToDate, $bankBalance, @mainLedgerOptions) = @ARGV; + +$bankBalance = ParseNumber($bankBalance); + +my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-e', $endDate, '-F', '%t\n', 'bal', "/$account/"); + +open(FILE, "-|", @fullCommand) or die "unable to run command ledger command: @fullCommand: $!"; + +my $total; +foreach my $line (<FILE>) { + chomp $line; + die "Unable to parse output line from: \"$line\"" + unless $line =~ /^\s*\$\s*([\-\d\.\,]+)\s*$/ and not defined $total; + $total = $1; + $total = ParseNumber($total); +} +close FILE; +if (not defined $total or $? != 0) { + die "unable to run ledger @fullCommand: $!"; +} +my $differenceSought = $total - $bankBalance; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%Y-%m-%d"); +die "Date calculation error on $endDate" if ($err); + +my $earliestStartDate = DateCalc(ParseDate($endDate), ParseDateDelta("- 1 month"), \$err); + +die "Date calculation error on $endDate" if ($err); + +my $startDate = ParseDate($startSearchFromDate); + +my @solution; +while ($startDate ge $earliestStartDate) { + $startDate = DateCalc(ParseDate($startDate), ParseDateDelta("- 1 day"), \$err); + die "Date calculation error on $endDate" if ($err); + + my $formattedStartDate = UnixDate($startDate, "%Y-%m-%d"); + + print STDERR "Testing $formattedStartDate through $endSearchToDate for a total of ", Commify($differenceSought), ": \n" + if $VERBOSE; + + my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $formattedStartDate, '-e', $endSearchToDate, + '-F', '"%(date)","%C","%P","%t"\n', + 'reg', "/$account/"); + + open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + + my @entries; + + foreach my $line (<FILE>) { + die "Unable to parse output line from: $line" + unless $line =~ /^\s*"([^"]*)","([^"]*)","([^"]*)","([^"]*)"\s*$/; + my($date, $checkNum, $payee, $amount) = ($1, $2, $3, $4); + die "$amount is not a valid amount" + unless $amount =~ s/\s*\$\s*([\-\d\.\,]+)\s*$/$1/; + $amount = ParseNumber($amount); + + push(@entries, { date => $date, checkNum => $checkNum, + payee => $payee, amount => $amount }); + } + close FILE; + die "unable to properly run ledger command: @fullCommand: $!" unless ($? == 0); + + @solution = $solver->(\@entries, + ConvertTwoDigitPrecisionToInteger($differenceSought), + \&ConvertTwoDigitPrecisionToIntegerInEntry); + if ($VERBOSE) { + if ($solution[0]) { + use Data::Dumper; + print STDERR "Solution for $formattedStartDate to $formattedEndDate, $differenceSought: \n", + Data::Dumper->Dump(\@solution); + } else { + print STDERR "No Solution Found. :(\n"; + } + } + last if ($solution[0]); +} +if ($solution[0]) { + print "\"title:$formattedEndDate: $title\"\n\"BANK RECONCILATION: $account\",\"ENDING\",\"$formattedEndDate\"\n"; + print "\n\n\"DATE\",\"CHECK NUM\",\"PAYEE\",\"AMOUNT\"\n\n"; + print "\"$formattedEndDate\",\"\",\"BANK ACCOUNT BALANCE\",\"\$$bankBalance\"\n\n"; + foreach my $ee (sort { $a->{date} cmp $b->{date} } @{$solution[1]}) { + print "\"$ee->{date}\",\"$ee->{checkNum}\",\"$ee->{payee}\",\"\$$ee->{amount}\"\n"; + } + print "\n\"$formattedEndDate\",\"\",\"OUR ACCOUNT BALANCE\",\"\$$total\"\n\n"; +} +############################################################################### +# +# Local variables: +# compile-command: "perl -c bank-reconcilation.plx" +# End: diff --git a/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx b/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx new file mode 100755 index 00000000..f2055a62 --- /dev/null +++ b/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx @@ -0,0 +1,203 @@ +#!/usr/bin/perl +# cash-receipts-and-disbursments-journals -*- Perl -*- +# +# Script to generate a cash receipts and disbursement joural reports +# using Ledger. +# +# Accountants sometimes ask for a report called the "cash receipts and +# disbursements journals". From a programmer's perspective, these are two +# reports that have the following properties: +# +# * Receipts: "a list of all transactions in the period where funds +# enter a cash account (i.e., the amount reconciled +# against the cash account is > 0" +# +# * Disbursements: "a list of all transactions in the period where +# funds leave a cash account (i.e., the amount +# reconciled against the cash account is < 0) +# +# Copyright (C) 2011, 2012, 2013 Bradley M. Kuhn +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; +use File::Temp qw/tempfile/; + +my $LEDGER_CMD = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 75; + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} + +sub LedgerAcctToFilename($) { + my $x = $_[0]; + $x =~ s/ /-/g; + $x =~ s/:/-/g; + return $x; +} + +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <BEGIN_DATE> <END_DATE> <OTHER_LEDGER_OPTS>\n"; + exit 1; +} + +my($beginDate, $endDate, @otherLedgerOpts) = @ARGV; + +my(@chartOfAccountsOpts) = ('-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'accounts'); + +open(CHART_DATA, "-|", $LEDGER_CMD, @chartOfAccountsOpts) + or die "Unable to run $LEDGER_CMD @chartOfAccountsOpts: $!"; + +my @accounts; +while (my $line = <CHART_DATA>) { + chomp $line; + next if $line =~ /^\s*\<\s*Adjustment\s*\>\s*$/; + next if $line =~ /^Equity:/; # Stupid auto-account made by ledger. + $line =~ s/^\s*//; $line =~ s/\s*$//; + push(@accounts, $line); + +} +close(CHART_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; + +my $formattedEndDate = new Date::Manip::Date; +die "badly formatted end date, $endDate" if $formattedEndDate->parse($endDate); +my $oneDayLess = new Date::Manip::Delta; +die "bad one day less" if $oneDayLess->parse("- 1 day"); +$formattedEndDate = $formattedEndDate->calc($oneDayLess); +$formattedEndDate = $formattedEndDate->printf("%Y/%m/%d"); + +foreach my $typeData ({ name => 'disbursements', query => 'a<=0' }, + { name => 'receipts', query => 'a>0' }) { + my $fileNameBase = $typeData->{name}; + + open(CSV_OUT, ">", "$fileNameBase.csv") or die "unable to open $fileNameBase.csv: $!"; + + foreach my $acct (sort { $a cmp $b } @accounts) { + next unless ($acct =~ /^(?:Assets|Liabilities)/); + + my @entryLedgerOpts = ('-l', $typeData->{query}, + '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'print', $acct); + + open(ENTRY_DATA, "-|", $LEDGER_CMD, @entryLedgerOpts) + or die "Unable to run $LEDGER_CMD @entryLedgerOpts: $!"; + + my($tempFH, $tempFile) = tempfile("cashreportsXXXXXXXX", TMPDIR => 1); + + while (my $line = <ENTRY_DATA>) { print $tempFH $line; } + close(ENTRY_DATA); die "Error reading ledger output for entries: $!" unless $? == 0; + $tempFH->close() or die "Error writing ledger output for entries to temp file, $tempFile: $!"; + + goto SKIP_REGISTER_COMMANDS if (-z $tempFile); + + print CSV_OUT "\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$beginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n"; + print CSV_OUT '"DATE","CHECK NUM","NAME","ACCOUNT","AMOUNT"'; + + my $formatString = '\n"%(date)","%C","%P","%A","%t"'; + my $tagStrings = ""; + foreach my $tagField (qw/Receipt Invoice Statement Contract PurchaseOrder Approval Check IncomeDistributionAnalysis CurrencyRate/) { + print CSV_OUT ',"', $tagField, '"'; + $tagStrings .= ',"link:%(tag(\'' . $tagField . '\'))"'; + } + $formatString .= $tagStrings . '\n%/"","","","%A","%t"' . $tagStrings . '\n'; + + # I thought '--sort', 'd', '--sort-xact', 'a', should + # have worked below for a good sort. Then I tried + # rather than '--sort', "d,n,a", which didn't work either. + # I opened a bug: https://github.com/ledger/ledger/issues/901 + + my @csvRegLedgerOpts = ('-f', $tempFile, '-V', '-F', $formatString, '-w', '--sort', 'd', + '-b', $beginDate, '-e', $endDate, 'reg'); + + open(CSV_DATA, "-|", $LEDGER_CMD, @csvRegLedgerOpts) + or die "unable to run ledger command for $fileNameBase.csv: $!"; + + my($curDepositDate, $curDepositTotal); + + while (my $line = <CSV_DATA>) { + $line =~ s/"link:"/""/g; + + # Skip lines that have Adjustment or Equity: in them. + next if $line =~ + /^\s*"[^"]*","[^"]*","[^"]*","(\s*\<\s*Adjustment\s*\>\s*|Equity:)/; + + # Note that we don't do our usual "$TWO_CENTS" check on Adjustment + # here. That's by design: if we consistently ignore Adjustements in + # the same way, it might have the appearance that a Superman + # III/Office Space -style movement of funds is going on. By just + # straight "ignoring" them here, and not doing the TWO_CENTS test, it + # helps to assure that. + + # However, it's worth noting that the ignoring of "Adjustment" in these + # scripts is not that meaningful and doesn't indicate as Superman + # III/Office Space -style scheme, because such a scheme would also have + # to be implemented in the main Ledger codebase. + + + my $date = $line; chomp $date; + $date =~ s/^\s*"([^"]*)"\s*,.*$/$1/; + if (defined $date and $date !~ /^\s*$/ and + defined $curDepositDate and ($date ne $curDepositDate or + ($date eq $curDepositDate and $line !~ /DEPOSIT[\s\-]+BRANCH/))) { + print CSV_OUT "\"$curDepositDate\",\"SUBTOTAL\",\"BRANCH DEPOSIT TOTAL:\",\"\",\"\$$curDepositTotal\"\n\n"; + $curDepositTotal = $curDepositDate = undef; + } + if ($line =~ /DEPOSIT[\s\-]+BRANCH/) { + if (not defined $curDepositDate) { + $curDepositDate = $line; chomp $curDepositDate; + $curDepositDate =~ s/^\s*"([^"]+)"\s*,.*$/$1/; + } + } + # This is a bit of a hack because I can't ssume that the line with the + # description on it has the account name in it. + if (defined $curDepositDate and $line =~ /$acct/) { + my $amt = $line; + chomp $amt; + $amt =~ s/^\s*"[^"]*","[^"]*","[^"]*","[^"]*","\$\s*([^"]*)".*$/$1/; + $amt =~ s/,//g; + + $curDepositTotal = 0.0 unless defined $curDepositTotal; + $curDepositTotal += $amt; + } + print CSV_OUT $line; + } + # Catch potential last Deposit subtotal + print CSV_OUT "\n\"$curDepositDate\",\"SUBTOTAL\",\"BRANCH DEPOSIT TOTAL:\",\"\",\"\$$curDepositTotal\"\n\n" + if (defined $curDepositDate); + + close(CSV_DATA); die "Error read from csv ledger command $!" unless $? == 0; + print CSV_OUT "pagebreak\n"; + SKIP_REGISTER_COMMANDS: + unlink($tempFile); + } + close(CSV_OUT); die "Error read write csv out to $fileNameBase.csv: $!" unless $? == 0; +} +############################################################################### +# +# Local variables: +# compile-command: "perl -c cash-receipts-and-disbursments-journals.plx" +# End: + diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py new file mode 100755 index 00000000..6aabcb59 --- /dev/null +++ b/contrib/non-profit-audit-reports/csv2ods.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# csv2ods.py +# Convert example csv file to ods +# +# Copyright (c) 2012 Tom Marble +# Copyright (c) 2012, 2013 Bradley M. Kuhn +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +import sys, os, os.path, optparse +import csv +import ooolib2 +import shutil +import string +from Crypto.Hash import SHA256 + +def err(msg): + print 'error: %s' % msg + sys.exit(1) + +def ReadChecksums(inputFile): + checksums = {} + with open(inputFile, "r") as inputFH: + entries = inputFH.readlines() + for ee in entries: + fileName, checksum = ee.split(":") + fileName = fileName.replace(' ', "") + checksum = checksum.replace(' ', "") + checksum = checksum.replace("\n", "") + checksums[checksum] = fileName + return checksums + +def ChecksumFile(filename): + sha256 = SHA256.new() + chunk_size = 8192 + with open(filename, 'rb') as myFile: + while True: + chunk = myFile.read(chunk_size) + if len(chunk) == 0: + break + sha256.update(chunk) + return sha256.hexdigest() + +def main(): + program = os.path.basename(sys.argv[0]) + + print get_file_checksum(sys.argv[1]) + +def csv2ods(csvname, odsname, encoding='', singleFileDirectory=None, knownChecksums={}, verbose = False): + filesSavedinManifest = {} + + if knownChecksums: + checksumCache = {} + + if verbose: + print 'converting from %s to %s' % (csvname, odsname) + + if singleFileDirectory: + if not os.path.isdir(os.path.join(os.getcwd(),singleFileDirectory)): + os.mkdir(singleFileDirectory) + + doc = ooolib2.Calc() + # add a pagebreak style + style = 'pagebreak' + style_pagebreak = doc.styles.get_next_style('row') + style_data = tuple([style, ('style:row-height', doc.styles.property_row_height)]) + doc.styles.style_config[style_data] = style_pagebreak + # add a currency style + style = 'currency' + style_currency = doc.styles.get_next_style('cell') + style_data = tuple([style]) + doc.styles.style_config[style_data] = style_currency + + row = 1 + csvdir = os.path.dirname(csvname) + if len(csvdir) == 0: + csvdir = '.' + csvfile = open(csvname, 'rb') + reader = csv.reader(csvfile, delimiter=',', quotechar='"') + for fields in reader: + if len(fields) > 0: + for col in range(len(fields)): + val = fields[col] + if encoding != '' and val[0:5] != "link:": # Only utf8 encode if it's not a filename + val = unicode(val, 'utf8') + if len(val) > 0 and val[0] == '$': + doc.set_cell_value(col + 1, row, 'currency', val[1:]) + else: + if (len(val) > 0 and val[0:5] == "link:"): + val = val[5:] + linkname = os.path.basename(val) # name is just the last component + newFile = None + + if not singleFileDirectory: + newFile = val + + if knownChecksums: + if not checksumCache.has_key(val): + checksum = ChecksumFile(val) + checksumCache[val] = checksum + else: + checksum = checksumCache[val] + + if knownChecksums.has_key(checksum): + newFile = knownChecksums[checksum] + print "FOUND new file in known: " + newFile + + if not newFile: + relativeFileWithPath = os.path.basename(val) + + fileName, fileExtension = os.path.splitext(relativeFileWithPath) + newFile = fileName[:15] # 15 is an arbitrary choice. + newFile = newFile + fileExtension + # We'll now test to see if we made this file + # before, and if it matched the same file we + # now want. If it doesn't, try to make a + # short file name for it. + if filesSavedinManifest.has_key(newFile) and filesSavedinManifest[newFile] != val: + testFile = None + for cc in list(string.letters) + list(string.digits): + testFile = cc + newFile + if not filesSavedinManifest.has_key(testFile): + break + testFile = None + if not testFile: + raise Exception("too many similar file names for linkage; giving up") + else: + newFile = testFile + if not os.path.exists(csvdir + '/' + val): + raise Exception("File" + csvdir + '/' + val + " does not exist in single file directory mode; giving up") + src = os.path.join(csvdir, val) + dest = os.path.join(csvdir, singleFileDirectory, newFile) + shutil.copyfile(src, dest) + shutil.copystat(src, dest) + shutil.copymode(src, dest) + + newFile = os.path.join(singleFileDirectory, newFile) + + if knownChecksums: + checksumCache[checksum] = newFile + knownChecksums[checksum] = newFile + + linkrel = '../' + newFile # ../ means remove the name of the *.ods + doc.set_cell_value(col + 1, row, 'link', (linkrel, linkname)) + linkpath = csvdir + '/' + val + + if not val in filesSavedinManifest: + filesSavedinManifest[newFile] = val + + if not os.path.exists(linkpath): + print "WARNING: link %s DOES NOT EXIST at %s" % (val, linkpath) + if verbose: + if os.path.exists(linkpath): + print 'relative link %s EXISTS at %s' % (val, linkpath) + else: + if val == "pagebreak": + doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak) + else: + if val[0:6] == "title:": + doc.sheets[doc.sheet_index].set_name(val[6:]) + else: + doc.set_cell_value(col + 1, row, 'string', val) + else: + # enter an empty string for blank lines + doc.set_cell_value(1, row, 'string', '') + row += 1 + # save manifest file + if filesSavedinManifest.keys() != []: + manifestFH = open("MANIFEST", "a") + manifestFH.write("# Files from %s\n" % odsname) + for file in filesSavedinManifest.keys(): + manifestFH.write("%s\n" % file) + + manifestFH.close() + # Save spreadsheet file. + doc.save(odsname) + +def main(): + program = os.path.basename(sys.argv[0]) + version = '0.1' + parser = optparse.OptionParser(usage='%prog [--help] [--verbose]', + version='%prog ' + version) + parser.add_option('-v', '--verbose', action='store_true', + dest='verbose', + help='provide extra information while processing') + parser.add_option('-c', '--csv', action='store', + help='csv file to process') + parser.add_option('-o', '--ods', action='store', + help='ods output filename') + parser.add_option('-e', '--encoding', action='store', + help='unicode character encoding type') + parser.add_option('-d', '--single-file-directory', action='store', + help='directory name to move all files into') + parser.add_option('-s', '--known-checksum-list', action='store', + help='directory name to move all files into') + (options, args) = parser.parse_args() + + if len(args) != 0: + parser.error("not expecting extra args") + if not os.path.exists(options.csv): + err('csv does not exist: %s' % options.csv) + if not options.ods: + (root, ext) = os.path.splitext(options.csv) + options.ods = root + '.ods' + if options.verbose: + print '%s: verbose mode on' % program + print 'csv:', options.csv + print 'ods:', options.ods + print 'ods:', options.encoding + if options.known_checksum_list and not options.single_file_directory: + err(program + ": --known-checksum-list option is completely useless without --single-file-directory") + knownChecksums = {} + if options.known_checksum_list: + if not os.access(options.known_checksum_list, os.R_OK): + err(program + ": unable to read file: " + options.known_checksum_list) + knownChecksums = ReadChecksums(options.known_checksum_list) + csv2ods(options.csv, options.ods, options.encoding, options.single_file_directory, knownChecksums, options.verbose) + +if __name__ == '__main__': + main() diff --git a/contrib/non-profit-audit-reports/demo.sh b/contrib/non-profit-audit-reports/demo.sh new file mode 100755 index 00000000..a4b837a6 --- /dev/null +++ b/contrib/non-profit-audit-reports/demo.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# demo.sh +# Demonstrate a non-profit GL export and conversion to ODS + +program=$(basename $0) +dir=$(dirname $0) +cd $dir +dir=$(pwd -P) +export PYTHONPATH=$dir/ooolib2 + +getcsv=$dir/general-ledger-report.plx +csv2ods=$dir/csv2ods.py + +echo "Demonstrating ledger to ODS export in $dir/tests" +cd $dir/tests +sampledata=non-profit-test-data.ledger +echo " based on the sample data in $sampledata" + +$getcsv 2011/03/01 2012/03/01 -f $sampledata +if [ -e general-ledger.csv ]; then + echo "data was exported to: general-ledger.csv" +else + echo "error creating csv file" + exit 1 +fi + +$csv2ods --verbose --csv general-ledger.csv +if [ -e general-ledger.ods ]; then + echo "csv was converted to: general-ledger.ods" +else + echo "error creating ods file" + exit 1 +fi + +echo general-ledger.ods >> MANIFEST + +# create a portable zip file with the spreadsheet +# and the linked artifacts + +echo creating portable zipfile... +cat MANIFEST | zip -@ ../general-ledger.zip + +echo " " +echo "created general-ledger.zip" + diff --git a/contrib/non-profit-audit-reports/fund-report.plx b/contrib/non-profit-audit-reports/fund-report.plx new file mode 100755 index 00000000..ce59da96 --- /dev/null +++ b/contrib/non-profit-audit-reports/fund-report.plx @@ -0,0 +1,235 @@ +#!/usr/bin/perl +# fund-report.plx -*- Perl -*- +# +# Script to generate a Restricted Fund Report. Usefulness of this +# script may be confined to those who track separate funds in their +# accounts by having accounts that match this format: +# /^(Income|Expenses|Unearned Income|(Accrued:[^:]+:):PROJECTNAME/ + +# Conservancy does this because we carefully track fund balances for our +# fiscal sponsored projects. Those who aren't fiscal sponsors won't find +# this report all that useful, I suspect. Note that the name +# "Conservancy" is special-cased in a few places, mainly because our +# "General" fund is called "Conservancy". + +# +# Copyright (C) 2011, 2012, 2013 Bradley M. Kuhn +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; + +my $LEDGER_CMD = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 70; + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); +my $TWO_CENTS = Math::BigFloat->new("0.02"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <START_DATE> <END_DATE> <LEDGER_OPTIONS>\n"; + exit 1; +} +my($startDate, $endDate, @mainLedgerOptions) = @ARGV; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%Y/%m/%d"); +die "Date calculation error on $endDate" if ($err); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%Y/%m/%d"); +die "Date calculation error on $startDate" if ($err); + +# First, get balances for starting and ending for each fund + +my %funds; + +foreach my $type ('starting', 'ending') { + my(@ledgerOptions) = (@mainLedgerOptions, + '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-s'); + + if ($type eq 'starting') { + push(@ledgerOptions, '-e', $startDate); + } else { + push(@ledgerOptions,'-e', $endDate); + } + push(@ledgerOptions, 'reg', '/^(Income|Expenses):([^:]+):/'); + + open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions) + or die "Unable to run $LEDGER_CMD @ledgerOptions: $!"; + + while (my $fundLine = <LEDGER_FUNDS>) { + die "Unable to parse output line from first funds command: \"$fundLine\"" + unless $fundLine =~ /^\s*([^\$]+)\s+\$\s*\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\<Adjustment\>/ and (abs($amount) <= $TWO_CENTS); + die "Weird account found, $account with amount of $amount in command: @ledgerOptions\n" + unless $account =~ s/^\s*(?:Income|Expenses):([^:]+)://; + $account = $1; + $account = 'General' if $account eq 'Conservancy'; # FIXME: this is a special case for Consrevancy + $funds{$account}{$type} += $amount; + } + close LEDGER_FUNDS; + die "Failure on ledger command @ledgerOptions: $!" unless ($? == 0); +} +foreach my $fund (keys %funds) { + foreach my $type (keys %{$funds{$fund}}) { + $funds{$fund}{$type} = $ZERO - $funds{$fund}{$type}; + } +} +my(@ledgerOptions) = (@mainLedgerOptions, + '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-w', '-s', + '-b', $startDate, '-e', $endDate, 'reg'); + +my @possibleTypes = ('Income', 'Expenses', 'Unearned Income', 'Retained Earnings', 'Retained Costs', + 'Accrued:Loans Receivable', 'Accrued:Accounts Payable', + 'Accrued:Accounts Receivable', 'Accrued:Expenses'); + +foreach my $type (@possibleTypes) { + foreach my $fund (keys %funds) { + my $query; + $query = ($fund eq 'General') ? "/^${type}:Conservancy/": "/^${type}:$fund/"; + open(LEDGER_INCOME, "-|", $LEDGER_CMD, @ledgerOptions, $query) + or die "Unable to run $LEDGER_CMD for funds: $!"; + $funds{$fund}{$type} = $ZERO; + while (my $line = <LEDGER_INCOME>) { + die "Unable to parse output line from $type line command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $funds{$fund}{$type} += $amount; + } + close LEDGER_INCOME; + die "Failure on ledger command for ${type}:$fund: $!" unless ($? == 0); + } +} + +my %tot; +($tot{Start}, $tot{End}) = ($ZERO, $ZERO); + +my %beforeEndings = ('Income' => 1, 'Expenses' => 1); +my %afterEndings; + +# For other @possibleTypes, build up @fieldsList to just thoes that are present. + +foreach my $fund (keys %funds) { + foreach my $type (@possibleTypes) { + if ($funds{$fund}{$type} != $ZERO) { + if ($type =~ /^(Unearned Income|Accrued)/) { + $afterEndings{$type} = 1; + } else { + $beforeEndings{$type} = 1; + } + } + } +} +my(@beforeEndingFields, @afterEndingFields); + +foreach my $ii (@possibleTypes) { + push(@beforeEndingFields, $ii) if defined $beforeEndings{$ii}; + push(@afterEndingFields, $ii) if defined $afterEndings{$ii}; +} +# Make sure fieldLists present items are zero for those that should be zero. +foreach my $fund (keys %funds) { + foreach my $type ('starting', @beforeEndingFields, 'ending', @afterEndingFields) { + $funds{$fund}{$type} = $ZERO unless defined $funds{$fund}{$type}; + } +} + +print '"RESTRICTED AND GENERAL FUND REPORT",', "\"BEGINNING:\",\"$formattedStartDate\",\"ENDING:\",\"$formattedEndDate\"\n\n"; +print '"FUND","STARTING BALANCE",'; +my @finalPrints; +foreach my $type (@beforeEndingFields) { + $tot{$type} = $ZERO; + my $formattedType = $type; + print "\"$formattedType\","; +} +print '"ENDING BALANCE",""'; +foreach my $type (@afterEndingFields) { + $tot{$type} = $ZERO; + my $formattedType = $type; + $formattedType = "Prepaid Expenses" if $formattedType eq 'Accrued:Expenses'; + $formattedType =~ s/^Accrued://; + print ",\"$formattedType\""; +} +print "\n\n"; + +sub printTotal ($$) { + my($label, $tot) = @_; + print "\"$label\",\"\$$tot->{Start}\","; + foreach my $type (@beforeEndingFields) { + print "\"\$$tot->{$type}\","; + } + print "\"\$$tot->{End}\",\"\""; + foreach my $type (@afterEndingFields) { + print ",\"\$$tot->{$type}\""; + } + print "\n"; +} + +foreach my $fund (sort { + if ($a eq "General") { return 1 } + elsif ($b eq "General") { return -1 } + else { return $a cmp $b } } + keys %funds) { + my $sanityTotal = $funds{$fund}{starting}; + + if ($fund eq 'General') { + print "\n"; + printTotal("Restricted Subtotal", \%tot); + print "\n"; + } + $tot{Start} += $funds{$fund}{starting}; + $tot{End} += $funds{$fund}{ending}; + + print "\"$fund\",\"\$$funds{$fund}{starting}\","; + foreach my $type (@beforeEndingFields) { + print "\"\$$funds{$fund}{$type}\","; + $tot{$type} += $funds{$fund}{$type}; + } + print "\"\$$funds{$fund}{ending}\",\"\""; + foreach my $type (@afterEndingFields) { + print ",\"\$$funds{$fund}{$type}\""; + $tot{$type} += $funds{$fund}{$type}; + } + print "\n"; + # Santity check: + if (abs($funds{$fund}{ending} - + ($funds{$fund}{starting} + - $funds{$fund}{Income} - $funds{$fund}{Expenses})) + > $TWO_CENTS) { + print "$fund FAILED SANITY CHECK: Ending: $funds{$fund}{ending} \n\n\n"; + warn "$fund FAILED SANITY CHECK"; + } +} +print "\n"; +printTotal("OVERALL TOTAL", \%tot); +############################################################################### +# +# Local variables: +# compile-command: "perl -c fund-report.plx" +# End: + diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx new file mode 100755 index 00000000..4a4f89cc --- /dev/null +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -0,0 +1,224 @@ +#!/usr/bin/perl +# general-ledger-report.plx -*- Perl -*- +# +# Script to generate a General Ledger report that accountants like +# using Ledger. +# +# Copyright (C) 2011, 2012, 2013 Bradley M. Kuhn +# Copyright (C) 2012 Tom Marble +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; + +my $LEDGER_CMD = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 75; + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} + +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); + +if (@ARGV < 3) { + print STDERR "usage: $0 <BEGIN_DATE> <END_DATE> <OTHER_LEDGER_OPTS>\n"; + exit 1; +} + + +open(MANIFEST, ">", "MANIFEST") or die "Unable to open MANIFEST for writing: $!"; + +my($beginDate, $endDate, @otherLedgerOpts) = @ARGV; + +my $formattedEndDate = new Date::Manip::Date; +die "badly formatted end date, $endDate" if $formattedEndDate->parse($endDate); +my $oneDayLess = new Date::Manip::Delta; +die "bad one day less" if $oneDayLess->parse("- 1 day"); +$formattedEndDate = $formattedEndDate->calc($oneDayLess); +$formattedEndDate = $formattedEndDate->printf("%Y/%m/%d"); + +my $formattedBeginDate = new Date::Manip::Date; +die "badly formatted end date, $beginDate" if $formattedBeginDate->parse($beginDate); +$formattedBeginDate = $formattedBeginDate->printf("%Y/%m/%d"); + + +my(@chartOfAccountsOpts) = ('-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'accounts'); + +open(CHART_DATA, "-|", $LEDGER_CMD, @chartOfAccountsOpts) + or die "Unable to run $LEDGER_CMD @chartOfAccountsOpts: $!"; + +my @accounts; +while (my $line = <CHART_DATA>) { + chomp $line; + next if $line =~ /^\s*\<\s*Adjustment\s*\>\s*$/; + next if $line =~ /^\s*Equity:/; # Stupid auto-account made by ledger. + $line =~ s/^\s*//; $line =~ s/\s*$//; + push(@accounts, $line); + +} +close(CHART_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; + +open(CHART_OUTPUT, ">", "chart-of-accounts.csv") or die "unable to write chart-of-accounts.csv: $!"; +print MANIFEST "chart-of-accounts.csv\n"; + +print CHART_OUTPUT "\"CHART OF ACCOUNTS\","; +print CHART_OUTPUT "\"BEGINNING:\",\"$formattedBeginDate\","; +print CHART_OUTPUT "\"ENDING:\",\"$formattedEndDate\"\n"; + +sub preferredAccountSorting ($$) { + if ($_[0] =~ /^Assets/ and $_[1] !~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Assets/ and $_[0] !~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^(Assets|Liabilities)/) { + return -1; + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^(Assets|Liabilities)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued:[^:]+Receivable)/ and $_[1] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued:[^:]+Receivable)/ and $_[0] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + return 1; + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return -1; + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return 1; + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return -1; + } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return 1; + } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { + return -1; + } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { + return 1; + } else { + return $_[0] cmp $_[1]; + } +} + +my @sortedAccounts; +foreach my $acct ( sort preferredAccountSorting @accounts) { + print CHART_OUTPUT "\"$acct\"\n"; + push(@sortedAccounts, $acct); +} +close(CHART_OUTPUT); die "error writing to chart-of-accounts.txt: $!" unless $? == 0; + +my %commands = ( + 'totalEnd' => [ $LEDGER_CMD, @otherLedgerOpts, '-V', '-X', '$', + '-e', $endDate, '-F', '%-.80A %22.108t\n', '-s', + 'reg' ], + 'totalBegin' => [ $LEDGER_CMD, @otherLedgerOpts, '-V', '-X', '$', + '-e', $beginDate, '-F', '%-.80A %22.108t\n', + '-s', 'reg' ]); + +my %balanceData; + +foreach my $id (keys %commands) { + my(@command) = @{$commands{$id}}; + + open(FILE, "-|", @command) or die "unable to run command ledger command: @command: $!"; + + foreach my $line (<FILE>) { + die "Unable to parse output line from balance data $id command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\<Adjustment\>/ and (abs($amount) <= 0.02); + next if $account =~ /^Equity:/; # Stupid auto-account made by ledger. + $balanceData{$id}{$account} = $amount; + } + close FILE; + die "unable to run balance data ledger command, @command: $!" unless ($? == 0); +} + +open(GL_TEXT_OUT, ">", "general-ledger.txt") or die "unable to write general-ledger.txt: $!"; +print MANIFEST "general-ledger.txt\n"; +open(GL_CSV_OUT, ">", "general-ledger.csv") or die "unable to write general-ledger.csv: $!"; +print MANIFEST "general-ledger.csv\n"; + +my %manifest; +foreach my $acct (@sortedAccounts) { + print GL_TEXT_OUT "\n\nACCOUNT: $acct\nFROM: $beginDate TO $formattedEndDate\n\n"; + my @acctLedgerOpts = ('-V', '-F', + "%(date) %-.10C %-.80P %-.80N %18t %18T\n", '-w', '--sort', 'd', + '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', '/^' . $acct . '$/'); + open(GL_TEXT_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts) + or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!"; + + foreach my $line (<GL_TEXT_DATA>) { + print GL_TEXT_OUT $line; + } + close(GL_TEXT_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; + + print GL_CSV_OUT "\n\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$formattedBeginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n"; + print GL_CSV_OUT '"DATE","CHECK NUM","NAME","TRANSACTION AMT","BALANCE"'; + + my $formatString = '"%(date)","%C","%P","%t",""'; + foreach my $tagField (qw/Receipt Invoice Statement Contract PurchaseOrder Approval Check IncomeDistributionAnalysis CurrencyRate/) { + print GL_CSV_OUT ',"', $tagField, '"'; + $formatString .= ',"link:%(tag(\'' . $tagField . '\'))"'; + } + $formatString .= "\n"; + print GL_CSV_OUT "\n"; + if ($acct =~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + $balanceData{totalBegin}{$acct} = $ZERO unless defined $balanceData{totalBegin}{$acct}; + print GL_CSV_OUT "\"$formattedBeginDate\"", ',"","BALANCE","","$', "$balanceData{totalBegin}{$acct}\"\n"; + } + + @acctLedgerOpts = ('-V', '-F', $formatString, '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', '/^' . $acct . '$/'); + open(GL_CSV_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts) + or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!"; + + foreach my $line (<GL_CSV_DATA>) { + $line =~ s/"link:"/""/g; + print GL_CSV_OUT $line; + next if $line =~ /ACCOUNT:.*PERIOD/; # Skip column header lines + $line =~ s/^"[^"]*","[^"]*","[^"]*","[^"]*","[^"]*",//; + while ($line =~ s/^"([^"]*)"(,|$)//) { + my $file = $1; + next if $file =~ /^\s*$/; + $file =~ s/^link:(.*)$/$1/; + warn "$file does not exist and/or is not readable" unless -r $file; + print MANIFEST "$file\n" if not defined $manifest{$file}; + $manifest{$file} = $line; + } + } + if ($acct =~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + $balanceData{totalEnd}{$acct} = $ZERO unless defined $balanceData{totalEnd}{$acct}; + print GL_CSV_OUT "\"$formattedEndDate\"", ',"","BALANCE","","$', "$balanceData{totalEnd}{$acct}\"\n"; + } + print GL_CSV_OUT "pagebreak\n"; + close(GL_CSV_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; +} +close(GL_TEXT_OUT); die "error writing to general-ledger.txt: $!" unless $? == 0; +close(GL_CSV_OUT); die "error writing to general-ledger.csv: $!" unless $? == 0; +############################################################################### +# +# Local variables: +# compile-command: "perl -c general-ledger-report.plx" +# End: + diff --git a/contrib/non-profit-audit-reports/ooolib2/__init__.py b/contrib/non-profit-audit-reports/ooolib2/__init__.py new file mode 100644 index 00000000..6106fc5c --- /dev/null +++ b/contrib/non-profit-audit-reports/ooolib2/__init__.py @@ -0,0 +1,1987 @@ +"ooolib-python - Copyright (C) 2006-2009 Joseph Colton" + +# ooolib-python - Python module for creating Open Document Format documents. +# Copyright (C) 2006-2009 Joseph Colton + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. + +# This library 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 +# Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +# You can contact me by email at josephcolton@gmail.com + +# Import Standard Modules +import zipfile # Needed for reading/writing documents +import time +import sys +import glob +import os +import re +import xml.parsers.expat # Needed for parsing documents + +def version_number(): + "Get the ooolib-python version number" + return "0.0.17" + +def version(): + "Get the ooolib-python version" + return "ooolib-python-%s" % version_number() + +def clean_string(data): + "Returns an XML friendly copy of the data string" + + data = unicode(data) # This line thanks to Chris Ender + + data = data.replace('&', '&') + data = data.replace("'", ''') + data = data.replace('"', '"') + data = data.replace('<', '<') + data = data.replace('>', '>') + data = data.replace('\t', '<text:tab-stop/>') + data = data.replace('\n', '<text:line-break/>') + return data + +class XML: + "XML Class - Used to convert nested lists into XML" + def __init__(self): + "Initialize ooolib XML instance" + pass + + def _xmldata(self, data): + datatype = data.pop(0) + datavalue = data.pop(0) + outstring = '%s' % datavalue + return outstring + + def _xmltag(self, data): + outstring = '' + # First two + datatype = data.pop(0) + dataname = data.pop(0) + outstring = '<%s' % dataname + # Element Section + element = 1 + while(data): + # elements + newdata = data.pop(0) + if (newdata[0] == 'element' and element): + newstring = self._xmlelement(newdata) + outstring = '%s %s' % (outstring, newstring) + continue + if (newdata[0] != 'element' and element): + element = 0 + outstring = '%s>' % outstring + if (newdata[0] == 'tag' or newdata[0] == 'tagline'): + outstring = '%s\n' % outstring + if (newdata[0] == 'tag'): + newstring = self._xmltag(newdata) + outstring = '%s%s' % (outstring, newstring) + continue + if (newdata[0] == 'tagline'): + newstring = self._xmltagline(newdata) + outstring = '%s%s' % (outstring, newstring) + continue + if (newdata[0] == 'data'): + newstring = self._xmldata(newdata) + outstring = '%s%s' % (outstring, newstring) + continue + if (element): + element = 0 + outstring = '%s>\n' % outstring + outstring = '%s</%s>\n' % (outstring, dataname) + return outstring + + def _xmltagline(self, data): + outstring = '' + # First two + datatype = data.pop(0) + dataname = data.pop(0) + outstring = '<%s' % dataname + # Element Section + while(data): + # elements + newdata = data.pop(0) + if (newdata[0] != 'element'): break + newstring = self._xmlelement(newdata) + outstring = '%s %s' % (outstring, newstring) + outstring = '%s/>\n' % outstring + # Non-Element Section should not exist + return outstring + + def _xmlelement(self, data): + datatype = data.pop(0) + dataname = data.pop(0) + datavalue = data.pop(0) + outstring = '%s="%s"' % (dataname, datavalue) + return outstring + + def convert(self, data): + """Convert nested lists into XML + + The convert method takes a nested lists and converts them + into XML to be used in Open Document Format documents. + There are three types of lists that are recognized at this + time. They are as follows: + + 'tag' - Tag opens a set of data that is eventually closed + with a similar tag. + List: ['tag', 'xml'] + XML: <xml></xml> + + 'tagline' - Taglines are similar to tags, except they open + and close themselves. + List: ['tagline', 'xml'] + XML: <xml/> + + 'element' - Elements are pieces of information stored in an + opening tag or tagline. + List: ['element', 'color', 'blue'] + XML: color="blue" + + 'data' - Data is plain text directly inserted into the XML + document. + List: ['data', 'hello'] + XML: hello + + Bring them all together for something like this. + + Lists: + ['tag', 'xml', ['element', 'a', 'b'], ['tagline', 'xml2'], + ['data', 'asdf']] + + XML: + <xml a="b"><xml2/>asdf</xml> + """ + outlines = [] + outlines.append('<?xml version="1.0" encoding="UTF-8"?>') + if (type(data) == type([]) and len(data) > 0): + if data[0] == 'tag': outlines.append(self._xmltag(data)) + return outlines + +class Meta: + "Meta Data Class" + + def __init__(self, doctype, debug=False): + self.doctype = doctype + + # Set the debug mode + self.debug = debug + + # The generator should always default to the version number + self.meta_generator = version() + self.meta_title = '' + self.meta_subject = '' + self.meta_description = '' + self.meta_keywords = [] + self.meta_creator = 'ooolib-python' + self.meta_editor = '' + self.meta_user1_name = 'Info 1' + self.meta_user2_name = 'Info 2' + self.meta_user3_name = 'Info 3' + self.meta_user4_name = 'Info 4' + self.meta_user1_value = '' + self.meta_user2_value = '' + self.meta_user3_value = '' + self.meta_user4_value = '' + self.meta_creation_date = self.meta_time() + + # Parser data + self.parser_element_list = [] + self.parser_element = "" + self.parser_count = 0 + + def set_meta(self, metaname, value): + """Set meta data in your document. + + Currently implemented metaname options are as follows: + 'creator' - The document author + """ + if metaname == 'creator': self.meta_creator = value + if metaname == 'editor': self.meta_editor = value + if metaname == 'title': self.meta_title = value + if metaname == 'subject': self.meta_subject = value + if metaname == 'description': self.meta_description = value + if metaname == 'user1name': self.meta_user1_name = value + if metaname == 'user2name': self.meta_user2_name = value + if metaname == 'user3name': self.meta_user3_name = value + if metaname == 'user4name': self.meta_user4_name = value + if metaname == 'user1value': self.meta_user1_value = value + if metaname == 'user2value': self.meta_user2_value = value + if metaname == 'user3value': self.meta_user3_value = value + if metaname == 'user4value': self.meta_user4_value = value + if metaname == 'keyword': + if value not in self.meta_keywords: + self.meta_keywords.append(value) + + def get_meta_value(self, metaname): + "Get meta data value for a given metaname." + + if metaname == 'creator': return self.meta_creator + if metaname == 'editor': return self.meta_editor + if metaname == 'title': return self.meta_title + if metaname == 'subject': return self.meta_subject + if metaname == 'description': return self.meta_description + if metaname == 'user1name': return self.meta_user1_name + if metaname == 'user2name': return self.meta_user2_name + if metaname == 'user3name': return self.meta_user3_name + if metaname == 'user4name': return self.meta_user4_name + if metaname == 'user1value': return self.meta_user1_value + if metaname == 'user2value': return self.meta_user2_value + if metaname == 'user3value': return self.meta_user3_value + if metaname == 'user4value': return self.meta_user4_value + if metaname == 'keyword': return self.meta_keywords + + def meta_time(self): + "Return time string in meta data format" + t = time.localtime() + stamp = "%04d-%02d-%02dT%02d:%02d:%02d" % (t[0], t[1], t[2], t[3], t[4], t[5]) + return stamp + + def parse_start_element(self, name, attrs): + if self.debug: print '* Start element:', name + self.parser_element_list.append(name) + self.parser_element = self.parser_element_list[-1] + + # Need the meta name from the user-defined tags + if (self.parser_element == "meta:user-defined"): + self.parser_count += 1 + # Set user-defined name + self.set_meta("user%dname" % self.parser_count, attrs['meta:name']) + + # Debugging statements + if self.debug: print " List: ", self.parser_element_list + if self.debug: print " Attributes: ", attrs + + + def parse_end_element(self, name): + if self.debug: print '* End element:', name + if name != self.parser_element: + print "Tag Mismatch: '%s' != '%s'" % (name, self.parser_element) + self.parser_element_list.pop() + + # Readjust parser_element_list and parser_element + if (self.parser_element_list): + self.parser_element = self.parser_element_list[-1] + else: + self.parser_element = "" + + def parse_char_data(self, data): + if self.debug: print " Character data: ", repr(data) + + # Collect Meta data fields + if (self.parser_element == "dc:title"): + self.set_meta("title", data) + if (self.parser_element == "dc:description"): + self.set_meta("description", data) + if (self.parser_element == "dc:subject"): + self.set_meta("subject", data) + if (self.parser_element == "meta:initial-creator"): + self.set_meta("creator", data) + + # Try to maintain the same creation date + if (self.parser_element == "meta:creation-date"): + self.meta_creation_date = data + + # The user defined fields need to be kept track of, parser_count does that + if (self.parser_element == "meta:user-defined"): + self.set_meta("user%dvalue" % self.parser_count, data) + + def meta_parse(self, data): + "Parse Meta Data from a meta.xml file" + + # Debugging statements + if self.debug: + # Sometimes it helps to see the document that was read from + print data + print "\n\n\n" + + # Create parser + parser = xml.parsers.expat.ParserCreate() + # Set up parser callback functions + parser.StartElementHandler = self.parse_start_element + parser.EndElementHandler = self.parse_end_element + parser.CharacterDataHandler = self.parse_char_data + + # Actually parse the data + parser.Parse(data, 1) + + def get_meta(self): + "Generate meta.xml file data" + self.meta_date = self.meta_time() + self.data = ['tag', 'office:document-meta', + ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'], + ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'], + ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'], + ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'], + ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'], + ['element', 'office:version', '1.0'], + ['tag', 'office:meta', + ['tag', 'meta:generator', # Was: 'OpenOffice.org/2.0$Linux OpenOffice.org_project/680m5$Build-9011' + ['data', self.meta_generator]], # Generator is set the the ooolib-python version. + ['tag', 'dc:title', + ['data', self.meta_title]], # This data is the document title + ['tag', 'dc:description', + ['data', self.meta_description]], # This data is the document description + ['tag', 'dc:subject', + ['data', self.meta_subject]], # This data is the document subject + ['tag', 'meta:initial-creator', + ['data', self.meta_creator]], # This data is the document creator + ['tag', 'meta:creation-date', + ['data', self.meta_creation_date]], # This is the original creation date of the document + ['tag', 'dc:creator', + ['data', self.meta_editor]], # This data is the document editor + ['tag', 'dc:date', + ['data', self.meta_date]], # This is the last modified date of the document + ['tag', 'dc:language', + ['data', 'en-US']], # We will probably always use en-US for language + ['tag', 'meta:editing-cycles', + ['data', '1']], # Edit cycles will probably always be 1 for generated documents + ['tag', 'meta:editing-duration', + ['data', 'PT0S']], # Editing duration is modified - creation date + ['tag', 'meta:user-defined', + ['element', 'meta:name', self.meta_user1_name], + ['data', self.meta_user1_value]], + ['tag', 'meta:user-defined', + ['element', 'meta:name', self.meta_user2_name], + ['data', self.meta_user2_value]], + ['tag', 'meta:user-defined', + ['element', 'meta:name', self.meta_user3_name], + ['data', self.meta_user3_value]], + ['tag', 'meta:user-defined', + ['element', 'meta:name', self.meta_user4_name], + ['data', self.meta_user4_value]]]] +# ['tagline', 'meta:document-statistic', +# ['element', 'meta:table-count', len(self.sheets)], # len(self.sheets) ? +# ['element', 'meta:cell-count', '15']]]] # Not sure how to keep track + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + + + +class CalcStyles: + "Calc Style Management - Used to keep track of created styles." + + def __init__(self): + self.style_config = {} + # Style Counters + self.style_table = 1 + self.style_column = 1 + self.style_row = 1 + self.style_cell = 1 + # Style Properties (Defaults) - To be used later + self.property_column_width_default = '0.8925in' # Default Column Width + self.property_row_height_default = '0.189in' # Default Row Height + # Set Defaults + self.property_column_width = '0.8925in' # Default Column Width + self.property_row_height = '0.189in' # Default Row Height + self.property_cell_bold = False # Bold off be default + self.property_cell_italic = False # Italic off be default + self.property_cell_underline = False # Underline off be default + self.property_cell_fg_color = 'default' # Text Color Default + self.property_cell_bg_color = 'default' # Cell Background Default + self.property_cell_bg_image = 'none' # Cell Background Default + self.property_cell_fontsize = '10' # Cell Font Size Default + self.property_cell_valign = 'default' # Vertial Alignment Default + self.property_cell_halign = 'default' # Horizantal Alignment Default + + def get_next_style(self, style): + "Returns the next style code for the given style" + style_code = "" + if style == 'table': + style_code = 'ta%d' % self.style_table + self.style_table+=1 + if style == 'column': + style_code = 'co%d' % self.style_column + self.style_column+=1 + if style == 'row': + style_code = 'ro%d' % self.style_row + self.style_row+=1 + if style == 'cell': + style_code = 'ce%d' % self.style_cell + self.style_cell+=1 + return style_code + + def set_property(self, style, name, value): + "Sets a property which will later be turned into a code" + if style == 'table': + pass + if style == 'column': + if name == 'style:column-width': self.property_column_width = value + if style == 'row': + if name == 'style:row-height': self.property_row_height = value + if style == 'cell': + if name == 'bold' and type(value) == type(True): self.property_cell_bold = value + if name == 'italic' and type(value) == type(True): self.property_cell_italic = value + if name == 'underline' and type(value) == type(True): self.property_cell_underline = value + if name == 'fontsize': self.property_cell_fontsize = value + if name == 'color': + self.property_cell_fg_color = 'default' + redata = re.search("^(#[\da-fA-F]{6})$", value) + if redata: self.property_cell_fg_color = value.lower() + if name == 'background': + self.property_cell_bg_color = 'default' + redata = re.search("^(#[\da-fA-F]{6})$", value) + if redata: self.property_cell_bg_color = value.lower() + if name == 'backgroundimage': + self.property_cell_bg_image = value + if name == 'valign': + self.property_cell_valign = value + if name == 'halign': + self.property_cell_halign = value + + def get_style_code(self, style): + style_code = "" + if style == 'table': + style_code = "ta1" + if style == 'column': + style_data = tuple([style, + ('style:column-width', self.property_column_width)]) + if style_data in self.style_config: + # Style Exists, return code + style_code = self.style_config[style_data] + else: + # Style does not exist, create code and return it + style_code = self.get_next_style(style) + self.style_config[style_data] = style_code + if style == 'row': + style_data = tuple([style, + ('style:row-height', self.property_row_height)]) + if style_data in self.style_config: + # Style Exists, return code + style_code = self.style_config[style_data] + else: + # Style does not exist, create code and return it + style_code = self.get_next_style(style) + self.style_config[style_data] = style_code + if style == 'cell': + style_data = [style] + # Add additional styles + if self.property_cell_bold: style_data.append(('bold', True)) + if self.property_cell_italic: style_data.append(('italic', True)) + if self.property_cell_underline: style_data.append(('underline', True)) + if self.property_cell_fontsize != '10': + style_data.append(('fontsize', self.property_cell_fontsize)) + if self.property_cell_fg_color != 'default': + style_data.append(('color', self.property_cell_fg_color)) + if self.property_cell_bg_color != 'default': + style_data.append(('background', self.property_cell_bg_color)) + if self.property_cell_bg_image != 'none': + style_data.append(('backgroundimage', self.property_cell_bg_image)) + if self.property_cell_valign != 'default': + style_data.append(('valign', self.property_cell_valign)) + if self.property_cell_halign != 'default': + style_data.append(('halign', self.property_cell_halign)) + + style_data = tuple(style_data) + if style_data in self.style_config: + # Style Exists, return code + style_code = self.style_config[style_data] + else: + # Style does not exist, create code and return it + style_code = self.get_next_style(style) + self.style_config[style_data] = style_code + return style_code + + def get_automatic_styles(self): + "Return 'office:automatic-styles' lists" + automatic_styles = ['tag', 'office:automatic-styles'] + + for style_data in self.style_config: + style_code = self.style_config[style_data] + style_data = list(style_data) + style = style_data.pop(0) + + if style == 'column': + style_list = ['tag', 'style:style', + ['element', 'style:name', style_code], # Column 'co1' properties + ['element', 'style:family', 'table-column']] + tagline = ['tagline', 'style:table-column-properties', + ['element', 'fo:break-before', 'auto']] # unsure what break before means + + for set in style_data: + name, value = set + if name == 'style:column-width': + tagline.append(['element', 'style:column-width', value]) + style_list.append(tagline) + automatic_styles.append(style_list) + + if style == 'row': + style_list = ['tag', 'style:style', + ['element', 'style:name', style_code], # Column 'ro1' properties + ['element', 'style:family', 'table-row']] + tagline = ['tagline', 'style:table-row-properties'] + + for set in style_data: + name, value = set + if name == 'style:row-height': + tagline.append(['element', 'style:row-height', value]) + tagline.append(['element', 'fo:break-before', 'auto']) +# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings + style_list.append(tagline) + automatic_styles.append(style_list) + + if style == 'pagebreak': + style_list = ['tag', 'style:style', + ['element', 'style:name', style_code], # Column 'ro1' properties + ['element', 'style:family', 'table-row']] + tagline = ['tagline', 'style:table-row-properties'] + + for set in style_data: + name, value = set + if name == 'style:row-height': + tagline.append(['element', 'style:row-height', value]) + tagline.append(['element', 'fo:break-before', 'page']) +# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings + style_list.append(tagline) + automatic_styles.append(style_list) + + if style == 'cell': + style_list = ['tag', 'style:style', + ['element', 'style:name', style_code], # ce1 style + ['element', 'style:family', 'table-cell'], # cell + ['element', 'style:parent-style-name', 'Default']] # parent is Default + # hack for currency + if style_code == 'ce1': + style_list.append(['element', + 'style:data-style-name', + 'N104']) + + # Cell Properties + tagline = ['tag', 'style:table-cell-properties'] + tagline_additional = [] + for set in style_data: + name, value = set + if name == 'background': + tagline.append(['element', 'fo:background-color', value]) + if name == 'backgroundimage': + tagline.append(['element', 'fo:background-color', 'transparent']) + # Additional tags added later + bgimagetag = ['tagline', 'style:background-image'] + bgimagetag.append(['element', 'xlink:href', value]) + bgimagetag.append(['element', 'xlink:type', 'simple']) + bgimagetag.append(['element', 'xlink:actuate', 'onLoad']) + tagline_additional.append(bgimagetag) + if name == 'valign': + if value in ['top', 'bottom', 'middle']: + tagline.append(['element', 'style:vertical-align', value]) + if name == 'halign': + tagline.append(['element', 'style:text-align-source', 'fix']) + if value in ['filled']: + tagline.append(['element', 'style:repeat-content', 'true']) + else: + tagline.append(['element', 'style:repeat-content', 'false']) + + # Add any additional internal tags + while tagline_additional: + tagadd = tagline_additional.pop(0) + tagline.append(tagadd) + + style_list.append(tagline) + + # Paragraph Properties + tagline = ['tagline', 'style:paragraph-properties'] + tagline_valid = False + for set in style_data: + name, value = set + if name == 'halign': + tagline_valid = True + if value in ['center']: + tagline.append(['element', 'fo:text-align', 'center']) + if value in ['end', 'right']: + tagline.append(['element', 'fo:text-align', 'end']) + if value in ['start', 'filled', 'left']: + tagline.append(['element', 'fo:text-align', 'start']) + if value in ['justify']: + tagline.append(['element', 'fo:text-align', 'justify']) + # Conditionally add the tagline + if tagline_valid: style_list.append(tagline) + + + # Text Properties + tagline = ['tagline', 'style:text-properties'] + for set in style_data: + name, value = set + if name == 'bold': + tagline.append(['element', 'fo:font-weight', 'bold']) + if name == 'italic': + tagline.append(['element', 'fo:font-style', 'italic']) + if name == 'underline': + tagline.append(['element', 'style:text-underline-style', 'solid']) + tagline.append(['element', 'style:text-underline-width', 'auto']) + tagline.append(['element', 'style:text-underline-color', 'font-color']) + if name == 'color': + tagline.append(['element', 'fo:color', value]) + if name == 'fontsize': + tagline.append(['element', 'fo:font-size', '%spt' % value]) + style_list.append(tagline) + + automatic_styles.append(style_list) + + + # Attach ta1 style + automatic_styles.append(['tag', 'style:style', + ['element', 'style:name', 'ta1'], + ['element', 'style:family', 'table'], + ['element', 'style:master-page-name', 'Default'], + ['tagline', 'style:table-properties', + ['element', 'table:display', 'true'], + ['element', 'style:writing-mode', 'lr-tb']]]) + + + return automatic_styles + + + +class CalcSheet: + "Calc Sheet Class - Used to keep track of the data for an individual sheet." + + def __init__(self, sheetname): + "Initialize a sheet" + self.sheet_name = sheetname + self.sheet_values = {} + self.sheet_config = {} + self.max_col = 0 + self.max_row = 0 + + def get_sheet_dimensions(self): + "Returns the max column and row" + return (self.max_col, self.max_row) + + def clean_formula(self, data): + "Returns a formula for use in ODF" + # Example Translations + # '=SUM(A1:A2)' + # datavalue = 'oooc:=SUM([.A1:.A2])' + # '=IF((A5>A4);A4;"")' + # datavalue = 'oooc:=IF(([.A5]>[.A4]);[.A4];"")' + data = str(data) + data = clean_string(data) + redata = re.search('^=([A-Z]+)(\(.*)$', data) + if redata: + # funct is the function name. The rest if the string will be the functArgs + funct = redata.group(1) + functArgs = redata.group(2) + # Search for cell lebels and replace them + reList = re.findall('([A-Z]+\d+)', functArgs) + # sort and keep track so we do not do a cell more than once + reList.sort() + lastVar = '' + while reList: + # Replace each cell label + curVar = reList.pop() + if curVar == lastVar: continue + lastVar = curVar + functArgs = functArgs.replace(curVar, '[.%s]' % curVar) + data = 'oooc:=%s%s' % (funct, functArgs) + return data + + def get_name(self): + "Returns the sheet name" + return self.sheet_name + + def set_name(self, sheetname): + "Resets the sheet name" + self.sheet_name = sheetname + + def get_sheet_values(self): + "Returns the sheet cell values" + return self.sheet_values + + def get_sheet_value(self, col, row): + "Get the value contents of a cell" + cell = (col, row) + if cell in self.sheet_values: + return self.sheet_values[cell] + else: + return None + + def get_sheet_config(self): + "Returns the sheet cell properties" + return self.sheet_config + + def set_sheet_config(self, location, style_code): + "Sets Style Code for a given location" + self.sheet_config[location] = style_code + + def set_sheet_value(self, cell, datatype, datavalue): + """Sets the value for a specific cell + + cell must be in the format (col, row) where row and col are int. + Example: B5 would be written as (2, 5) + datatype must be one of 'string', 'float', 'formula', 'currency' + datavalue should be a string + """ + # Catch invalid data + if type(cell) != type(()) or len(cell) != 2: + print "Invalid Cell" + return + (col, row) = cell + if type(col) != type(1): + print "Invalid Cell" + return + if type(row) != type(1): + print "Invalid Cell" + return + # Fix String Data + if datatype in ['string', 'annotation']: + datavalue = clean_string(datavalue) + # Fix Link Data. Link's value is a tuple containing (url, description) + if (datatype == 'link'): + url = clean_string(datavalue[0]) + desc = clean_string(datavalue[1]) + datavalue = (url, desc) + # Fix Formula Data + if datatype == 'formula': + datavalue = self.clean_formula(datavalue) + # Adjust maximum sizes + if col > self.max_col: self.max_col = col + if row > self.max_row: self.max_row = row + datatype = str(datatype) + if (datatype not in ['string', 'float', 'currency', 'formula', 'annotation', 'link']): + # Set all unknown cell types to string + datatype = 'string' + datavalue = str(datavalue) + + # The following lines are taken directly from HPS + # self.sheet_values[cell] = (datatype, datavalue) + # HPS: Cell content is now a list of tuples instead of a tuple + # While storing here, store the cell contents first and the annotation next. While generating the XML reverse this + contents = self.sheet_values.get(cell, {'annotation':None,'link':None, 'value':None}) + if datatype == 'annotation': + contents['annotation'] = (datatype, datavalue) + elif datatype == 'link': + contents['link'] = (datatype, datavalue) + else: + contents['value'] = (datatype, datavalue) + + self.sheet_values[cell] = contents + + + def get_lists(self): + "Returns nested lists for XML processing" + if (self.max_col == 0 and self.max_row == 0): + sheet_lists = ['tag', 'table:table', + ['element', 'table:name', self.sheet_name], # Set the Sheet Name + ['element', 'table:style-name', 'ta1'], + ['element', 'table:print', 'false'], + ['tagline', 'table:table-column', + ['element', 'table:style-name', 'co1'], + ['element', 'table:default-cell-style-name', 'Default']], + ['tag', 'table:table-row', + ['element', 'table:style-name', 'ro1'], + ['tagline', 'table:table-cell']]] + else: + # Base Information + sheet_lists = ['tag', 'table:table', + ['element', 'table:name', self.sheet_name], # Set the sheet name + ['element', 'table:style-name', 'ta1'], + ['element', 'table:print', 'false']] + +# ['tagline', 'table:table-column', +# ['element', 'table:style-name', 'co1'], +# ['element', 'table:number-columns-repeated', self.max_col], # max_col? '2' +# ['element', 'table:default-cell-style-name', 'Default']], + + # Need to add column information + for col in range(1, self.max_col+1): + location = ('col', col) + style_code = 'co1' + if location in self.sheet_config: + style_code = self.sheet_config[location] + sheet_lists.append(['tagline', 'table:table-column', + ['element', 'table:style-name', style_code], + ['element', 'table:default-cell-style-name', 'Default']]) + + + # Need to create each row + for row in range(1, self.max_row + 1): + location = ('row', row) + style_code = 'ro1' + if location in self.sheet_config: + style_code = self.sheet_config[location] + rowlist = ['tag', 'table:table-row', + ['element', 'table:style-name', style_code]] + for col in range(1, self.max_col + 1): + cell = (col, row) + style_code = 'ce1' # Default all cells to ce1 + if cell in self.sheet_config: + style_code = self.sheet_config[cell] # Lookup cell if available + if cell in self.sheet_values: + # (datatype, datavalue) = self.sheet_values[cell] # Marked for removal + collist = ['tag', 'table:table-cell'] + if style_code != 'ce1': + collist.append(['element', 'table:style-name', style_code]) + + # Contents, annotations, and links added by HPS + contents = self.sheet_values[cell] # cell contents is a dictionary + if contents['value']: + (datatype, datavalue) = contents['value'] + if datatype == 'float': + collist.append(['element', 'office:value-type', datatype]) + collist.append(['element', 'office:value', datavalue]) + if datatype == 'currency': + collist.append(['element', 'table:style-name', "ce1"]) + collist.append(['element', 'office:value-type', datatype]) + collist.append(['element', 'office:currency', 'USD']) + collist.append(['element', 'office:value', datavalue]) + + if datatype == 'string': + collist.append(['element', 'office:value-type', datatype]) + if datatype == 'formula': + collist.append(['element', 'table:formula', datavalue]) + collist.append(['element', 'office:value-type', 'float']) + collist.append(['element', 'office:value', '0']) + datavalue = '0' + else: + datavalue = None + + if contents['annotation']: + (annotype, annoval) = contents['annotation'] + collist.append(['tag', 'office:annotation', + ['tag', 'text:p', ['data', annoval]]]) + + if contents['link']: + (linktype, linkval) = contents['link'] + if datavalue: + collist.append(['tag', 'text:p', ['data', datavalue], + ['tag', 'text:a', ['element', 'xlink:href', linkval[0]], + ['data', linkval[1]]]]) + else: # no value; just fill the link + collist.append(['tag', 'text:p', + ['tag', 'text:a', ['element', 'xlink:href', linkval[0]], + ['data', linkval[1]]]]) + else: + if datavalue: + collist.append(['tag', 'text:p', ['data', datavalue]]) + + + + else: + collist = ['tagline', 'table:table-cell'] + rowlist.append(collist) + sheet_lists.append(rowlist) + return sheet_lists + +class Calc: + "Calc Class - Used to create OpenDocument Format Calc Spreadsheets." + def __init__(self, sheetname=None, opendoc=None, debug=False): + "Initialize ooolib Calc instance" + # Default to no debugging + self.debug = debug + if not sheetname: sheetname = "Sheet1" + self.sheets = [CalcSheet(sheetname)] # The main sheet will be initially called 'Sheet1' + self.sheet_index = 0 # We initially start on the first sheet + self.styles = CalcStyles() + self.meta = Meta('ods') + self.styles.get_style_code('column') # Force generation of default column + self.styles.get_style_code('row') # Force generation of default row + self.styles.get_style_code('table') # Force generation of default table + self.styles.get_style_code('cell') # Force generation of default cell + self.manifest_files = [] # List of extra files included + self.manifest_index = 1 # Index of added manifest files + + # Data Parsing + self.parser_element_list = [] + self.parser_element = "" + self.parser_sheet_num = 0 + self.parser_sheet_row = 0 + self.parser_sheet_column = 0 + self.parser_cell_repeats = 0 + self.parser_cell_string_pending = False + self.parser_cell_string_line = "" + + # See if we need to read a document + if opendoc: + # Verify that the document exists + if self.debug: print "Opening Document: %s" % opendoc + + # Okay, now we load the file + self.load(opendoc) + + def debug_level(self, level): + """Set debug level: + True if you want debugging messages + False if you do not. + """ + self.debug = level + + def file_mimetype(self, filename): + "Determine the filetype from the filename" + parts = filename.lower().split('.') + ext = parts[-1] + if (ext == 'png'): return (ext, "image/png") + if (ext == 'gif'): return (ext, "image/gif") + return (ext, "image/unknown") + + def add_file(self, filename): + """Prepare a file for loading into ooolib + + The filename should be the local filesystem name for + the file. The file is then prepared to be included in + the creation of the final document. The file needs to + remain in place so that it is available when the actual + document creation happens. + """ + # mimetype set to (ext, filetype) + mimetype = self.file_mimetype(filename) + newname = "Pictures/%08d.%s" % (self.manifest_index, mimetype[0]) + self.manifest_index += 1 + filetype = mimetype[1] + self.manifest_files.append((filename, filetype, newname)) + return newname + + def set_meta(self, metaname, value): + "Set meta data in your document." + self.meta.set_meta(metaname, value) + + def get_meta_value(self, metaname): + "Get meta data value for a given metaname" + return self.meta.get_meta_value(metaname) + + def get_sheet_name(self): + "Returns the sheet name" + return self.sheets[self.sheet_index].get_name() + + def get_sheet_dimensions(self): + "Returns the sheet dimensions in (cols, rows)" + return self.sheets[self.sheet_index].get_sheet_dimensions() + + def set_column_property(self, column, name, value): + "Set Column Properties" + if name == 'width': + # column number column needs column-width set to value + self.styles.set_property('column', 'style:column-width', value) + style_code = self.styles.get_style_code('column') + self.sheets[self.sheet_index].set_sheet_config(('col', column), style_code) + + def set_row_property(self, row, name, value): + "Set row Properties" + if name == 'height': + # row number row needs row-height set to value + self.styles.set_property('row', 'style:row-height', value) + style_code = self.styles.get_style_code('row') + self.sheets[self.sheet_index].set_sheet_config(('row', row), style_code) + + def set_cell_property(self, name, value): + """Turn and off cell properties + + Actual application of properties is handled by setting a value.""" + # background images need to be handled a little differently + # because they need to also be inserted into the final document + if (name == 'backgroundimage'): + # Add file and modify value + value = self.add_file(value) + self.styles.set_property('cell', name, value) + + def get_sheet_index(self): + "Return the current sheet index number" + return self.sheet_index + + def set_sheet_index(self, index): + "Set the sheet index" + if type(index) == type(1): + if index >= 0 and index < len(self.sheets): + self.sheet_index = index + return self.sheet_index + + def get_sheet_count(self): + "Returns the number of existing sheets" + return len(self.sheets) + + def new_sheet(self, sheetname): + "Create a new sheet" + self.sheet_index = len(self.sheets) + self.sheets.append(CalcSheet(sheetname)) + return self.sheet_index + + def set_cell_value(self, col, row, datatype, value): + "Set the value for a given cell" + self.sheets[self.sheet_index].set_sheet_value((col, row), datatype, value) + style_code = self.styles.get_style_code('cell') + self.sheets[self.sheet_index].set_sheet_config((col, row), style_code) + + def get_cell_value(self, col, row): + "Get a cell value tuple (type, value) for a given cell" + sheetvalue = self.sheets[self.sheet_index].get_sheet_value(col, row) + # We stop here if there is no value for sheetvalue + if sheetvalue == None: return sheetvalue + # Now check to see if we have a value tuple + if 'value' in sheetvalue: + return sheetvalue['value'] + else: + return None + + def load(self, filename): + """Load .ods spreadsheet. + + The load function loads data from a document into the current cells. + """ + # Read in the important files + + # meta.xml + data = self._zip_read(filename, "meta.xml") + self.meta.meta_parse(data) + + # content.xml + data = self._zip_read(filename, "content.xml") + self.content_parse(data) + + # settings.xml - I do not remember putting anything here + # styles.xml - I do not remember putting anything here + + def parse_content_start_element(self, name, attrs): + if self.debug: print '* Start element:', name + self.parser_element_list.append(name) + self.parser_element = self.parser_element_list[-1] + + # Keep track of the current sheet number + if (self.parser_element == 'table:table'): + # Move to starting cell + self.parser_sheet_row = 0 + self.parser_sheet_column = 0 + # Increment the sheet number count + self.parser_sheet_num += 1 + if (self.parser_sheet_num - 1 != self.sheet_index): + # We are not on the first sheet and need to create a new sheet. + # We will automatically move to the new sheet + sheetname = "Sheet%d" % self.parser_sheet_num + if 'table:name' in attrs: sheetname = attrs['table:name'] + self.new_sheet(sheetname) + else: + # We are on the first sheet and will need to overwrite the default name + sheetname = "Sheet%d" % self.parser_sheet_num + if 'table:name' in attrs: sheetname = attrs['table:name'] + self.sheets[self.sheet_index].set_name(sheetname) + + # Update the row numbers + if (self.parser_element == 'table:table-row'): + self.parser_sheet_row += 1 + self.parser_sheet_column = 0 + + # Okay, now keep track of the sheet cell data + if (self.parser_element == 'table:table-cell'): + # By default it will repeat zero times + self.parser_cell_repeats = 0 + # We must be in a new column + self.parser_sheet_column += 1 + # Set some default values + datatype = "" + value = "" + # Get values from attrs hash + if 'office:value-type' in attrs: datatype = attrs['office:value-type'] + if 'office:value' in attrs: value = attrs['office:value'] + if 'table:formula' in attrs: + datatype = 'formula' + value = attrs['table:formula'] + if datatype == 'string': + datatype = "" + self.parser_cell_string_pending = True + self.parser_cell_string_line = "" + if 'table:number-columns-repeated' in attrs: + self.parser_cell_repeats = int(attrs['table:number-columns-repeated']) - 1 + # Set the cell value + if datatype: + # I should do this once per cell repeat above 0 + for i in range(0, self.parser_cell_repeats+1): + self.set_cell_value(self.parser_sheet_column+i, self.parser_sheet_row, datatype, value) + + # There are lots of interesting cases with table:table-cell data. One problem is + # reading the number of embedded spaces correctly. This code should help us get + # the number of spaces out. + + if (self.parser_element == 'text:s'): + # This means we have a number of spaces + count_num = 0 + if 'text:c' in attrs: + count_alpha = attrs['text:c'] + if (count_alpha.isdigit()): + count_num = int(count_alpha) + # I am not sure what to do if we do not have a string pending + if (self.parser_cell_string_pending == True): + # Append the currect number of spaces to the end + self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, ' '*count_num) + + if (self.parser_element == 'text:tab-stop'): + if (self.parser_cell_string_pending == True): + self.parser_cell_string_line = "%s\t" % (self.parser_cell_string_line) + + if (self.parser_element == 'text:line-break'): + if (self.parser_cell_string_pending == True): + self.parser_cell_string_line = "%s\n" % (self.parser_cell_string_line) + + # Debugging statements + if self.debug: print " List: ", self.parser_element_list + if self.debug: print " Attributes: ", attrs + + + def parse_content_end_element(self, name): + if self.debug: print '* End element:', name + if name != self.parser_element: + print "Tag Mismatch: '%s' != '%s'" % (name, self.parser_element) + self.parser_element_list.pop() + + # If the element was text:p and we are in string mode + if (self.parser_element == 'text:p'): + if (self.parser_cell_string_pending): + self.parser_cell_string_pending = False + + # Take care of repeated cells + if (self.parser_element == 'table:table-cell'): + self.parser_sheet_column += self.parser_cell_repeats + + # Readjust parser_element_list and parser_element + if (self.parser_element_list): + self.parser_element = self.parser_element_list[-1] + else: + self.parser_element = "" + + def parse_content_char_data(self, data): + if self.debug: print " Character data: ", repr(data) + + if (self.parser_element == 'text:p' or self.parser_element == 'text:span'): + if (self.parser_cell_string_pending): + # Set the string and leave string pending mode + # This does feel a little kludgy, but it does the job + self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, data) + + # I should do this once per cell repeat above 0 + for i in range(0, self.parser_cell_repeats+1): + self.set_cell_value(self.parser_sheet_column+i, self.parser_sheet_row, + 'string', self.parser_cell_string_line) + + + def content_parse(self, data): + "Parse Content Data from a content.xml file" + + # Debugging statements + if self.debug: + # Sometimes it helps to see the document that was read from + print data + print "\n\n\n" + + # Create parser + parser = xml.parsers.expat.ParserCreate() + # Set up parser callback functions + parser.StartElementHandler = self.parse_content_start_element + parser.EndElementHandler = self.parse_content_end_element + parser.CharacterDataHandler = self.parse_content_char_data + + # Actually parse the data + parser.Parse(data, 1) + + def save(self, filename): + """Save .ods spreadsheet. + + The save function saves the current cells and settings into a document. + """ + if self.debug: print "Writing %s" % filename + self.savefile = zipfile.ZipFile(filename, "w") + if self.debug: print " meta.xml" + self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta()) + if self.debug: print " mimetype" + self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.spreadsheet") + if self.debug: print " Configurations2/accelerator/current.xml" + self._zip_insert(self.savefile, "Configurations2/accelerator/current.xml", "") + if self.debug: print " META-INF/manifest.xml" + self._zip_insert(self.savefile, "META-INF/manifest.xml", self._ods_manifest()) + if self.debug: print " content.xml" + self._zip_insert(self.savefile, "content.xml", self._ods_content()) + if self.debug: print " settings.xml" + self._zip_insert(self.savefile, "settings.xml", self._ods_settings()) + if self.debug: print " styles.xml" + self._zip_insert(self.savefile, "styles.xml", self._ods_styles()) + + # Add additional files if needed + for fileset in self.manifest_files: + (filename, filetype, newname) = fileset + # Read in the file + data = self._file_load(filename) + if self.debug: print " Inserting '%s' as '%s'" % (filename, newname) + self._zip_insert_binary(self.savefile, newname, data) + + def _file_load(self, filename): + "Load a file" + file = open(filename, "rb") + data = file.read() + file.close() + return data + + def _zip_insert_binary(self, file, filename, data): + "Insert a binary file into the zip archive" + now = time.localtime(time.time())[:6] + info = zipfile.ZipInfo(filename) + info.date_time = now + info.compress_type = zipfile.ZIP_DEFLATED + file.writestr(info, data) + + + def _zip_insert(self, file, filename, data): + "Insert a file into the zip archive" + + # zip seems to struggle with non-ascii characters + data = data.encode('utf-8') + + now = time.localtime(time.time())[:6] + info = zipfile.ZipInfo(filename) + info.date_time = now + info.compress_type = zipfile.ZIP_DEFLATED + file.writestr(info, data) + + def _zip_read(self, file, filename): + "Get the data from a file in the zip archive by filename" + file = zipfile.ZipFile(file, "r") + data = file.read(filename) + # Need to close the file + file.close() + return data + + def _ods_content(self): + "Generate ods content.xml data" + + # This will list all of the sheets in the document + self.sheetdata = ['tag', 'office:spreadsheet'] + for sheet in self.sheets: + if self.debug: + sheet_name = sheet.get_name() + print " Creating Sheet '%s'" % sheet_name + sheet_list = sheet.get_lists() + self.sheetdata.append(sheet_list) + # Automatic Styles + self.automatic_styles = self.styles.get_automatic_styles() + + self.data = ['tag', 'office:document-content', + ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'], + ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'], + ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'], + ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'], + ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'], + ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'], + ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'], + ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'], + ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'], + ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'], + ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'], + ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'], + ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'], + ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'], + ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'], + ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'], + ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'], + ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'], + ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'], + ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'], + ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'], + ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'], + ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'], + ['element', 'office:version', '1.0'], + ['tagline', 'office:scripts'], + ['tag', 'office:font-face-decls', + ['tagline', 'style:font-face', + ['element', 'style:name', 'DejaVu Sans'], + ['element', 'svg:font-family', ''DejaVu Sans''], + ['element', 'style:font-pitch', 'variable']], + ['tagline', 'style:font-face', + ['element', 'style:name', 'Nimbus Sans L'], + ['element', 'svg:font-family', ''Nimbus Sans L''], + ['element', 'style:font-family-generic', 'swiss'], + ['element', 'style:font-pitch', 'variable']]], + + # Automatic Styles + self.automatic_styles, + + ['tag', 'office:body', + self.sheetdata]] # Sheets are generated from the CalcSheet class + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + + def _ods_manifest(self): + "Generate ods manifest.xml data" + self.data = ['tag', 'manifest:manifest', + ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet'], + ['element', 'manifest:full-path', '/']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'application/vnd.sun.xml.ui.configuration'], + ['element', 'manifest:full-path', 'Configurations2/']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', ''], + ['element', 'manifest:full-path', 'Configurations2/accelerator/']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', ''], + ['element', 'manifest:full-path', 'Configurations2/accelerator/current.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'content.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'styles.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'meta.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'settings.xml']]] + + # Add additional files to manifest list + for fileset in self.manifest_files: + (filename, filetype, newname) = fileset + addfile = ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', filetype], + ['element', 'manifest:full-path', newname]] + self.data.append(addfile) + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + + + def _ods_settings(self): + "Generate ods settings.xml data" + self.data = ['tag', 'office:document-settings', + ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'], + ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'], + ['element', 'xmlns:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0'], + ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'], + ['element', 'office:version', '1.0'], + ['tag', 'office:settings', + ['tag', 'config:config-item-set', + ['element', 'config:name', 'ooo:view-settings'], + ['tag', 'config:config-item', + ['element', 'config:name', 'VisibleAreaTop'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'VisibleAreaLeft'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'VisibleAreaWidth'], + ['element', 'config:type', 'int'], + ['data', '6774']], + ['tag', 'config:config-item', + ['element', 'config:name', 'VisibleAreaHeight'], + ['element', 'config:type', 'int'], + ['data', '2389']], + ['tag', 'config:config-item-map-indexed', + ['element', 'config:name', 'Views'], + ['tag', 'config:config-item-map-entry', + ['tag', 'config:config-item', + ['element', 'config:name', 'ViewId'], + ['element', 'config:type', 'string'], + ['data', 'View1']], + ['tag', 'config:config-item-map-named', + ['element', 'config:name', 'Tables'], + ['tag', 'config:config-item-map-entry', + ['element', 'config:name', 'Sheet1'], + ['tag', 'config:config-item', + ['element', 'config:name', 'CursorPositionX'], # Cursor Position A + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'CursorPositionY'], # Cursor Position 1 + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HorizontalSplitMode'], + ['element', 'config:type', 'short'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'VerticalSplitMode'], + ['element', 'config:type', 'short'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HorizontalSplitPosition'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'VerticalSplitPosition'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ActiveSplitRange'], + ['element', 'config:type', 'short'], + ['data', '2']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PositionLeft'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PositionRight'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PositionTop'], + ['element', 'config:type', 'int'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PositionBottom'], + ['element', 'config:type', 'int'], + ['data', '0']]]], + ['tag', 'config:config-item', + ['element', 'config:name', 'ActiveTable'], + ['element', 'config:type', 'string'], + ['data', 'Sheet1']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HorizontalScrollbarWidth'], + ['element', 'config:type', 'int'], + ['data', '270']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ZoomType'], + ['element', 'config:type', 'short'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ZoomValue'], + ['element', 'config:type', 'int'], + ['data', '100']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PageViewZoomValue'], + ['element', 'config:type', 'int'], + ['data', '60']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowPageBreakPreview'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowZeroValues'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowNotes'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowGrid'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'GridColor'], + ['element', 'config:type', 'long'], + ['data', '12632256']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowPageBreaks'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HasColumnRowHeaders'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HasSheetTabs'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsOutlineSymbolsSet'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsSnapToRaster'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterIsVisible'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterResolutionX'], + ['element', 'config:type', 'int'], + ['data', '1270']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterResolutionY'], + ['element', 'config:type', 'int'], + ['data', '1270']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterSubdivisionX'], + ['element', 'config:type', 'int'], + ['data', '1']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterSubdivisionY'], + ['element', 'config:type', 'int'], + ['data', '1']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsRasterAxisSynchronized'], + ['element', 'config:type', 'boolean'], + ['data', 'true']]]]], + ['tag', 'config:config-item-set', + ['element', 'config:name', 'ooo:configuration-settings'], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowZeroValues'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowNotes'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowGrid'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'GridColor'], + ['element', 'config:type', 'long'], + ['data', '12632256']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ShowPageBreaks'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'LinkUpdateMode'], + ['element', 'config:type', 'short'], + ['data', '3']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HasColumnRowHeaders'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'HasSheetTabs'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsOutlineSymbolsSet'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsSnapToRaster'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterIsVisible'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterResolutionX'], + ['element', 'config:type', 'int'], + ['data', '1270']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterResolutionY'], + ['element', 'config:type', 'int'], + ['data', '1270']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterSubdivisionX'], + ['element', 'config:type', 'int'], + ['data', '1']], + ['tag', 'config:config-item', + ['element', 'config:name', 'RasterSubdivisionY'], + ['element', 'config:type', 'int'], + ['data', '1']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsRasterAxisSynchronized'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'AutoCalculate'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PrinterName'], + ['element', 'config:type', 'string'], + ['data', 'Generic Printer']], + ['tag', 'config:config-item', + ['element', 'config:name', 'PrinterSetup'], + ['element', 'config:type', 'base64Binary'], + ['data', 'YgH+/0dlbmVyaWMgUHJpbnRlcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU0dFTlBSVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAMAqAAAAAAA//8FAFZUAAAkbQAASm9iRGF0YSAxCnByaW50ZXI9R2VuZXJpYyBQcmludGVyCm9yaWVudGF0aW9uPVBvcnRyYWl0CmNvcGllcz0xCnNjYWxlPTEwMAptYXJnaW5kYWp1c3RtZW50PTAsMCwwLDAKY29sb3JkZXB0aD0yNApwc2xldmVsPTAKY29sb3JkZXZpY2U9MApQUERDb250ZXhEYXRhClBhZ2VTaXplOkxldHRlcgAA']], + ['tag', 'config:config-item', + ['element', 'config:name', 'ApplyUserData'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'CharacterCompressionType'], + ['element', 'config:type', 'short'], + ['data', '0']], + ['tag', 'config:config-item', + ['element', 'config:name', 'IsKernAsianPunctuation'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'SaveVersionOnClose'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'UpdateFromTemplate'], + ['element', 'config:type', 'boolean'], + ['data', 'false']], + ['tag', 'config:config-item', + ['element', 'config:name', 'AllowPrintJobCancel'], + ['element', 'config:type', 'boolean'], + ['data', 'true']], + ['tag', 'config:config-item', + ['element', 'config:name', 'LoadReadonly'], + ['element', 'config:type', 'boolean'], + ['data', 'false']]]]] + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + + + def _ods_styles(self): + "Generate ods styles.xml data" + self.data = ['tag', 'office:document-styles', + ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'], + ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'], + ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'], + ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'], + ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'], + ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'], + ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'], + ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'], + ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'], + ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'], + ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'], + ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'], + ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'], + ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'], + ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'], + ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'], + ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'], + ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'], + ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'], + ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'], + ['element', 'office:version', '1.0'], + ['tag', 'office:font-face-decls', + ['tagline', 'style:font-face', + ['element', 'style:name', 'DejaVu Sans'], + ['element', 'svg:font-family', ''DejaVu Sans''], + ['element', 'style:font-pitch', 'variable']], + ['tagline', 'style:font-face', + ['element', 'style:name', 'Nimbus Sans L'], + ['element', 'svg:font-family', ''Nimbus Sans L''], + ['element', 'style:font-family-generic', 'swiss'], + ['element', 'style:font-pitch', 'variable']]], + ['tag', 'office:styles', + ['tag', 'style:default-style', + ['element', 'style:family', 'table-cell'], + ['tagline', 'style:table-cell-properties', + ['element', 'style:decimal-places', '2']], + ['tagline', 'style:paragraph-properties', + ['element', 'style:tab-stop-distance', '0.5in']], + ['tagline', 'style:text-properties', + ['element', 'style:font-name', 'Nimbus Sans L'], + ['element', 'fo:language', 'en'], + ['element', 'fo:country', 'US'], + ['element', 'style:font-name-asian', 'DejaVu Sans'], + ['element', 'style:language-asian', 'none'], + ['element', 'style:country-asian', 'none'], + ['element', 'style:font-name-complex', 'DejaVu Sans'], + ['element', 'style:language-complex', 'none'], + ['element', 'style:country-complex', 'none']]], + ['tag', 'number:number-style', + ['element', 'style:name', 'N0'], + ['tagline', 'number:number', + ['element', 'number:min-integer-digits', '1']]], + ['tag', 'number:currency-style', + ['element', 'style:name', 'N104P0'], + ['element', 'style:volatile', 'true'], + ['tag', 'number:currency-symbol', + ['element', 'number:language', 'en'], + ['element', 'number:country', 'US'], + ['data', '$']], + ['tagline', 'number:number', + ['element', 'number:decimal-places', '2'], + ['element', 'number:min-integer-digits', '1'], + ['element', 'number:grouping', 'true']]], + ['tag', 'number:currency-style', + ['element', 'style:name', 'N104'], + ['tagline', 'style:text-properties', + ['element', 'fo:color', '#ff0000']], + ['tag', 'number:text', + ['data', '-']], + ['tag', 'number:currency-symbol', + ['element', 'number:language', 'en'], + ['element', 'number:country', 'US'], + ['data', '$']], + ['tagline', 'number:number', + ['element', 'number:decimal-places', '2'], + ['element', 'number:min-integer-digits', '1'], + ['element', 'number:grouping', 'true']], + ['tagline', 'style:map', + ['element', 'style:condition', 'value()>=0'], + ['element', 'style:apply-style-name', 'N104P0']]], + ['tagline', 'style:style', + ['element', 'style:name', 'Default'], + ['element', 'style:family', 'table-cell']], + ['tag', 'style:style', + ['element', 'style:name', 'Result'], + ['element', 'style:family', 'table-cell'], + ['element', 'style:parent-style-name', 'Default'], + ['tagline', 'style:text-properties', + ['element', 'fo:font-style', 'italic'], + ['element', 'style:text-underline-style', 'solid'], + ['element', 'style:text-underline-width', 'auto'], + ['element', 'style:text-underline-color', 'font-color'], + ['element', 'fo:font-weight', 'bold']]], + ['tagline', 'style:style', + ['element', 'style:name', 'Result2'], + ['element', 'style:family', 'table-cell'], + ['element', 'style:parent-style-name', 'Result'], + ['element', 'style:data-style-name', 'N104']], + ['tag', 'style:style', + ['element', 'style:name', 'Heading'], + ['element', 'style:family', 'table-cell'], + ['element', 'style:parent-style-name', 'Default'], + ['tagline', 'style:table-cell-properties', + ['element', 'style:text-align-source', 'fix'], + ['element', 'style:repeat-content', 'false']], + ['tagline', 'style:paragraph-properties', + ['element', 'fo:text-align', 'center']], + ['tagline', 'style:text-properties', + ['element', 'fo:font-size', '16pt'], + ['element', 'fo:font-style', 'italic'], + ['element', 'fo:font-weight', 'bold']]], + ['tag', 'style:style', + ['element', 'style:name', 'Heading1'], + ['element', 'style:family', 'table-cell'], + ['element', 'style:parent-style-name', 'Heading'], + ['tagline', 'style:table-cell-properties', + ['element', 'style:rotation-angle', '90']]]], + ['tag', 'office:automatic-styles', + ['tag', 'style:page-layout', + ['element', 'style:name', 'pm1'], + ['tagline', 'style:page-layout-properties', + ['element', 'style:writing-mode', 'lr-tb']], + ['tag', 'style:header-style', + ['tagline', 'style:header-footer-properties', + ['element', 'fo:min-height', '0.2957in'], + ['element', 'fo:margin-left', '0in'], + ['element', 'fo:margin-right', '0in'], + ['element', 'fo:margin-bottom', '0.0984in']]], + ['tag', 'style:footer-style', + ['tagline', 'style:header-footer-properties', + ['element', 'fo:min-height', '0.2957in'], + ['element', 'fo:margin-left', '0in'], + ['element', 'fo:margin-right', '0in'], + ['element', 'fo:margin-top', '0.0984in']]]], + ['tag', 'style:page-layout', + ['element', 'style:name', 'pm2'], + ['tagline', 'style:page-layout-properties', + ['element', 'style:writing-mode', 'lr-tb']], + ['tag', 'style:header-style', + ['tag', 'style:header-footer-properties', + ['element', 'fo:min-height', '0.2957in'], + ['element', 'fo:margin-left', '0in'], + ['element', 'fo:margin-right', '0in'], + ['element', 'fo:margin-bottom', '0.0984in'], + ['element', 'fo:border', '0.0346in solid #000000'], + ['element', 'fo:padding', '0.0071in'], + ['element', 'fo:background-color', '#c0c0c0'], + ['tagline', 'style:background-image']]], + ['tag', 'style:footer-style', + ['tag', 'style:header-footer-properties', + ['element', 'fo:min-height', '0.2957in'], + ['element', 'fo:margin-left', '0in'], + ['element', 'fo:margin-right', '0in'], + ['element', 'fo:margin-top', '0.0984in'], + ['element', 'fo:border', '0.0346in solid #000000'], + ['element', 'fo:padding', '0.0071in'], + ['element', 'fo:background-color', '#c0c0c0'], + ['tagline', 'style:background-image']]]]], + ['tag', 'office:master-styles', + ['tag', 'style:master-page', + ['element', 'style:name', 'Default'], + ['element', 'style:page-layout-name', 'pm1'], + ['tag', 'style:header', + ['tag', 'text:p', + ['data', '<text:sheet-name>???</text:sheet-name>']]], + ['tagline', 'style:header-left', + ['element', 'style:display', 'false']], + ['tag', 'style:footer', + ['tag', 'text:p', + ['data', 'Page <text:page-number>1</text:page-number>']]], + ['tagline', 'style:footer-left', + ['element', 'style:display', 'false']]], + ['tag', 'style:master-page', + ['element', 'style:name', 'Report'], + ['element', 'style:page-layout-name', 'pm2'], + ['tag', 'style:header', + ['tag', 'style:region-left', + ['tag', 'text:p', + ['data', '<text:sheet-name>???</text:sheet-name> (<text:title>???</text:title>)']]], + ['tag', 'style:region-right', + ['tag', 'text:p', + ['data', '<text:date style:data-style-name="N2" text:date-value="2006-09-29">09/29/2006</text:date>, <text:time>13:02:56</text:time>']]]], + ['tagline', 'style:header-left', + ['element', 'style:display', 'false']], + ['tag', 'style:footer', + ['tag', 'text:p', + ['data', 'Page <text:page-number>1</text:page-number> / <text:page-count>99</text:page-count>']]], + ['tagline', 'style:footer-left', + ['element', 'style:display', 'false']]]]] + + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + +class Writer: + "Writer Class - Used to create OpenDocument Format Writer Documents." + def __init__(self): + "Initialize ooolib Writer instance" + # Default to no debugging + self.debug = False + self.meta = Meta('odt') + + def set_meta(self, metaname, value): + "Set meta data in your document." + self.meta.set_meta(metaname, value) + + def save(self, filename): + """Save .odt document + + The save function saves the current .odt document. + """ + if self.debug: print "Writing %s" % filename + self.savefile = zipfile.ZipFile(filename, "w") + if self.debug: print " meta.xml" + self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta()) + if self.debug: print " mimetype" + self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.text") + if self.debug: print " META-INF/manifest.xml" + self._zip_insert(self.savefile, "META-INF/manifest.xml", self._odt_manifest()) + if self.debug: print " content.xml" + self._zip_insert(self.savefile, "content.xml", self._odt_content()) + if self.debug: print " settings.xml" + # self._zip_insert(self.savefile, "settings.xml", self._odt_settings()) + if self.debug: print " styles.xml" + # self._zip_insert(self.savefile, "styles.xml", self._odt_styles()) + + # We need to close the file now that we are done creating it. + self.savefile.close() + + def _zip_insert(self, file, filename, data): + now = time.localtime(time.time())[:6] + info = zipfile.ZipInfo(filename) + info.date_time = now + info.compress_type = zipfile.ZIP_DEFLATED + file.writestr(info, data) + + def _odt_manifest(self): + "Generate odt manifest.xml data" + + self.data = ['tag', 'manifest:manifest', + ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.text'], + ['element', 'manifest:full-path', '/']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'content.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'styles.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'meta.xml']], + ['tagline', 'manifest:file-entry', + ['element', 'manifest:media-type', 'text/xml'], + ['element', 'manifest:full-path', 'settings.xml']]] + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.lines.insert(1, '<!DOCTYPE manifest:manifest PUBLIC "-//OpenOffice.org//DTD Manifest 1.0//EN" "Manifest.dtd">') + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + + def _odt_content(self): + "Generate odt content.xml data" + + self.data = ['tag', 'office:document-content', + ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'], + ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'], + ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'], + ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'], + ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'], + ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'], + ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'], + ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'], + ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'], + ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'], + ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'], + ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'], + ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'], + ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'], + ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'], + ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'], + ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'], + ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'], + ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'], + ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'], + ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'], + ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'], + ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'], + ['element', 'office:version', '1.0'], + ['tagline', 'office:scripts'], + ['tag', 'office:font-face-decls', + ['tagline', 'style:font-face', + ['element', 'style:name', 'DejaVu Sans'], + ['element', 'svg:font-family', ''DejaVu Sans''], + ['element', 'style:font-pitch', 'variable']], + ['tagline', 'style:font-face', + ['element', 'style:name', 'Nimbus Roman No9 L'], + ['element', 'svg:font-family', ''Nimbus Roman No9 L''], + ['element', 'style:font-family-generic', 'roman'], + ['element', 'style:font-pitch', 'variable']], + ['tagline', 'style:font-face', + ['element', 'style:name', 'Nimbus Sans L'], + ['element', 'svg:font-family', ''Nimbus Sans L''], + ['element', 'style:font-family-generic', 'swiss'], + ['element', 'style:font-pitch', 'variable']]], + ['tagline', 'office:automatic-styles'], + ['tag', 'office:body', + ['tag', 'office:text', + ['tagline', 'office:forms', + ['element', 'form:automatic-focus', 'false'], + ['element', 'form:apply-design-mode', 'false']], + ['tag', 'text:sequence-decls', + ['tagline', 'text:sequence-decl', + ['element', 'text:display-outline-level', '0'], + ['element', 'text:name', 'Illustration']], + ['tagline', 'text:sequence-decl', + ['element', 'text:display-outline-level', '0'], + ['element', 'text:name', 'Table']], + ['tagline', 'text:sequence-decl', + ['element', 'text:display-outline-level', '0'], + ['element', 'text:name', 'Text']], + ['tagline', 'text:sequence-decl', + ['element', 'text:display-outline-level', '0'], + ['element', 'text:name', 'Drawing']]], + ['tagline', 'text:p', + ['element', 'text:style-name', 'Standard']]]]] + + # Generate content.xml XML data + xml = XML() + self.lines = xml.convert(self.data) + self.filedata = '\n'.join(self.lines) + # Return generated data + return self.filedata + + diff --git a/contrib/non-profit-audit-reports/readcsv.py b/contrib/non-profit-audit-reports/readcsv.py new file mode 100755 index 00000000..67fc5663 --- /dev/null +++ b/contrib/non-profit-audit-reports/readcsv.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# readcsv.py +# CSV reading technical study +# +# Copyright (c) 2012 Tom Marble +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +import csv + +dialects = csv.list_dialects() +for dialect in dialects: + print 'dialect %s' % str(dialect) + +csvfile = open('tests/general-ledger.csv', 'rb') +reader = csv.reader(csvfile, delimiter=',', quotechar='"') +for row in reader: + print row diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx new file mode 100755 index 00000000..ddc97737 --- /dev/null +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -0,0 +1,465 @@ +#!/usr/bin/perl +# fund-report.plx -*- Perl -*- +# +# Script to generate end-of-year summary reports. +# +# Copyright (C) 2011, 2012, 2013, Bradley M. Kuhn +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; + +my $VERBOSE = 0; +my $DEBUG = 0; + +my $LEDGER_BIN = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 70; + +sub Commify ($) { + my $text = reverse $_[0]; + $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; + return scalar reverse $text; +} + +sub preferredAccountSorting ($$) { + if ($_[0] =~ /^Assets/ and $_[1] !~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Assets/ and $_[0] !~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^(Assets|Liabilities)/) { + return -1; + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^(Assets|Liabilities)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued:[^:]+Receivable)/ and $_[1] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued:[^:]+Receivable)/ and $_[0] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + return 1; + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return -1; + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return 1; + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return -1; + } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return 1; + } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { + return -1; + } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { + return 1; + } else { + return $_[0] cmp $_[1]; + } +} + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); +my $ONE_PENNY = Math::BigFloat->new("0.01"); +my $TWO_CENTS = Math::BigFloat->new("0.02"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <START_DATE> <END_DATE> <LEDGER_OPTIONS>\n"; + exit 1; +} +my($startDate, $endDate, @mainLedgerOptions) = @ARGV; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%B %e, %Y"); +die "Date calculation error on $endDate" if ($err); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%B %e, %Y"); +die "Date calculation error on $startDate" if ($err); + +my %reportFields = + ('Cash' => { args => [ '-e', $endDate, 'bal', '/^Assets/' ] }, + 'Accounts Receivable' => {args => [ '-e', $endDate, 'bal', '/^Accrued:Accounts Receivable/' ]}, + 'Loans/Fraud Receivable' => {args => [ '-e', $endDate, 'bal', '/^Accrued:(Loans|Fraud) Receivable/' ]}, + 'Accounts Payable' => {args => [ '-e', $endDate, 'bal', '/^Accrued.*Accounts Payable/' ]}, + 'Accrued Expenses' => {args => [ '-e', $endDate, 'bal', '/^Accrued.*Expenses/' ]}, + 'Liabilities, Credit Cards' => {args => [ '-e', $endDate, 'bal', '/^Liabilities:Credit Card/' ]}, + 'Liabilities, Other' => {args => [ '-e', $endDate, 'bal', '/^Liabilities/', + 'and', 'not', '/^Liabilities:Credit Card/']}, + 'Unearned Income, Conference Registration' => {args => [ '-e', $endDate, 'bal', + '/^Unearned Income.*Reg/' ]}, + 'Unearned Income, Other' => {args => [ '-e', $endDate, 'bal', '/^Unearned Income/', 'and', 'not', + '/^Unearned Income.*Reg/' ]}, + 'Unrestricted Net Assets' => {args => [ '-e', $endDate, 'bal', '/^(Income|Expenses):Conservancy/' ]}, + 'Temporarily Restricted Net Assets' => {args => [ '-e', $endDate, 'bal', '/^(Income|Expenses)/', + 'and', 'not', '/^(Unearned Income|(Income|Expenses):Conservancy)/' ]}, + 'Total Net Assets' => {args => [ '-e', $endDate, 'bal', '/^(Income|Expenses)/' ]}, + +); +foreach my $item (keys %reportFields) { + my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, + '-V', '-X', '$', '-S', 'T', '-s', '-d', 'T', @{$reportFields{$item}{args}}); + open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + + my $foundBalance; + my $seenTotalLine = 0; + + print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + print STDERR " Output of @fullCommand\n" if $DEBUG; + + while (my $line = <FILE>) { + print STDERR $line if ($DEBUG); + + $seenTotalLine = 1 if $line =~ /^\s*\-+\s*/; # Skip lines until the total line + $foundBalance = $1 + if (not $seenTotalLine and $line =~ /^\s*[^0-9\-]+\s*([\-\d,\.]+)\s+/); + + if ($line =~ /^\s*\$\s*([\-\d,\.]+)\s*$/) { + $foundBalance = $1; + last; + } + } + close FILE; + die "problem running ledger command: @fullCommand: $!" unless ($? == 0); + if (not defined $foundBalance) { + $foundBalance = $ZERO; + } else { + $foundBalance =~ s/,//g; + $foundBalance = Math::BigFloat->new($foundBalance); + } + $foundBalance = $ZERO if not defined $foundBalance; + $reportFields{$item}{total} = abs($foundBalance); + print STDERR "$item: $reportFields{$item}{total}\n" if $VERBOSE; +} + +open(BALANCE_SHEET, ">", "balance-sheet.csv") + or die "unable to open balance-sheet.csv for writing: $!"; + +print BALANCE_SHEET "\"BALANCE SHEET\"\n", + "\"Ending\",\"", $formattedEndDate, "\"\n", + "\n\n\"ASSETS\"\n\n"; + +my $formatStr = "\"\",\"%-42s\",\"\$%13s\"\n"; +my $formatStrTotal = "\"\",\"%-45s\",\"\$%13s\"\n"; +my $tot = $ZERO; +foreach my $item ('Cash', 'Accounts Receivable', 'Loans/Fraud Receivable') { + next if $reportFields{$item}{total} == $ZERO; + print BALANCE_SHEET sprintf($formatStr, "$item:", Commify($reportFields{$item}{total})); + $tot += $reportFields{$item}{total}; +} +print BALANCE_SHEET "\n", sprintf($formatStrTotal, "TOTAL ASSETS", Commify($tot)), "\n\nLIABILITIES\n\n"; + +my $totLiabilities = $ZERO; +foreach my $item ('Accounts Payable', 'Accrued Expenses', + 'Liabilities, Credit Cards', 'Liabilities, Other', + 'Unearned Income, Conference Registration', 'Unearned Income, Other') { + next if $reportFields{$item}{total} == $ZERO; + print BALANCE_SHEET sprintf($formatStr, "$item:", Commify($reportFields{$item}{total})); + $totLiabilities += $reportFields{$item}{total}; +} +print BALANCE_SHEET "\n", sprintf($formatStr, "TOTAL LIABILTIES", Commify($totLiabilities)), + "\n\nNET ASSETS\n\n"; + +my $totNetAssets = $ZERO; +foreach my $item ('Unrestricted Net Assets', 'Temporarily Restricted Net Assets') { + next if $reportFields{$item}{total} == $ZERO; + print BALANCE_SHEET sprintf($formatStr, "$item:", Commify($reportFields{$item}{total})); + $totNetAssets += $reportFields{$item}{total}; +} +print BALANCE_SHEET "\n", sprintf($formatStr, "TOTAL NET ASSETS", Commify($totNetAssets)), "\n\n", + sprintf($formatStrTotal, "TOTAL LIABILITIES AND NET ASSETS", + Commify($totNetAssets + $totLiabilities)); + +close BALANCE_SHEET; +print STDERR "\n"; +die "unable to write to balance-sheet.csv: $!" unless ($? == 0); + +die "Cash+accounts receivable total does not equal net assets and liabilities total" + if (abs( ($reportFields{'Cash'}{total} + $reportFields{'Accounts Receivable'}{total} + + $reportFields{'Loans/Fraud Receivable'}{total})) - + abs($reportFields{'Accounts Payable'}{total} + + $reportFields{'Accrued Expenses'}{total} + + $reportFields{'Unearned Income, Conference Registration'}{total} + + $reportFields{'Unearned Income, Other'}{total} + + $reportFields{'Liabilities, Credit Cards'}{total} + + $reportFields{'Liabilities, Other'}{total} + + $reportFields{'Total Net Assets'}{total}) > $TWO_CENTS); + +die "Total net assets doesn't equal sum of restricted and unrestricted ones!" + if (abs($reportFields{'Total Net Assets'}{total}) - + abs($reportFields{'Unrestricted Net Assets'}{total} + + $reportFields{'Temporarily Restricted Net Assets'}{total}) > $TWO_CENTS); + + +my %incomeGroups = ('INTEREST INCOME' => { args => ['/^Income.*Interest/' ] }, + 'DONATIONS' => { args => [ '/^Income.*Donation/' ] }, + 'BOOK ROYALTIES & AFFILIATE PROGRAMS' => + { args => [ '/^Income.*(Royalt|Affilate)/' ] }, + 'CONFERENCES, REGISTRATION' => {args => [ '/^Income.*Reg/' ] }, + 'CONFERENCES, RELATED BUSINESS INCOME' => { args => [ '/^Income.*(Conferences?:.*Sponsor|Booth|RBI)/'] }, + 'LICENSE COMPLIANCE' => { args => [ '/^Income.*(Enforce|Compliance)/' ]}, + 'TRADEMARKS' => {args => [ '/^Income.*Trademark/' ]}, + 'ADVERSITING' => {args => [ '/^Income.*Advertising/' ]}); + +my @otherArgs; +foreach my $type (keys %incomeGroups) { + @otherArgs = ("/^Income/") if @otherArgs == 0; + push(@otherArgs, 'and', 'not', @{$incomeGroups{$type}{args}}); +} +$incomeGroups{"OTHER"}{args} = \@otherArgs; +$incomeGroups{"TOTAL"}{args} = ['/^Income/']; + +open(INCOME, ">", "income.csv") or die "unable to open income.csv for writing: $!"; + +foreach my $type (keys %incomeGroups) { + my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $startDate, '-e', $endDate, + '-F', '%-.80A %22.108t\n', '-s', + 'reg', @{$incomeGroups{$type}{args}}); + + open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + + print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + + $incomeGroups{$type}{total} = $ZERO; + $incomeGroups{$type}{output} = ""; + + foreach my $line (<FILE>) { + die "Unable to parse output line from second funds command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\<Adjustment\>/ and (abs($amount) <= $TWO_CENTS); + die "Weird account found, $account with amount of $amount in income command\n" + unless $account =~ /^\s*Income:/; + + $incomeGroups{$type}{total} += $amount; + $incomeGroups{$type}{output} .= "\"$account\",\"\$$amount\"\n"; + } +} +print INCOME "\"INCOME\",", + "\"STARTING:\",\"$formattedStartDate\",\"ENDING:\",\"$formattedEndDate\"\n\n"; + + +my $overallTotal = $ZERO; + +$formatStrTotal = "\"%-90s\",\"\$%14s\"\n"; +foreach my $type ('DONATIONS', 'LICENSE COMPLIANCE', + 'CONFERENCES, REGISTRATION', 'CONFERENCES, RELATED BUSINESS INCOME', + 'BOOK ROYALTIES & AFFILIATE PROGRAMS', 'ADVERSITING', + 'TRADEMARKS', 'INTEREST INCOME', 'OTHER') { + next if ($incomeGroups{$type}{output} =~ /^\s*$/ and $incomeGroups{$type}{total} == $ZERO); + print INCOME "\n\"$type\"\n", + $incomeGroups{$type}{output}, "\n", + sprintf($formatStrTotal, "TOTAL $type:", Commify($incomeGroups{$type}{total})); + $overallTotal += $incomeGroups{$type}{total}; +} +print INCOME "\n\n\n", sprintf($formatStrTotal, "OVERALL TOTAL:", Commify($overallTotal)); + +close INCOME; die "unable to write to income.csv: $!" unless ($? == 0); + +die "calculated total of $overallTotal does equal $incomeGroups{TOTAL}{total}" + if (abs($overallTotal) - abs($incomeGroups{TOTAL}{total}) > $TWO_CENTS); + +print STDERR "\n"; + +my %expenseGroups = ('BANKING FEES' => { regex => '^Expenses.*(Banking Fees|Currency Conversion)' }, + 'COMPUTING, HOSTING AND EQUIPMENT' => { regex => '^Expenses.*(Hosting|Computer Equipment)' }, + 'CONFERENCES' => { regex => '^Expenses.*(Conferences|Sprint)' }, + 'DEVELOPER MENTORING' => {regex => '^Expenses.*Mentor' }, + 'LICENSE COMPLIANCE' => { regex => '^Expenses.*(Enforce|Compliance)' }, + 'ACCOUNTING' => { regex => '^Expenses.*(Accounting|Annual Audit)' }, + 'PAYROLL' => { regex => '^Expenses.*Payroll' }, + 'OFFICE' => { regex => '^Expenses.*(Office|Phones)' }, + 'RENT' => { regex => '^Expenses.*Rent' }, + 'SOFTWARE DEVELOPMENT' => { regex => '^Expenses.*Development' }, + 'OTHER PROGRAM ACTIVITY' => {regex => '^Expenses.*Gould' }, + 'ADVOCACY AND PROMOTION' => {regex => '^Expenses.*(Slipstream|Advocacy Merchandise|Promotional)' }, + 'ADVERSITING' => {regex => '^Expenses.*Advertising' }); + +foreach my $type (keys %expenseGroups, 'TRAVEL') { + $expenseGroups{$type}{total} = $ZERO; + $expenseGroups{$type}{output} = ""; +} + +open(EXPENSE, ">", "expense.csv") or die "unable to open expense.csv for writing: $!"; + +my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $startDate, '-e', $endDate, + '-F', '%-.80A %22.108t\n', '-s', + 'reg', '/^Expenses/'); + +open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + +print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + +my $firstTotal = $ZERO; +foreach my $line (<FILE>) { + die "Unable to parse output line from second funds command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\<Adjustment\>/ and (abs($amount) <= $TWO_CENTS); + die "Weird account found, $account, with amount of $amount in expenses command\n" + unless $account =~ /^\s*Expenses:/; + + my $outputLine = "\"$account\",\"\$$amount\"\n"; + my $taken = 0; + # Note: Prioritize to put things under conference expenses if they were for a conference. + foreach my $type ('CONFERENCES', keys %expenseGroups) { + last if $taken; + next if $type eq 'TRAVEL' or $type eq 'OTHER'; + next unless $line =~ /$expenseGroups{$type}{regex}/; + $taken = 1; + $expenseGroups{$type}{total} += $amount; + $expenseGroups{$type}{output} .= $outputLine; + } + if (not $taken) { + if ($account =~ /Travel/) { + $expenseGroups{'TRAVEL'}{total} += $amount; + $expenseGroups{'TRAVEL'}{output} .= $outputLine; + } else { + $expenseGroups{'OTHER'}{total} += $amount; + $expenseGroups{'OTHER'}{output} .= $outputLine; + } + } + $firstTotal += $amount; +} +print EXPENSE "\"EXPENSES\",", + "\"STARTING:\",\"$formattedStartDate\",\"ENDING:\",\"$formattedEndDate\"\n\n"; +$overallTotal = $ZERO; +$formatStrTotal = "\"%-90s\",\"\$%14s\"\n"; + +my %verifyAllGroups; +foreach my $key (keys %expenseGroups) { + $verifyAllGroups{$key} = 1; +} +foreach my $type ('PAYROLL', 'SOFTWARE DEVELOPMENT', 'LICENSE COMPLIANCE', 'CONFERENCES', + 'DEVELOPER MENTORING', 'TRAVEL', 'BANKING FEES', 'ADVOCACY AND PROMOTION', + 'COMPUTING, HOSTING AND EQUIPMENT', 'ACCOUNTING', + 'OFFICE', 'RENT', 'ADVERSITING', 'OTHER PROGRAM ACTIVITY', 'OTHER') { + delete $verifyAllGroups{$type}; + + die "$type is not defined!" if not defined $expenseGroups{$type}; + next if ($expenseGroups{$type}{output} =~ /^\s*$/ and $expenseGroups{$type}{total} == $ZERO); + + print EXPENSE "\n\"$type\"\n", + $expenseGroups{$type}{output}, "\n", + sprintf($formatStrTotal, "TOTAL $type:", Commify($expenseGroups{$type}{total})); + $overallTotal += $expenseGroups{$type}{total}; +} + +print EXPENSE "\n\n\n", sprintf($formatStrTotal, "OVERALL TOTAL:", Commify($overallTotal)); + +close EXPENSE; die "unable to write to expense.csv: $!" unless ($? == 0); + +die "GROUPS NOT INCLUDED : ", join(keys(%verifyAllGroups), ", "), "\n" + unless (keys %verifyAllGroups == 0); + +die "calculated total of $overallTotal does *not* equal $firstTotal" + if (abs($overallTotal) - abs($firstTotal) > $TWO_CENTS); + +print STDERR "\n"; + +open(TRIAL, ">", "trial-balance.csv") or die "unable to open accrued.txt for writing: $!"; + +print TRIAL "\"TRIAL BALANCE REPORT\",\"ENDING: $formattedEndDate\"\n\n", + "\"ACCOUNT\",\"BALANCE AT $formattedStartDate\",\"CHANGE DURING PERIOD\",\"BALANCE AT $formattedEndDate\"\n\n"; + +my %commands = ( + 'totalEndFY' => [ $LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-e', $endDate, '-F', '%-.80A %22.108t\n', '-s', + 'reg' ], + 'amountInYear' => [ $LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $startDate, '-e', $endDate, '-F', '%-.80A %22.108t\n', + '-s', 'reg' ], + 'totalBeginFY' => [ $LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-e', $startDate, '-F', '%-.80A %22.108t\n', + '-s', 'reg' ]); + +my %trialBalanceData; +my %fullAccountList; + +foreach my $id (keys %commands) { + my(@command) = @{$commands{$id}}; + + open(FILE, "-|", @command) + or die "unable to run command ledger command: @command: $!"; + + print STDERR ($VERBOSE ? "Running: @command\n" : "."); + + foreach my $line (<FILE>) { + die "Unable to parse output line from trial balance $id command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\<Adjustment\>/ and (abs($amount) <= $TWO_CENTS); + next if $account =~ /^Equity:/; # Stupid auto-account made by ledger. + $trialBalanceData{$id}{$account} = $amount; + $fullAccountList{$account} = $id; + } + close FILE; + die "unable to run trial balance ledger command, @command: $!" unless ($? == 0); +} + +my $curOn = 'Assets'; + +foreach my $account (sort preferredAccountSorting keys %fullAccountList) { + # Blank lines right + if ($account !~ /^$curOn/) { + print TRIAL "pagebreak\n"; + $curOn = $account; + if ($curOn =~ /(Accrued:[^:]+):.*$/) { + $curOn = $1; + } else { + $curOn =~ s/^([^:]+):.*$/$1/; + } + } + if ($account =~ /^Assets|Liabilities|Accrued|Unearned Income/) { + foreach my $id (qw/totalBeginFY totalEndFY amountInYear/) { + $trialBalanceData{$id}{$account} = $ZERO + unless defined $trialBalanceData{$id}{$account}; + } + print TRIAL "\"$account\",\"\$$trialBalanceData{totalBeginFY}{$account}\",", + "\"\$$trialBalanceData{amountInYear}{$account}\",\"\$$trialBalanceData{totalEndFY}{$account}\"\n" + unless $trialBalanceData{totalBeginFY}{$account} == $ZERO and + $trialBalanceData{amountInYear}{$account} == $ZERO and + $trialBalanceData{totalEndFY}{$account} == $ZERO; + } else { + print TRIAL "\"$account\",\"\",\"\$$trialBalanceData{amountInYear}{$account}\",\"\"\n" + if defined $trialBalanceData{amountInYear}{$account} and + $trialBalanceData{amountInYear}{$account} != $ZERO; + } +} +close TRIAL; +die "unable to write trial-balance.csv: $!" unless ($? == 0); + +############################################################################### +# +# Local variables: +# compile-command: "perl -c summary-reports.plx" +# End: diff --git a/contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf b/contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf new file mode 100644 index 00000000..27b40353 Binary files /dev/null and b/contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf differ diff --git a/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf new file mode 100644 index 00000000..e2e06c98 Binary files /dev/null and b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf differ diff --git a/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt new file mode 100644 index 00000000..4d5fc907 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt @@ -0,0 +1,5 @@ +Invoice + +Date: May 10, 2011 + +Donation to the General Fund: $50.00 diff --git a/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf new file mode 100644 index 00000000..b6937670 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf @@ -0,0 +1,106 @@ +%PDF-1.4 +%쏢 +5 0 obj +<</Length 6 0 R/Filter /FlateDecode>> +stream +xn0E~1q#]4 @;0x؆*tQ!d_Bp:;=F&='(X}]D\+cQٲ K^jyYHTTMq<q#v纹 'nn~]c$33Ah{LC %Ԃ q{þVI!JAA*kʼn1]3{f&0"Lbwɿ"XP~v9BVxu`@Yz=t1o+Wʻk/S-/RWQB"5}m?^f|}8?M*2 o 8?Qsv_RGSdrendstream +endobj +6 0 obj +369 +endobj +4 0 obj +<</Type/Page/MediaBox [0 0 612 792] +/Rotate 0/Parent 3 0 R +/Resources<</ProcSet[/PDF /Text] +/ExtGState 11 0 R +/Font 12 0 R +>> +/Contents 5 0 R +>> +endobj +3 0 obj +<< /Type /Pages /Kids [ +4 0 R +] /Count 1 +>> +endobj +1 0 obj +<</Type /Catalog /Pages 3 0 R +/Metadata 13 0 R +>> +endobj +7 0 obj +<</Type/ExtGState +/OPM 1>>endobj +11 0 obj +<</R7 +7 0 R>> +endobj +12 0 obj +<</R9 +9 0 R/R8 +8 0 R/R10 +10 0 R>> +endobj +9 0 obj +<</BaseFont/Helvetica/Type/Font +/Subtype/Type1>> +endobj +8 0 obj +<</BaseFont/Courier/Type/Font +/Subtype/Type1>> +endobj +10 0 obj +<</BaseFont/Helvetica-Bold/Type/Font +/Subtype/Type1>> +endobj +13 0 obj +<</Type/Metadata +/Subtype/XML/Length 1393>>stream +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<?adobe-xap-filters esc="CRLF"?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='XMP toolkit 2.9.1-13, framework 1.6'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:iX='http://ns.adobe.com/iX/1.0/'> +<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:pdf='http://ns.adobe.com/pdf/1.3/' pdf:Producer='GPL Ghostscript 8.71'/> +<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:xmp='http://ns.adobe.com/xap/1.0/'><xmp:ModifyDate>2012-09-07T15:02:10-04:00</xmp:ModifyDate> +<xmp:CreateDate>2012-09-07T15:02:10-04:00</xmp:CreateDate> +<xmp:CreatorTool>a2ps version 4.14</xmp:CreatorTool></rdf:Description> +<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:xapMM='http://ns.adobe.com/xap/1.0/mm/' xapMM:DocumentID='1335afed-313b-11ed-0000-eb02a9a83ec4'/> +<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:dc='http://purl.org/dc/elements/1.1/' dc:format='application/pdf'><dc:title><rdf:Alt><rdf:li xml:lang='x-default'>receipt</rdf:li></rdf:Alt></dc:title><dc:creator><rdf:Seq><rdf:li>Bradley M. Kuhn</rdf:li></rdf:Seq></dc:creator></rdf:Description> +</rdf:RDF> +</x:xmpmeta> + + +<?xpacket end='w'?> +endstream +endobj +2 0 obj +<</Producer(GPL Ghostscript 8.71) +/CreationDate(D:20120907150210-04'00') +/ModDate(D:20120907150210-04'00') +/Title(receipt) +/Author(Bradley M. Kuhn) +/Creator(a2ps version 4.14)>>endobj +xref +0 14 +0000000000 65535 f +0000000692 00000 n +0000002544 00000 n +0000000633 00000 n +0000000473 00000 n +0000000015 00000 n +0000000454 00000 n +0000000757 00000 n +0000000942 00000 n +0000000878 00000 n +0000001004 00000 n +0000000798 00000 n +0000000828 00000 n +0000001074 00000 n +trailer +<< /Size 14 /Root 1 0 R /Info 2 0 R +/ID [<346C5213A8B2262C0696706A70350365><346C5213A8B2262C0696706A70350365>] +>> +startxref +2736 +%%EOF diff --git a/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf new file mode 100644 index 00000000..7241909a Binary files /dev/null and b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf differ diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf new file mode 100644 index 00000000..8441f3e6 Binary files /dev/null and b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf differ diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt new file mode 100644 index 00000000..e2722c45 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt @@ -0,0 +1,6 @@ +Baz Hosting Services, LLC + +Date: April 20, 2011 + +Charge: $250.00 + diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf b/contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf new file mode 100644 index 00000000..11f6286c Binary files /dev/null and b/contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf differ diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt b/contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt new file mode 100644 index 00000000..c5ac98ac --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt @@ -0,0 +1 @@ +I, Another J. Donor, would like $400 to be earmarked for Foo! diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger b/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger new file mode 100644 index 00000000..fb6134ff --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger @@ -0,0 +1,28 @@ + +2010/01/01 Kindly T. Donor + Income:Foo:Donation $-100.00 + ;Invoice: Projects/Foo/Invoices/Invoice20100101.pdf + Assets:Checking $100.00 + + +2011/03/15 Another J. Donor + Income:Foo:Donation $-400.00 + ;Approval: Projects/Foo/earmark-record.txt + Assets:Checking $400.00 + +2011/04/20 (1) Baz Hosting Services, LLC + Expenses:Foo:Hosting $250.00 + ;Receipt: Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf + Assets:Checking $-250.00 + +2011/05/10 Donation to General Fund + Income:Donation $-50.00 + ;Invoice: Financial/Invoices/Invoice20110510.pdf + Assets:Checking $50.00 + +2011/04/20 (2) Baz Hosting Services, LLC + Expenses:Blah:Hosting $250.00 + ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf + ;Invoice: Projects/Blah/Expenses/hosting/april-invoice.pdf + Assets:Checking $-250.00 + ;Statement: Financial/BankStuff/bank-statement.pdf diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST b/contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST new file mode 100644 index 00000000..b8bfc107 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST @@ -0,0 +1,10 @@ +chart-of-accounts.csv +general-ledger.txt +general-ledger.csv +Financial/BankStuff/bank-statement.pdf +Financial/Invoices/Invoice20110510.pdf +Projects/Foo/Invoices/Invoice20100101.pdf +Projects/Foo/earmark-record.txt +Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf +Projects/Blah/Expenses/hosting/april-invoice.pdf +Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv new file mode 100644 index 00000000..445bc412 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv @@ -0,0 +1,6 @@ +"CHART OF ACCOUNTS","BEGINNING:","2012/03/01","ENDING:","2012/02/29" +"Assets:Checking" +"Income:Donation" +"Income:Foo:Donation" +"Expenses:Blah:Hosting" +"Expenses:Foo:Hosting" diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods b/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods new file mode 100644 index 00000000..8eae706f Binary files /dev/null and b/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods differ diff --git a/contrib/non-profit-audit-reports/unpaid-accruals-report.plx b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx new file mode 100755 index 00000000..f481e02f --- /dev/null +++ b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx @@ -0,0 +1,110 @@ +#!/usr/bin/perl +# unpaid-acccurals-report.plx -*- Perl -*- + +# This report is designed to create what our accounts call a "Schedule of +# accounts payable". and "Schedule of accounts receivable". + + + +# Copyright (C) 2013 Bradley M. Kuhn +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; + +my $LEDGER_CMD = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 70; + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); +my $TWO_CENTS = Math::BigFloat->new("0.02"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <START_DATE> <END_DATE> <LEDGER_OPTIONS>\n"; + exit 1; +} +my($startDate, $endDate, @mainLedgerOptions) = @ARGV; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%Y/%m/%d"); +die "Date calculation error on $endDate" if ($err); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%Y/%m/%d"); +die "Date calculation error on $startDate" if ($err); + +my(@ledgerOptions) = (@mainLedgerOptions, + '-V', '-X', '$', '-e', $endDate, '-F', + '\"%(tag("Invoice"))\",\"%A\",\"%(date)\",\"%(payee)\",\"%22.108t\"\n', + '--limit', 'tag("Invoice") !~ /^\s*$/', 'reg'); + +my @possibleTypes = ('Accrued:Loans Receivable', 'Accrued:Accounts Payable', + 'Accrued:Accounts Receivable', 'Accrued:Expenses'); + +my %data; +foreach my $type (@possibleTypes) { + open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions, "/^$type/") + or die "Unable to run $LEDGER_CMD @ledgerOptions: $!"; + + while (my $line = <LEDGER_FUNDS>) { + next if $line =~ /"\<Adjustment\>"/; + die "Unable to parse output line $line from @ledgerOptions" + + unless $line =~ /^\s*"([^"]+)","([^"]+)","([^"]+)","([^"]+)","\s*\$\s*([\-\d\.\,]+)"\s*$/; + my($invoice, $account, $date, $payee, $amount) = ($1, $2, $3, $4, $5); + $amount = ParseNumber($amount); + + push(@{$data{$type}{$invoice}{entries}}, { account => $account, date => $date, payee => $payee, amount => $amount}); + $data{$type}{$invoice}{total} = $ZERO unless defined $data{$type}{$invoice}{total}; + $data{$type}{$invoice}{total} += $amount; + } + close LEDGER_FUNDS; + die "Failure on ledger command for $type: $!" unless ($? == 0); + +} +foreach my $type (keys %data) { + foreach my $invoice (keys %{$data{$type}}) { + delete $data{$type}{$invoice} if abs($data{$type}{$invoice}{total}) <= $TWO_CENTS; + } +} +foreach my $type (keys %data) { + delete $data{$type} if scalar(keys %{$data{$type}}) == 0; +} +foreach my $type (keys %data) { + print "\"SCHEDULE OF $type\"\n\"ENDING:\",\"$formattedEndDate\"\n\n", + '"DATE","PAYEE","ACCOUNT","AMOUNT","INVOICE"', "\n"; + foreach my $invoice (keys %{$data{$type}}) { + my $vals; + foreach my $vals (@{$data{$type}{$invoice}{entries}}) { + print "\"$vals->{date}\",\"$vals->{payee}\",\"$vals->{account}\",\"\$$vals->{amount}\",\"link:$invoice\"\n"; + } + } + print "pagebreak\n"; +} +############################################################################### +# +# Local variables: +# compile-command: "perl -c unpaid-accruals-report.plx" +# End: + diff --git a/contrib/raw/GenerateLatexExpeneseReport.pl b/contrib/raw/GenerateLatexExpeneseReport.pl new file mode 100755 index 00000000..670d9f21 --- /dev/null +++ b/contrib/raw/GenerateLatexExpeneseReport.pl @@ -0,0 +1,429 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use Getopt::Long; # Options processing +use Smart::Comments -ENV, "###"; # Ignore my dividers, and use + # Smart_Comments=1 to activate +use Cwd; +use File::Basename; +use 5.10.0; +use POSIX qw(strftime); +use Date::Calc qw(Add_Delta_Days); + +use Template; +my $TT = Template->new( { POST_CHOMP => 1 } ); + +###################################################################### +# TODO +# +# DONE Meal summaries are broken for multi-week reports + +###################################################################### +# Options + +# Is this an internal report? +my $ExpenseReportCode = undef; +my $Internal = undef; +my $SuppressMeals = 0; +my $ViewAfter = 0; +my $ImageDir = "."; +my $Anonymize = 0; +my $Help = undef; + +GetOptions( 'c' => \$Internal, + 'm' => \$SuppressMeals, + 'v' => \$ViewAfter, + 'a' => \$Anonymize, + 'I' => \$ImageDir, + 'e:s' => \$ExpenseReportCode, + 'h|help' => \$Help + ); + +# Help + +defined $Help && do { + print <<EOF; +Usage: GenerateLatexExpenseReport.pl [OPTION] -e ERCode + +Options: + -c Internal report + -m Suppress meals + -v View PDF on completion + -a Anonymous, omit header/footer + -I Image directory + -e ER Code (AISER0001) + +EOF + exit -1; +}; + +die "Pass -e <ExpenseReportCode>" unless defined $ExpenseReportCode; + +###################################################################### +# Report items + +my @ItemizedExpenses; +my $ItemizedTotal = 0.00; + +my @ItemizedReceipts; + +my @MealsReport; + +###################################################################### +# Gather required data about this expense report from the directory name +# ie: ./AISER0015 20090419-20090425 AGIL2078 Shands HACMP/ +# +# ExpenseReportCode = AISER0015 +# DateRange = 20090419-20090425 +# ProjectCode = AGIL2078 +# Description = Shands HACMP + + +###################################################################### +# Remaining options + +# Where is the ledger binary? +my $LedgerBin = "./ledger -f ./.ledger -V"; + +# -E Show empty accounts +# -S d Sort by date +# -V Convert mileage to $ +my $LedgerOpts = "--no-color -S d"; + +my $LedgerAcct = "^Dest:Projects"; + +my $LedgerCriteria = "%" . "ER=$ExpenseReportCode"; + +# Internal report? + +if ( $Internal ) { + + # No mileage on an internal report + # $LedgerCriteria .= "&!/Mileage/"; # This shouldn't matter, just don't put metadata for ER on mileage + $LedgerAcct = "^Dest:Internal"; + +} + +my $CmdLine = "$LedgerBin reg $LedgerOpts -E \"$LedgerCriteria\" and ^Stub " + . "--format \"%(tag('ER'))~%(tag('PROJECT'))~%(tag('NOTE'))\n\""; +### $CmdLine + +my @TempLine = `$CmdLine`; + +# Match all remaining items +$TempLine[0] =~ m,^(?<Er>.*?)~ + (?<Project>.*?)~ + (?<Note>.*?)\s*$,x; + +my $ProjectCode= $+{'Project'}; +my $Description= $+{'Note'}; + +### $ExpenseReportCode +### $ProjectCode +### $Description +### $LedgerAcct +### $Internal +### $Anonymize +### $LedgerAcct +### $LedgerOpts +### $LedgerCriteria + + +###################################################################### +# Pull main ledger report of line items for the expense report +# Using ~ as a delimiter +# +# Example: +# '2009/04/25~AR:Projects:AGIL2078:PersMealsLunch~:AISER0015: PILOT 00004259 MIDWAY, FL~ 8.68~Receipts/AGIL2078/20090425_Pilot_8_68_21204.jpg\n' +# +#./ledger --no-color reg %ER=AISER0040 and ^Projects -y "%Y/%m/%d" -V --format "%(date)~%(account)~%(payee)~%(amount)~%(tag('NOTE'))\n" + +$CmdLine = "$LedgerBin reg $LedgerOpts \"$LedgerCriteria\" and \"$LedgerAcct\" " + . "-y \"%Y/%m/%d\" " + . "--format \"%(date)~%(tag('CATEGORY'))~%(payee)~%(display_amount)~%(tag('NOTE'))~%(tag('RECEIPT'))\\n\""; +### $CmdLine +my @MainReport = `$CmdLine`; + + +### MainReport: @MainReport + +# Remove any project codes and linefeeds +#map { chomp(); s/(:AISER[0-9][0-9][0-9][0-9])+://g; } @MainReport; # No need, thats now metadata + +foreach my $line (@MainReport) { ### Processing Main Report... done + + # Remove bad chars (#&) + $line =~ tr/#&//d; + + # Match all remaining items + $line =~ m,^(?<Date>[0-9]{4}/[0-9]{2}/[0-9]{2})~ + (?<Category>.*?)~ + (?<Vendor>.*?)~ + (?<Amount>.*?)~ + (?<Note>.*?)~ + (?<Receipts>.*?)\s*$,x; + my %Record = %+; + + $Record{'Amount'}=~tr/$,//d; + + foreach (keys %Record) { + $Record{$_} =~ s/^\s+//g; + } + + + # Grab images from <<file:///dir/filename.jpg>> + my $ImageList = $Record{'Receipts'}; + $ImageList //= ''; + my @Images = split( /,/, $ImageList ); + + # Cleanup + # Take last word of account name as category + $Record{'Category'} = ( split( /:/, $Record{'Category'} ) )[-1]; + + # If no images, italicise the line item. + $Record{'Italics'} = 1; + + # Test images + foreach my $Image (@Images) { + + # Turn off italics because there is an image + $Record{'Italics'} = 0; + + if (! -r $ImageDir . "/" . $Image) { + print STDERR "Missing $ImageDir/$Image\n"; + } + } + + # Add to itemized expenses to be printed + push( @ItemizedExpenses, \%Record ); + $ItemizedTotal += $Record{'Amount'}; + + # Add to itemized reciepts for printing + push( @ItemizedReceipts, { 'Vendor' => $Record{'Vendor'}, + 'Amount' => $Record{'Amount'}, + 'Date' => $Record{'Date'}, + 'Images' => \@Images } ) + if $ImageList; + +} + +### @ItemizedExpenses + +###################################################################### +# Meals report + +# Summarize total spent on meals by day +$CmdLine = "$LedgerBin reg $LedgerOpts " + . "\"$LedgerCriteria\" and \"$LedgerAcct\" and \%CATEGORY=Meals " + . "-D -n " + . "--format \"%(account)~%(payee)~%(display_amount)~%(total)\n\" " + . "| grep -v '<None>'"; + +### $CmdLine +my @MealsOutput = `$CmdLine`; + +### @MealsOutput + +foreach my $line (@MealsOutput) { + + # Match all remaining items + $line =~ m,^(?<Account>.*?)~ + (?<DOW>.*?)~\$ + (?<Amount>\s*[0-9]+\.[0-9]+)~\$ + (?<RunningTotal>.*?)\s*$,x; + my %TRecord=%+; + $TRecord{'Account'}=~s/^Projects://g; + $TRecord{'DOW'}=~s/^- //g; + + # Add to itemized expenses to be printed + push( @MealsReport, \%TRecord ); + +} + +###################################################################### +# Total by category + +$CmdLine = "$LedgerBin bal $LedgerOpts " + . "\"$LedgerCriteria\" and \"$LedgerAcct\" '--account=tag(\"CATEGORY\")' " + . "--format \"%(account)~%(display_total)\\n\""; + +### $CmdLine +my @CategoryOutput = `$CmdLine`; + +### @CategoryOutput + +my @CategoryReport; + +foreach my $line (@CategoryOutput) { + + chomp $line; + $line =~ tr/\$,//d; + + # Match all remaining items + my @Temp = split(/~/,$line); + + my %TRecord= ( 'Category' => $Temp[0], + 'Amount' => $Temp[1]); + + if ($TRecord{'Category'} eq '') { + $TRecord{'Category'} = '\\hline \\bf Total'; + } + + # Cleanup + # Take last word of account name as category + $TRecord{'Category'} = ( split( /:/, $TRecord{'Category'} ) )[0]; + + # Add to itemized expenses to be printed + push( @CategoryReport, \%TRecord ); + +} + +### @CategoryReport + + +###################################################################### +# Output +###################################################################### + +my $TTVars = { + 'Internal' => $Internal, + 'SuppressMeals' => $SuppressMeals, + 'ExpenseReportCode' => $ExpenseReportCode, + 'ProjectCode' => $ProjectCode, + 'Description' => $Description, + 'ItemizedExpenses' => \@ItemizedExpenses, + 'ItemizedTotal' => $ItemizedTotal, + 'MealsReport' => \@MealsReport, + 'CategoryReport' => \@CategoryReport, + 'ItemizedReceipts' => \@ItemizedReceipts, + 'ImageDir' => $ImageDir, + 'Anonymize' => $Anonymize +}; + +#### $TTVars + +my $LatexTemplate = <<EOF; +[% USE format %][% ToDollars = format('\\\$%.2f') %] +%%%%%%%%%%% Header + +\\documentclass[10pt,letterpaper]{article} +\\usepackage[letterpaper,includeheadfoot,top=0.5in,bottom=0.5in,left=0.75in,right=0.75in]{geometry} +\\usepackage[utf8]{inputenc} +\\usepackage[T1]{fontenc} +\\usepackage[scaled]{helvet} +\\renewcommand*\\familydefault{\\sfdefault} +\\usepackage{lastpage} +\\usepackage{fancyhdr} +\\usepackage{graphicx} +\\usepackage{multicol} +\\usepackage[colorlinks,linkcolor=blue]{hyperref} +\\pagestyle{fancy} +\\renewcommand{\\headrulewidth}{1pt} +\\renewcommand{\\footrulewidth}{0.5pt} +\\geometry{headheight=48pt} + + +\\begin{document} + +%%%%%%%%%% Itemized table + +\\section{Itemized Expenses} +[% FOREACH Expense IN ItemizedExpenses %] +[% IF loop.first() or ( ( loop.count mod 20 ) == 0 ) %] +\\begin{tabular}{|l|l|p{2in}|r|p{2in}|} +\\hline +\\hline +\\bf Date & \\bf Category & \\bf Expense & \\bf Amount & \\bf Notes \\\\ +\\hline \\hline +[% END %] +[% IF Expense.Italics %]\\it[% END %] [% Expense.Date %] & [% IF Expense.Italics %]\\it[% END %] [% Expense.Category %] & [% IF Expense.Italics %]\\it[% END %] [% Expense.Vendor %] & [% IF Expense.Italics %]\\it[% END %] [% ToDollars(Expense.Amount) %] & [% IF Expense.Italics %]\\it[% END %] [% Expense.Note %] \\\\ \\hline +[% IF loop.last() %] +\\hline + & & \\bf Total & \\bf [% ToDollars(ItemizedTotal) %] & \\\\ +[% END %] +[% IF ( ( (loop.count + 1) mod 20 ) == 0 ) or loop.last() %] +\\hline +\\hline +\\end{tabular} +[% IF ( ( (loop.count + 1) mod 20 ) == 0 ) %]\\newline {\\it Continued on next page...}[% END %] +\\newpage +[% END %] +[% END %] + +[% IF ! Internal %][% IF ! SuppressMeals %] +%%%%%%%%%% Meals summary table + +\\section{Meals Summary By Day} + +\\begin{tabular}{|l|l|p{2in}|p{2in}|} +\\hline +\\hline +\\bf DOW & \\bf Daily Total & \\bf Running Total \\\\ +\\hline \\hline +[% FOREACH Meal IN MealsReport %] +[% Meal.DOW %] & [% ToDollars(Meal.Amount) %] & [% ToDollars(Meal.RunningTotal) %] \\\\ \\hline +[% END %] +\\hline +\\hline +\\end{tabular} +[% END %][% END %] + +%%%%%%%%%% Category summary + +\\section{Expenses Summary} + +\\begin{tabular}{|l|l|} +\\hline +\\hline +\\bf Category & \\bf Total \\\\ +\\hline \\hline +[% FOREACH Category IN CategoryReport %] +[% Category.Category %] & [% ToDollars(Category.Amount) %] \\\\ \\hline +[% END %] +\\hline +\\hline +\\end{tabular} + + +%%%%%%%%%% Begin receipts + +\\section{Scanned Receipts} + +[% FOREACH Receipt IN ItemizedReceipts %] +\\subsection{[% Receipt.Date %] [% Receipt.Vendor %]: [% ToDollars(Receipt.Amount) %]} +[% FOREACH Image IN Receipt.Images %] +\\includegraphics[angle=90,width=\\textwidth,keepaspectratio]{[% ImageDir %]/[% Image %]} \\\\ +[% END %] + +[% END %] + + +%%%%%%%%%% Footer + +\\end{document} +EOF + +my $LatexFN = $ExpenseReportCode . "-" . $ProjectCode . ".tex"; +### $LatexFN + +$TT->process( \$LatexTemplate, $TTVars, "./tmp/" . $LatexFN ) || do { + my $error = $TT->error(); + print "error type: ", $error->type(), "\n"; + print "error info: ", $error->info(), "\n"; + die $error; +}; + + +my $LatexOutput = `pdflatex -interaction batchmode -output-directory ./tmp "$LatexFN"`; +### $LatexOutput + + $LatexOutput = `pdflatex -interaction batchmode -output-directory ./tmp "$LatexFN"`; +### $LatexOutput + +if ($ViewAfter) { + my $ViewFN = $LatexFN; + $ViewFN =~ s/\.tex$/.pdf/; + `acroread "./tmp/$ViewFN"`; +} + diff --git a/contrib/raw/MetadataExample.dat b/contrib/raw/MetadataExample.dat new file mode 100644 index 00000000..791eaf77 --- /dev/null +++ b/contrib/raw/MetadataExample.dat @@ -0,0 +1,111 @@ +; TAG key: value + +2009/09/27 * (09/28/2009) HUDSON NEWS HOUSTN HBB HOUSTON, TX + Source:Visa -$6.55 + Projects:Meals + ; ER: AISER0033 + ; PROJECT: PROJXXXX + +2009/09/27 * (09/28/2009) PEET'S COFFEE & TEA KINGWOOD, TX + Source:Visa -$2.44 + Projects:Meals + ; ER: AISER0033 + ; PROJECT: PROJXXXX + +2009/09/28 * (09/29/2009) FUSIA NEW YORK, NY + Source:Visa -$15.25 + Projects:Meals + ; ER: AISER0033 + ; PROJECT: PROJXXXX + + +2009/09/29 * (09/30/2009) BALUCHI'S NEW YORK, NY + Source:Visa + Projects:Meals $20.00 + ; ER: AISER0033 + ; PROJECT: PROJXXXX + Internal:Travel $10.44 + ; ER: AISER0036 + ; PROJECT: Internal + + +2009/10/01 * Reimbursing AISER0036 + Bank:AISChecking + ; ER: AISER0036 + ; PROJECT: Internal + Source:Visa $10.44 + + +---------- + +$ ./ledger -Ef AISER0033.dat bal '--account=tag("ER")' + $-44.24 + $44.24 AISER0033 + 0 AISER0036 +-------------------- + 0 + +$ ./ledger -Ef AISER0033.dat bal + $-10.44 Bank:AISChecking + $10.44 Internal:Travel + $44.24 Projects:Meals + $-44.24 Source:Visa +-------------------- + 0 + +$ ./ledger -Ef AISER0033.dat bal '--account=tag("PROJECT")' + $-44.24 + 0 Internal + $44.24 PROJXXXX +-------------------- + 0 + +$ ./ledger -f AISER0033.dat reg '--account=tag("PROJECT")' +09-Sep-27 HUDSON NEWS HOUSTN .. $-6.55 $-6.55 +09-Sep-27 HUDSON NEWS HOUSTN .. PROJXXXX $6.55 0 +09-Sep-27 PEET'S COFFEE & TEA.. $-2.44 $-2.44 +09-Sep-27 PEET'S COFFEE & TEA.. PROJXXXX $2.44 0 +09-Sep-28 FUSIA NEW YORK, NY $-15.25 $-15.25 +09-Sep-28 FUSIA NEW YORK, NY PROJXXXX $15.25 0 +09-Sep-29 BALUCHI'S NEW YORK,.. $-30.44 $-30.44 +09-Sep-29 BALUCHI'S NEW YORK,.. PROJXXXX $20.00 $-10.44 +09-Sep-29 BALUCHI'S NEW YORK,.. Internal $10.44 0 +09-Oct-01 Reimbursing AISER0036 Internal $-10.44 $-10.44 +09-Oct-01 Reimbursing AISER0036 $10.44 0 + +$ ./ledger -f AISER0033.dat reg '--account=tag("ER")' +09-Sep-27 HUDSON NEWS HOUSTN .. $-6.55 $-6.55 +09-Sep-27 HUDSON NEWS HOUSTN .. AISER0033 $6.55 0 +09-Sep-27 PEET'S COFFEE & TEA.. $-2.44 $-2.44 +09-Sep-27 PEET'S COFFEE & TEA.. AISER0033 $2.44 0 +09-Sep-28 FUSIA NEW YORK, NY $-15.25 $-15.25 +09-Sep-28 FUSIA NEW YORK, NY AISER0033 $15.25 0 +09-Sep-29 BALUCHI'S NEW YORK,.. $-30.44 $-30.44 +09-Sep-29 BALUCHI'S NEW YORK,.. AISER0033 $20.00 $-10.44 +09-Sep-29 BALUCHI'S NEW YORK,.. AISER0036 $10.44 0 +09-Oct-01 Reimbursing AISER0036 AISER0036 $-10.44 $-10.44 +09-Oct-01 Reimbursing AISER0036 $10.44 0 + + +$ ./ledger -f AISER0033.dat reg %ER=AISER0033 +09-Sep-27 HUDSON NEWS HOUSTN .. Projects:Meals $6.55 $6.55 +09-Sep-27 PEET'S COFFEE & TEA.. Projects:Meals $2.44 $8.99 +09-Sep-28 FUSIA NEW YORK, NY Projects:Meals $15.25 $24.24 +09-Sep-29 BALUCHI'S NEW YORK,.. Projects:Meals $20.00 $44.24 + + +$ ./ledger -f AISER0033.dat reg %ER=AISER0036 +09-Sep-29 BALUCHI'S NEW YORK,.. Internal:Travel $10.44 $10.44 +09-Oct-01 Reimbursing AISER0036 Bank:AISChecking $-10.44 0 + +$ ./ledger -f AISER0033.dat reg %PROJECT=PROJXXXX +09-Sep-27 HUDSON NEWS HOUSTN .. Projects:Meals $6.55 $6.55 +09-Sep-27 PEET'S COFFEE & TEA.. Projects:Meals $2.44 $8.99 +09-Sep-28 FUSIA NEW YORK, NY Projects:Meals $15.25 $24.24 +09-Sep-29 BALUCHI'S NEW YORK,.. Projects:Meals $20.00 $44.24 + +$ ./ledger -f AISER0033.dat --prepend-format='%(tag("IMG")) ' reg %ER=0033 +image1.jpg 09-Sep-27 HUDSON NEWS HOUSTN .. Projects:Meals $6.55 $6.55 +image2.jpg 09-Sep-27 PEET'S COFFEE & TEA.. Projects:Meals $2.44 $8.99 +image3.jpg 09-Sep-28 FUSIA NEW YORK, NY Projects:Meals $15.25 $24.24 +image4.jpg 09-Sep-29 BALUCHI'S NEW YORK,.. Projects:Meals $20.00 $44.24 diff --git a/contrib/raw/README b/contrib/raw/README new file mode 100644 index 00000000..82ae74e1 --- /dev/null +++ b/contrib/raw/README @@ -0,0 +1,5 @@ +These scripts are from my (rladams) local ledger customizations. + +They are intended as examples for features that can be made generic to benefit other Ledger users. + +As they become refined, the raw files will be removed and replaced by suitable sources in contrib. diff --git a/contrib/raw/VerifyImages.sh b/contrib/raw/VerifyImages.sh new file mode 100755 index 00000000..5975f7cf --- /dev/null +++ b/contrib/raw/VerifyImages.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +grep -h '; RECEIPT: ' \ + *.dat \ + */*.dat \ + | sed 's,\W*; RECEIPT: ,,g' \ + | tr , '\n' \ + | sort -u \ + | while read X +do + [ -f "$X" ] \ + && echo OK $X \ + || echo XX $X +done diff --git a/contrib/raw/dotemacs.el b/contrib/raw/dotemacs.el new file mode 100644 index 00000000..a4babed7 --- /dev/null +++ b/contrib/raw/dotemacs.el @@ -0,0 +1,201 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Ledger + +;; Maybe later add this to the expense repo once it settles +(add-to-list 'load-path "/home/adamsrl/.emacs.d/addons/ledger") + +(add-to-list 'load-path "/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/bin") +(autoload 'ledger-mode "ldg-new" nil t) +(add-to-list 'auto-mode-alist '("\\.dat$" . ledger-mode)) + +(add-hook 'ledger-mode-hook + (lambda () + (setq truncate-lines 1) + (url-handler-mode 1) ; Enable hyperlinks + (require 'ledger-matching) ; Requires ldg-report anyway + (load-file "/home/adamsrl/.emacs.d/addons/ledger/ldg-xact.el") + (let ((map (current-local-map))) + (define-key map (kbd "\C-c o") 'find-file-at-point) ; Open images + (define-key map (kbd "<f8>") 'ledger-expense-shortcut) + (define-key map (kbd "M-i") 'ledger-expense-internal) + (define-key map (kbd "M-o") 'ledger-expense-personal) + (define-key map (kbd "M-'") 'ledger-expense-split) + (define-key map (kbd "M-n") '(lambda () + (interactive) + (ledger-post-next-xact) + (recenter) + (when (get-buffer "*Receipt*") + (ledger-expense-show-receipt)))) + (define-key map (kbd "M-p") '(lambda () (interactive) + (ledger-post-prev-xact) + (recenter) + (when (get-buffer "*Receipt*") + (ledger-expense-show-receipt)))) + (local-unset-key [tab]) ; Ideally this turns off pcomplete + (local-unset-key [(control ?i)]) ; Ideally this turns off pcomplete + ) + + ;(defface ledger-report-face-account-ok '((t (:foreground "Cyan"))) "Derp") + ;(defface ledger-report-face-account-bad '((t (:foreground "Red"))) "Derp") + + (font-lock-add-keywords + 'ledger-mode + '(("Unassigned\\|Unknown\\|; RECEIPT:$" 0 'highlight prepend))) )) + +;; My customizations to make receipt image matching work with ledger-report mode +(add-hook 'ledger-report-mode-hook + (lambda () + (hl-line-mode 1) + (local-set-key (kbd "<RET>") 'ledger-report-visit-source) ; Make return jump to the right txn + (local-set-key (kbd "<tab>") 'ledger-report-visit-source) ; Make tab jump to the right txn + (local-set-key (kbd "n") '(lambda () + (interactive) + (save-selected-window + (next-line) + (ledger-report-visit-source)))) ; Update a txn window but keep focus + (local-set-key (kbd "p") '(lambda () + (interactive) + (save-selected-window + (previous-line) + (ledger-report-visit-source)))) ; Update a txn window but keep focus + + + (local-set-key (kbd "M-r") 'ledger-receipt-matching) ; Link receipt to current item + (local-set-key (kbd "M-l") 'ledger-matching-tie-receipt-to-txn) ; Link receipt to current item + (local-set-key (kbd "M-n") '(lambda () + (interactive) + (ledger-matching-image-offset-adjust 1))) ; Next receipt image + (local-set-key (kbd "M-p") '(lambda () + (interactive) + (ledger-matching-image-offset-adjust -1))) ; prev receipt image + (local-set-key (kbd "M-s") '(lambda () + (interactive) + (ledger-receipt-skip))) ; Skip receipt image + (local-set-key (kbd "C-c C-e") '(lambda () (interactive) + (save-selected-window + (ledger-report-visit-source) + (ledger-toggle-current-entry) ))) ; Toggle entry + )) + +(defvar *ledger-expense-shortcut-ER* + "Current expense report number, just last four digits (ie: 1234 results in AISER1234).") + +(defvar *ledger-expense-shortcut-split-ER* + "Split (ie: internal) expense report number, just last four digits (ie: 1234 results in AISER1234).") + +(defvar *ledger-expense-shortcut-Proj* "" + "Current export report project code (ie: AGIL1292)") + +(defun ledger-expense-shortcut-ER-format-specifier () *ledger-expense-shortcut-ER*) + +(defun ledger-expense-shortcut-setup (ER Split Proj) + "Sets the variables expanded into the transaction." + (interactive "MER Number (4 digit number only): \nMSplit ER Number (4 digit number only): \nMProject: ") + (setq *ledger-expense-shortcut-ER* + (concatenate 'string "AISER" ER)) + (setq *ledger-expense-shortcut-split-ER* + (concatenate 'string "AISER" Split)) + (setq *ledger-expense-shortcut-Proj* Proj) + (setq ledger-matching-project Proj) + (message "Set Proj to %s and ER to %s, split to %s" + *ledger-expense-shortcut-Proj* + *ledger-expense-shortcut-ER* + *ledger-expense-shortcut-split-ER*)) + +(defun ledger-expense-shortcut () + "Updates the ER and Project metadata with the current values of the shortcut variables." + (interactive) + (when (eq major-mode 'ledger-mode) + (if (or (eql *ledger-expense-shortcut-ER* "") + (eql *ledger-expense-shortcut-Proj* "")) + (message "Run ledger-expense-shortcut-setup first.") + (save-excursion + (search-forward "; ER:") + (kill-line nil) + (insert " " *ledger-expense-shortcut-ER*)) + (save-excursion + (search-forward "; PROJECT:") + (kill-line nil) + (insert " " *ledger-expense-shortcut-Proj*))))) + +(defun ledger-expense-split () + "Splits the current transaction between internal and projects." + (interactive) + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (re-search-forward "^ +Dest:Projects") + (move-beginning-of-line nil) + (let ((begin (point)) + (end (re-search-forward "^$"))) + (goto-char end) + (insert (buffer-substring begin end)) + (goto-char end) + (re-search-forward "^ Dest:Projects") + (replace-match " Dest:Internal") + (re-search-forward "; ER: +[A-Za-z0-9]+") + (replace-match (concat "; ER: " *ledger-expense-shortcut-split-ER*) t) + (when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t) + (replace-match "; CATEGORY: Travel" t)))) + (re-search-backward "^[0-9]\\{4\\}/") + (re-search-forward "^ +Dest:Projects") + (insert-string " $") )) + +(defun ledger-expense-internal () + "Makes the expense an internal one." + (interactive) + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (let ((begin (point)) + (end (save-excursion (re-search-forward "^$")))) + (when (re-search-forward "^ Dest:Projects" end t) + (replace-match " Dest:Internal") ) + (when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t) + (replace-match "; CATEGORY: Travel" t)))))) + +(defun ledger-expense-personal () + "Makes the expense an personal one, eliminating metadata and receipts." + (interactive) + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (let ((begin (point)) + (end (save-excursion (re-search-forward "^$")))) + (when (re-search-forward "^ Dest:Projects" end t) + (replace-match " Other:Personal")) + (goto-char begin) + (save-excursion + (when (re-search-forward "^ +; ER:" end t) + (beginning-of-line) + (kill-line 1))) + (save-excursion + (when (re-search-forward "^ +; PROJECT:" end t) + (beginning-of-line) + (kill-line 1))) + (save-excursion + (when (re-search-forward "^ +; CATEGORY:" end t) + (beginning-of-line) + (kill-line 1))) + (save-excursion + (when (re-search-forward "^ +; RECEIPT:" end t) + (beginning-of-line) + (kill-line 1))) + (ledger-toggle-current-entry))))) + +(defun ledger-expense-show-receipt () + "Uses the Receipt buffer to show the receipt of the txn we're on." + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (let ((begin (point)) + (end (save-excursion (re-search-forward "^$")))) + (save-excursion + (when (re-search-forward "^\\( +; RECEIPT: +\\)\\([^,]+?.jpg\\).*$" end t) + (ledger-matching-display-image + (concat "/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/" + (match-string 2))) )))))) diff --git a/contrib/raw/ledger-matching.el b/contrib/raw/ledger-matching.el new file mode 100644 index 00000000..36006a69 --- /dev/null +++ b/contrib/raw/ledger-matching.el @@ -0,0 +1,342 @@ +;; This library is intended to allow me to view a receipt on one panel, and tie it to ledger transactions in another + +(require 'ledger-report) + +(defgroup ledger-matching nil + "Ledger image matching") + +(defcustom ledger-matching-sourcedir "~/AdamsInfoServ/BusinessDocuments/Ledger/Incoming" + "Source directory for images to process, ie: the incoming queue of images." + :group 'ledger-matching) + +(defcustom ledger-matching-destdir "~/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/Receipts" + "Destination directory for images when matched, will still have a project directory appended to it." + :group 'ledger-matching) + +(defcustom ledger-matching-relative-receipt-dir "Receipts" + "Relative directory root for destination images used in Ledger entries, will have the project directory appended and receipt filename." + :group 'ledger-matching) + +(defcustom ledger-matching-convert-binary "/usr/bin/convert" + "Path to the Imagemagick convert command." + :group 'ledger-matching) + +(defcustom ledger-matching-scale 50 + "Scaling parameter to Imagemagick's convert to resize an image for viewing." + :group 'ledger-matching) + +(defcustom ledger-matching-rotation 0 + "Rotation parameter to Imagemagick's convert to rotate an image for viewing. Images on disk should always be upright for reading." + :group 'ledger-matching) + + +(defconst ledger-matching-image-buffer "*Receipt*" + "Buffer name we load images into. Created if it doesn't exist, and persists across image loads.") + + +(defvar ledger-matching-project "Internal" + "The directory appended to the destination for the project code where receipts will be stored.") + +(defvar ledger-matching-image-offset 0 + "The index of the current file from the SORTED source directory contents.") + +(defvar ledger-matching-image-name nil + "The filename only of the current image.") + + +(defun ledger-matching-display-image (image-filename) + "Resize the image and load it into our viewing buffer." + + ;; Create our viewing buffer if needed, and set it. Do NOT switch, + ;; this buffer isn't the primary. Let the user leave it where they + ;; place it. + (unless (get-buffer ledger-matching-image-buffer) + (get-buffer-create ledger-matching-image-buffer)) + (set-buffer ledger-matching-image-buffer) + (erase-buffer) + (goto-char (point-min)) + (insert-string image-filename "\n") + + ;; Convert the source to the temporary dest applying resizing and rotation + (let* ((source (expand-file-name image-filename ledger-matching-sourcedir)) + (dest (make-temp-file "ledger-matching-" nil ".jpg")) + (result (call-process ledger-matching-convert-binary nil (get-buffer "*Messages*") nil + source + "-scale" (concat (number-to-string ledger-matching-scale) "%") + "-rotate" (number-to-string ledger-matching-rotation) + dest))) + + (if (/= 0 result) + + ;; Bomb out if the convert fails + (message "Error running convert, see *Messages* buffer for details.") + + ;; Insert scaled image into the viewing buffer, replacing + ;; current contents Temp buffer is to force sync reading into + ;; memory of the jpeg due to async race condition with display + ;; and file deletion + (let ((image (create-image (with-temp-buffer + (insert-file-contents-literally dest) + (string-as-unibyte (buffer-string))) + 'jpeg t))) + (insert-image image) + (goto-char (point-min)) + + ;; Redisplay is required to prevent a race condition between displaying the image and the deletion. Apparently its async. + ;; Either redisplay or the above string method work, both together can't hurt. + (redisplay) + )) + + ;; Delete our temporary file + (delete-file dest))) + + + +(defun ledger-matching-update-current-image () + "Grab the image from the source directory by offset and display" + + (let* ((file-listing (directory-files ledger-matching-sourcedir nil "\.jpg$" nil)) + (len (safe-length file-listing))) + + ;; Ensure our offset doesn't exceed the file list + (cond ((= len 0) + (message "No files found in source directory.")) + + ((< len 0) + (message "Error, list of files should never be negative. Epic fail.")) + + ((>= ledger-matching-image-offset len) + (message "Hit end of list. Last image.") + (setq ledger-matching-image-offset (1- len))) + + ((< ledger-matching-image-offset 0) + (message "Beginning of list. First image.") + (setq ledger-matching-image-offset 0))) + + ;; Get the name for the offset + (setq ledger-matching-image-name (nth ledger-matching-image-offset file-listing)) + + (ledger-matching-display-image ledger-matching-image-name))) + + + +(defun ledger-matching-image-offset-adjust (amount) + "Incr/decr the offset and update the receipt buffer." + + (setq ledger-matching-image-offset (+ ledger-matching-image-offset amount)) + (ledger-matching-update-current-image)) + + + +(defun ledger-receipt-matching () + "Open the receipt buffer and start with the first image." + (interactive) + (setq ledger-matching-image-offset 0) + (ledger-matching-update-current-image)) + + + +(defun ledger-matching-tie-receipt-to-txn () + (interactive) + (save-selected-window + (ledger-report-visit-source) + + ;; Assumes we're in a narrowed buffer with ONLY this txn + (backward-paragraph) + (beginning-of-line) + + ;; ;; Update the ER and Project while I'm there + ;; (save-excursion + ;; (search-forward "; ER:") + ;; (kill-line nil) + ;; (insert " " *ledger-expense-shortcut-ER*)) + ;; Just do the project for now. + (save-excursion + (search-forward "; PROJECT:") + (kill-line nil) + (insert " " *ledger-expense-shortcut-Proj*)) + + ;; Goto the receipt line, unless their isn't one then add one + (unless (search-forward "RECEIPT:" nil t) + + ;; Still at date line if that failed + (next-line) + (newline) + (insert-string " ; RECEIPT:")) + + ;; Point immediately after : on tag + + ;; Check for existing jpg file + (if (search-forward ".jpg" (line-end-position) t) + + ;; if present make it a comma delimited list + (insert-string ",") + + ;; otherwise just add a space to pad + (insert-string " ")) + + ;; Add our relative filename as the value of the RECEIPT tag + (insert-string (concat ledger-matching-relative-receipt-dir "/" + ledger-matching-project "/" + ledger-matching-image-name)) + + ;; Create the destination project dir if it doesn't exist. + (let ((full-destination (concat ledger-matching-destdir "/" ledger-matching-project ))) + (unless (file-accessible-directory-p full-destination) + (make-directory full-destination t))) + + ;; Rename the file from the source directory to its permanent home + (rename-file (concat ledger-matching-sourcedir "/" + ledger-matching-image-name) + (concat ledger-matching-destdir "/" + ledger-matching-project "/" + ledger-matching-image-name)) + + ;; Update the receipt screen + (ledger-matching-update-current-image) + + (message "Filed %s to project %s" ledger-matching-image-name ledger-matching-project))) + + + +(defun ledger-receipt-skip () + "Move the current image to the Skip directory because its not relevant." + + (rename-file (concat ledger-matching-sourcedir "/" + ledger-matching-image-name) + (concat ledger-matching-sourcedir "/Skip/" + ledger-matching-image-name)) + + ;; Update the receipt screen at the same offset + (ledger-matching-update-current-image)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Items below are speed entry macros, and should eventually migrate to their own file. + +(defvar *ledger-expense-shortcut-ER* + "Current expense report number, just last four digits (ie: 1234 results in AISER1234).") + +(defvar *ledger-expense-shortcut-split-ER* + "Split (ie: internal) expense report number, just last four digits (ie: 1234 results in AISER1234).") + +(defvar *ledger-expense-shortcut-Proj* "" + "Current export report project code (ie: AGIL1292)") + +(defun ledger-expense-shortcut-ER-format-specifier () *ledger-expense-shortcut-ER*) + +(defun ledger-expense-shortcut-Project-format-specifier () *ledger-expense-shortcut-Proj*) + +(defun ledger-expense-shortcut-setup (ER Split Proj) + "Sets the variables expanded into the transaction." + (interactive "MER Number (ER or IN and 4 digit number only): \nMSplit ER Number (ER or IN and 4 digit number only): \nMProject: ") + (setq *ledger-expense-shortcut-ER* + (concatenate 'string "AIS" ER)) + (setq *ledger-expense-shortcut-split-ER* + (concatenate 'string "AIS" Split)) + (setq *ledger-expense-shortcut-Proj* Proj) + (setq ledger-matching-project Proj) + (message "Set Proj to %s and ER to %s, split to %s" + *ledger-expense-shortcut-Proj* + *ledger-expense-shortcut-ER* + *ledger-expense-shortcut-split-ER*)) + +(defun ledger-expense-shortcut () + "Updates the ER and Project metadata with the current values of the shortcut variables." + (interactive) + (when (eq major-mode 'ledger-mode) + (if (or (eql *ledger-expense-shortcut-ER* "") + (eql *ledger-expense-shortcut-Proj* "")) + (message "Run ledger-expense-shortcut-setup first.") + (save-excursion + (search-forward "; ER:") + (kill-line nil) + (insert " " *ledger-expense-shortcut-ER*)) + (save-excursion + (search-forward "; PROJECT:") + (kill-line nil) + (insert " " *ledger-expense-shortcut-Proj*))))) + +(defun ledger-expense-split () + "Splits the current transaction between internal and projects." + (interactive) + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (re-search-forward "^ +Dest:Projects") + (move-beginning-of-line nil) + (let ((begin (point)) + (end (re-search-forward "^$"))) + (goto-char end) + (insert (buffer-substring begin end)) + (goto-char end) + (re-search-forward "^ Dest:Projects") + (replace-match " Dest:Internal") + (re-search-forward "; ER: +[A-Za-z0-9]+") + (replace-match (concat "; ER: " *ledger-expense-shortcut-split-ER*) t) + (when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t) + (replace-match "; CATEGORY: Travel" t)))) + (re-search-backward "^[0-9]\\{4\\}/") + (re-search-forward "^ +Dest:Projects") + (insert-string " $") )) + +(defun ledger-expense-internal () + "Makes the expense an internal one." + (interactive) + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (let ((begin (point)) + (end (save-excursion (re-search-forward "^$")))) + (when (re-search-forward "^ Dest:Projects" end t) + (replace-match " Dest:Internal") ) + (when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t) + (replace-match "; CATEGORY: Travel" t)))))) + +(defun ledger-expense-personal () + "Makes the expense an personal one, eliminating metadata and receipts." + (interactive) + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (let ((begin (point)) + (end (save-excursion (re-search-forward "^$")))) + (when (re-search-forward "^ Dest:Projects" end t) + (replace-match " Other:Personal")) + (goto-char begin) + (save-excursion + (when (re-search-forward "^ +; ER:" end t) + (beginning-of-line) + (kill-line 1))) + (save-excursion + (when (re-search-forward "^ +; PROJECT:" end t) + (beginning-of-line) + (kill-line 1))) + (save-excursion + (when (re-search-forward "^ +; CATEGORY:" end t) + (beginning-of-line) + (kill-line 1))) + (save-excursion + (when (re-search-forward "^ +; RECEIPT:" end t) + (beginning-of-line) + (kill-line 1))) + (ledger-toggle-current-entry))))) + +(defun ledger-expense-show-receipt () + "Uses the Receipt buffer to show the receipt of the txn we're on." + (when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode + (save-excursion + (end-of-line) + (re-search-backward "^[0-9]\\{4\\}/") + (let ((begin (point)) + (end (save-excursion (re-search-forward "^$")))) + (save-excursion + (when (re-search-forward "^\\( +; RECEIPT: +\\)\\([^,]+?.jpg\\).*$" end t) + (ledger-matching-display-image + (concat "/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/" + (match-string 2))) )))))) + + +(provide 'ledger-matching) diff --git a/contrib/raw/ledger-shell-environment-functions b/contrib/raw/ledger-shell-environment-functions new file mode 100644 index 00000000..7746dc41 --- /dev/null +++ b/contrib/raw/ledger-shell-environment-functions @@ -0,0 +1,90 @@ +# Environment for ledger expenses + +[ $(whoami) == "adamsrl" ] \ + && export LEDGER_HOME="/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell" \ + || export LEDGER_HOME="/home/Heather/AdamsRussell" + +[ $(hostname) == "cardamom" ] \ + && export LEDGER_BIN="${LEDGER_HOME}/ledger" \ + || export LEDGER_BIN="${LEDGER_HOME}/ledger.exe" + +[ $(whoami) == "andersonll" ] \ + && export LEDGER_HOME="/home/andersonll/AdamsInfoServ/Expenses" \ + && export LEDGER_BIN="${LEDGER_HOME}/ledger" + +# Common reports + +alias ledger='${LEDGER_BIN} -f "${LEDGER_HOME}/.ledger" -VE ' +alias ERSummary='ledger --pivot ER bal | egrep "AIS(ER|IN)[0-9]+|Unassigned"' + +function ERTxns() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + ledger reg "%ER=${1}" +} + +function ERCategorySummary() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + ledger bal --pivot CATEGORY "%ER=${1}" +} + +function ERMealSummary() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + ledger reg "%ER=${1}" and %CATEGORY=Meals -D +} + +function ERMeals() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + ledger reg "%ER=${1}" and %CATEGORY=Meals +} + +function ERUncleared() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + ledger reg "%ER=${1}" -U +} + +function ERMissingReceipts() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + ledger reg "%ER=${1}" and not %RECEIPT +} + +function ERVerify() { + [ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return + + echo "========== Uncleared txns below ==========" + ERUncleared "$1" + echo "========== Missing receipts below (miles and stubs ok) ==========" + ERMissingReceipts "$1" + echo "========== Category Summary (airline? mileage? car? hotel? ==========" + ERCategorySummary "$1" + echo "========== Meal summary (<\$50 / day unless otherwise specified) ==========" + ERMealSummary "$1" + echo "========== Account Verification (Internal vs Project ER should be ONE type) ==========" + echo $1 | grep AISIN >/dev/null 2>&1 \ + || { ledger reg "%ER=${1}" | grep Dest:Internal ; } \ + && { ledger reg "%ER=${1}" | grep Dest:Projects ; } + echo "========== Project Verification (only one project code should be listed) ==========" + ledger print "%ER=${1}" | grep PROJECT | sort -u + echo "========== Receipts missing ==========" + ledger print "%ER=${1}" | grep -h '; RECEIPT: ' \ + | sed 's,\W*; RECEIPT: ,,g' \ + | tr , '\n' \ + | sort -u \ + | while read X ; do + [ -f "${LEDGER_HOME}/${X}" ] \ + || echo XX $X + done +} + +function ERListing() { + ledger reg Stub --register-format="%(tag('ER')) %(tag('NOTE'))\n" | sort -u +} + +function ERQueue() { + ledger reg %ER=Unassigned --prepend-format="%(filename) " +} diff --git a/contrib/repl.sh b/contrib/repl.sh new file mode 100755 index 00000000..42fb54c6 --- /dev/null +++ b/contrib/repl.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +EXEC=$(which ledger) +if [[ -z "$EXEC" ]]; then + EXEC=$HOME/Products/ledger/ledger +fi + +if [[ ! -x "$EXEC" ]]; then + echo Cannot find Ledger executable + exit 1 +fi + +LESS=--quit-if-one-screen exec $EXEC --pager less "$@" diff --git a/contrib/report b/contrib/report new file mode 100755 index 00000000..1a84a399 --- /dev/null +++ b/contrib/report @@ -0,0 +1,21 @@ +#!/bin/sh + +# This script facilities plotting of a ledger register report. If you +# use OS/X, and have AquaTerm installed, you will probably want to set +# LEDGER_TERM to "aqua". +# +# Examples of use: +# +# report -j -M reg food # plot monthly food costs +# report -J reg checking # plot checking account balance + +if [ -z "$LEDGER_TERM" ]; then + LEDGER_TERM="x11 persist" +fi + +(cat <<EOF; ledger "$@") | gnuplot + set terminal $LEDGER_TERM + set xdata time + set timefmt "%Y-%m-%d" + plot "-" using 1:2 with lines +EOF diff --git a/contrib/tc b/contrib/tc new file mode 100755 index 00000000..c24be99a --- /dev/null +++ b/contrib/tc @@ -0,0 +1,7 @@ +#!/bin/sh + +timeclock out + +proj="$1" +shift +timeclock in "$proj" "$@" diff --git a/contrib/ti b/contrib/ti new file mode 100755 index 00000000..a7214e65 --- /dev/null +++ b/contrib/ti @@ -0,0 +1,5 @@ +#!/bin/sh + +proj="$1" +shift +timeclock in "$proj" "$@" diff --git a/contrib/to b/contrib/to new file mode 100755 index 00000000..3198db3c --- /dev/null +++ b/contrib/to @@ -0,0 +1,3 @@ +#!/bin/sh + +timeclock out "$@" diff --git a/contrib/trend b/contrib/trend new file mode 100755 index 00000000..3c189c0b --- /dev/null +++ b/contrib/trend @@ -0,0 +1,30 @@ +#!/bin/sh + +# This script requires Python support. +# +# To use, just run "trend" with the accounts to compute the trend for: +# +# trend dining +# +# The trend values are not terribly meaningful, but this gives an +# example of how Python can be used to create more complex reports. + +ledger --import-stdin -T "@rdev()" reg "$@" <<EOF +import ledger + +mean = ledger.parse_value_expr ("AT") +last_mean = None +last_dev = None + +def rdev (details): + global last_mean, last_dev + mval = mean.compute (details) + if last_mean is None: + dev = ledger.Value () + else: + dev = mval - last_mean + dev = (last_dev + dev) / 2 + last_mean = mval + last_dev = dev + return dev +EOF diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..f5e3dddd --- /dev/null +++ b/default.nix @@ -0,0 +1,49 @@ +{ stdenv, fetchgit, cmake, boost, gmp, mpfr, libedit, python +, texinfo, gnused }: + +let + version = "3.1.3"; + rev = "20190331"; +in + +stdenv.mkDerivation { + name = "ledger-${version}-${rev}"; + + # NOTE: fetchgit because ledger has submodules not included in the + # default github tarball. + src = ./.; + + buildInputs = [ cmake boost gmp mpfr libedit python texinfo gnused ]; + + enableParallelBuilding = true; + + # Skip byte-compiling of emacs-lisp files because this is currently + # broken in ledger... + postInstall = '' + mkdir -p $out/share/emacs/site-lisp/ + cp -v "$src/lisp/"*.el $out/share/emacs/site-lisp/ + ''; + + cmakeFlags = [ "-DCMAKE_INSTALL_LIBDIR=lib" ]; + + buildPhase = "make -j$NIX_BUILD_CORES"; + checkPhase = "ctest -j$NIX_BUILD_CORES"; + + doCheck = true; + + meta = { + homepage = "http://ledger-cli.org/"; + description = "A double-entry accounting system with a command-line reporting interface"; + license = stdenv.lib.licenses.bsd3; + + longDescription = '' + Ledger is a powerful, double-entry accounting system that is accessed + from the UNIX command-line. This may put off some users, as there is + no flashy UI, but for those who want unparalleled reporting access to + their data, there really is no alternative. + ''; + + platforms = stdenv.lib.platforms.all; + maintainers = with stdenv.lib.maintainers; [ the-kenny jwiegley ]; + }; +} diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 00000000..d8242eeb --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,138 @@ +# The following will be generated or updated when the 'doc' target is built: +# • user guide and man pages: if BUILD_DOCS is set +# • HTML versions of the above: if BUILD_DOCS and BUILD_WEB_DOCS are set +# • Doxygen / reference documentation: if USE_DOXYGEN is set + +######################################################################## + +configure_file( + ${PROJECT_SOURCE_DIR}/doc/version.texi.in + ${PROJECT_BINARY_DIR}/doc/version.texi) + +if (USE_DOXYGEN) + find_package(Doxygen) + if (NOT DOXYGEN_FOUND) + message(FATAL_ERROR "Could not find doxygen. Reference documentation cannot be built.") + endif() + + configure_file(Doxyfile.in Doxyfile @ONLY) + + # see INPUT/FILE_PATTERNS in Doxyfile.in + file(GLOB doxygen_input_files ${CMAKE_SOURCE_DIR}/src/*.h) + + add_custom_command(OUTPUT html/index.html + COMMAND ${DOXYGEN_EXECUTABLE} Doxyfile + DEPENDS Doxyfile ${doxygen_input_files} + COMMENT "Building doxygen documentation") + add_custom_target(doc.doxygen DEPENDS html/index.html) +else() + add_custom_target(doc.doxygen) +endif() + +######################################################################## + +# BUILD_WEB_DOCS implies BUILD_DOCS +if (BUILD_WEB_DOCS) + set(BUILD_DOCS 1) +endif() + +if (BUILD_DOCS) + find_program(MAKEINFO makeinfo) + find_program(TEXI2PDF texi2pdf) + find_program(TEX tex) + find_program(MAN2HTML man2html) + find_program(GROFF groff) + set(ledger_info_files ledger3.texi) + + if (NOT MAKEINFO) + message(WARNING "Could not find makeinfo. Info version of documentation cannot be built.") + endif() + + if (NOT TEXI2PDF OR NOT TEX) + message(WARNING "Could not find texi2pdf or tex. PDF version of documentation will not be built.") + endif() +endif() + +######################################################################## + +foreach(file ${ledger_info_files}) + get_filename_component(file_base ${file} NAME_WE) + + if (MAKEINFO) + add_custom_command(OUTPUT ${file_base}.info + COMMAND makeinfo --force --no-split -o ${file_base}.info ${CMAKE_CURRENT_SOURCE_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + VERBATIM) + list(APPEND ledger_doc_files ${file_base}.info) + endif() + + if (BUILD_WEB_DOCS AND MAKEINFO) + add_custom_command(OUTPUT ${file_base}.html + COMMAND makeinfo --force --html --no-split -o ${file_base}.html ${CMAKE_CURRENT_SOURCE_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + VERBATIM) + list(APPEND ledger_doc_files ${file_base}.html) + endif() + + if (TEXI2PDF AND TEX) + if (BUILD_A4_PDF) + set(papersize --texinfo=@afourpaper) + endif() + add_custom_command(OUTPUT ${file_base}.pdf + COMMAND texi2pdf ${papersize} -b -q -o ${file_base}.pdf ${CMAKE_CURRENT_SOURCE_DIR}/${file} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${file} + VERBATIM) + list(APPEND ledger_doc_files ${file_base}.pdf) + endif() +endforeach() + +######################################################################## + +if (BUILD_WEB_DOCS) + include(FindUnixCommands) + if (NOT BASH) + message(FATAL_ERROR "Could not find bash. Unable to build documentation.") + endif() + if (MAN2HTML) + add_custom_command(OUTPUT ledger.1.html + COMMAND ${BASH} -c "man2html ${CMAKE_CURRENT_SOURCE_DIR}/ledger.1 | tail -n+3 > ledger.1.html" + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/ledger.1 + VERBATIM) + list(APPEND ledger_doc_files ledger.1.html) + elseif(GROFF) + add_custom_command(OUTPUT ledger.1.html + COMMAND ${BASH} -c "groff -mandoc -Thtml ${CMAKE_CURRENT_SOURCE_DIR}/ledger.1 > ledger.1.html" + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/ledger.1 + VERBATIM) + list(APPEND ledger_doc_files ledger.1.html) + else() + message(FATAL_ERROR "Could not find man2html or groff. HTML version of man page cannot be built.") + endif() +endif(BUILD_WEB_DOCS) + +######################################################################## + +add_custom_target(doc DEPENDS ${ledger_doc_files} doc.doxygen) + +######################################################################## + +include(GNUInstallDirs) + +if (CMAKE_INSTALL_MANDIR) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ledger.1 + DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 COMPONENT doc) +endif(CMAKE_INSTALL_MANDIR) + +foreach(file ${ledger_doc_files}) + get_filename_component(file_ext ${file} EXT) + + if(file_ext STREQUAL ".info") + if(CMAKE_INSTALL_INFODIR) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${file} + DESTINATION ${CMAKE_INSTALL_INFODIR} COMPONENT doc) + endif() + elseif(CMAKE_INSTALL_DOCDIR) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${file} + DESTINATION ${CMAKE_INSTALL_DOCDIR} COMPONENT doc) + endif() +endforeach() diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in new file mode 100644 index 00000000..95373660 --- /dev/null +++ b/doc/Doxyfile.in @@ -0,0 +1,1877 @@ +# Doxyfile 1.8.3 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" "). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# http://www.gnu.org/software/libiconv for the list of possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or sequence of words) that should +# identify the project. Note that if you do not use Doxywizard you need +# to put quotes around the project name if it contains spaces. + +PROJECT_NAME = Ledger + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = 3.0 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer +# a quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify an logo or icon that is +# included in the documentation. The maximum height of the logo should not +# exceed 55 pixels and the maximum width should not exceed 200 pixels. +# Doxygen will copy the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = @CMAKE_CURRENT_BINARY_DIR@ + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, +# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English +# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, +# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, +# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = YES + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = YES + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = NO + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. Note that you specify absolute paths here, but also +# relative paths, which will be relative from the directory where doxygen is +# started. + +STRIP_FROM_PATH = @PROJECT_SOURCE_DIR@/src/ + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful if your file system +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = YES + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = YES + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 8 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding +# "class=itcl::class" will allow you to use the command class in the +# itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for +# Java. For instance, namespaces will be presented as packages, qualified +# scopes will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources only. Doxygen will then generate output that is more tailored for +# Fortran. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for +# VHDL. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, +# and language is one of the parsers supported by doxygen: IDL, Java, +# Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, +# C++. For instance to make doxygen treat .inc files as Fortran files (default +# is PHP), and .f files as C (default is Fortran), use: inc=Fortran f=C. Note +# that for custom extensions you also need to set FILE_PATTERNS otherwise the +# files are not read by doxygen. + +EXTENSION_MAPPING = + +# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all +# comments according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you +# can mix doxygen, HTML, and XML commands with Markdown formatting. +# Disable only in case of backward compatibilities issues. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented classes, +# or namespaces to their corresponding documentation. Such a link can be +# prevented in individual cases by by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also makes the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES (the +# default) will make doxygen replace the get and set methods by a property in +# the documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and +# unions are shown inside the group in which they are included (e.g. using +# @ingroup) instead of on a separate page (for HTML and Man pages) or +# section (for LaTeX and RTF). + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and +# unions with only public data fields will be shown inline in the documentation +# of the scope in which they are defined (i.e. file, namespace, or group +# documentation), provided this scope is documented. If set to NO (the default), +# structs, classes, and unions are shown on a separate page (for HTML and Man +# pages) or section (for LaTeX and RTF). + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to +# determine which symbols to keep in memory and which to flush to disk. +# When the cache is full, less often used symbols will be written to disk. +# For small to medium size projects (<1000 input files) the default value is +# probably good enough. For larger projects a too small cache size can cause +# doxygen to be busy swapping symbols to and from disk most of the time +# causing a significant performance penalty. +# If the system has enough physical memory increasing the cache will improve the +# performance by keeping more symbols in memory. Note that the value works on +# a logarithmic scale so increasing the size by one will roughly double the +# memory usage. The cache size is given by this formula: +# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols. + +SYMBOL_CACHE_SIZE = 0 + +# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be +# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given +# their name and scope. Since this can be an expensive process and often the +# same symbol appear multiple times in the code, doxygen keeps a cache of +# pre-resolved symbols. If the cache is too small doxygen will become slower. +# If the cache is too large, memory is wasted. The cache size is given by this +# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = NO + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespaces are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = YES + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = YES + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to +# do proper type resolution of all parameters of a function it will reject a +# match between the prototype and the implementation of a member function even +# if there is only one candidate or it is obvious which candidate to choose +# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen +# will still accept a match between prototype and implementation in such cases. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if section-label ... \endif +# and \cond section-label ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or macro consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and macros in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command <command> <input-file>, where <command> is the value of +# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files +# containing the references data. This must be a list of .bib files. The +# .bib extension is automatically appended if omitted. Using this command +# requires the bibtex tool to be installed. See also +# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style +# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this +# feature you need bibtex and perl available in the search path. Do not use +# file names with spaces, bibtex cannot handle them. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +# jww (2009-01-31): Enable this toward the end +WARN_IF_UNDOCUMENTED = NO + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# The WARN_NO_PARAMDOC option can be enabled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = YES + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +# please update dependencies in CMakeList.txt if you change this +INPUT = @PROJECT_SOURCE_DIR@/src + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is +# also the default input encoding. Doxygen uses libiconv (or the iconv built +# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh +# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py +# *.f90 *.f *.for *.vhd *.vhdl + +# please update dependencies in CMakeList.txt if you change this +FILE_PATTERNS = *.h + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command <filter> <input-file>, where <filter> +# is the value of the INPUT_FILTER tag, and <input-file> is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty or if +# non of the patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) +# and it is also possible to disable source filtering for a specific pattern +# using *.ext= (so without naming a filter). This option only has effect when +# FILTER_SOURCE_FILES is enabled. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MD_FILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page (index.html). +# This can be useful if you have a project on for instance GitHub and want reuse +# the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C, C++ and Fortran comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. Note that when using a custom header you are responsible +# for the proper inclusion of any scripts and style sheets that doxygen +# needs, which is dependent on the configuration options used. +# It is advised to generate a default header using "doxygen -w html +# header.html footer.html stylesheet.css YourConfigFile" and then modify +# that header. Note that the header is subject to change so you typically +# have to redo this when upgrading to a newer version of doxygen or when +# changing the value of configuration settings such as GENERATE_TREEVIEW! + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If left blank doxygen will +# generate a default style sheet. Note that it is recommended to use +# HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this +# tag will in the future become obsolete. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional +# user-defined cascading style sheet that is included after the standard +# style sheets created by doxygen. Using this option one can overrule +# certain style aspects. This is preferred over using HTML_STYLESHEET +# since it does not replace the standard style sheet and is therefore more +# robust against future updates. Doxygen will copy the style sheet file to +# the output directory. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that +# the files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the style sheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see http://en.wikipedia.org/wiki/Hue for more information. +# For instance the value 0 represents red, 60 is yellow, 120 is green, +# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. +# The allowed range is 0 to 359. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of +# the colors in the HTML output. For a value of 0 the output will use +# grayscales only. A value of 255 will produce the most vivid colors. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to +# the luminance component of the colors in the HTML output. Values below +# 100 gradually make the output lighter, whereas values above 100 make +# the output darker. The value divided by 100 is the actual gamma applied, +# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, +# and 100 does not change the gamma. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting +# this to NO can help when comparing the output of multiple runs. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. + +HTML_DYNAMIC_SECTIONS = YES + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of +# entries shown in the various tree structured indices initially; the user +# can expand and collapse entries dynamically later on. Doxygen will expand +# the tree to such a level that at most the specified number of entries are +# visible (unless a fully collapsed tree already exceeds this amount). +# So setting the number of entries 1 will produce a full collapsed tree by +# default. 0 is a special value representing an infinite number of entries +# and will result in a full expanded tree by default. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files +# will be generated that can be used as input for Apple's Xcode 3 +# integrated development environment, introduced with OSX 10.5 (Leopard). +# To create a documentation set, doxygen will generate a Makefile in the +# HTML output directory. Running make will produce the docset in that +# directory and running "make install" will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find +# it at startup. +# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely +# identify the documentation publisher. This should be a reverse domain-name +# style string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = doc + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters"> +# Qt Help Project / Custom Filters</a>. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes"> +# Qt Help Project / Filter Attributes</a>. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) +# at top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. Since the tabs have the same information as the +# navigation tree you can set this option to NO if you already set +# GENERATE_TREEVIEW to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. +# Since the tree basically has the same information as the tab index you +# could consider to set DISABLE_INDEX to NO when enabling this option. + +GENERATE_TREEVIEW = YES + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values +# (range [0,1..20]) that doxygen will group on one line in the generated HTML +# documentation. Note that a value of 0 will completely suppress the enum +# values from appearing in the overview section. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax +# (see http://www.mathjax.org) which uses client side Javascript for the +# rendering instead of using prerendered bitmaps. Use this if you do not +# have LaTeX installed or if you want to formulas look prettier in the HTML +# output. When enabled you may also need to install MathJax separately and +# configure the path to it using the MATHJAX_RELPATH option. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# thA MathJax output. Supported types are HTML-CSS, NativeMML (i.e. MathML) and +# SVG. The default value is HTML-CSS, which is slower, but has the best +# compatibility. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the +# HTML output directory using the MATHJAX_RELPATH option. The destination +# directory should contain the MathJax.js script. For instance, if the mathjax +# directory is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to +# the MathJax Content Delivery Network so you can quickly see the result without +# installing MathJax. +# However, it is strongly recommended to install a local +# copy of MathJax from http://www.mathjax.org before deployment. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension +# names that should be enabled during MathJax rendering. + +MATHJAX_EXTENSIONS = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = YES + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a web server instead of a web client using Javascript. +# There are two flavours of web server based search depending on the +# EXTERNAL_SEARCH setting. When disabled, doxygen will generate a PHP script for +# searching and an index file used by the script. When EXTERNAL_SEARCH is +# enabled the indexing and searching needs to be provided by external tools. +# See the manual for details. + +SERVER_BASED_SEARCH = NO + +# When EXTERNAL_SEARCH is enabled doxygen will no longer generate the PHP +# script for searching. Instead the search results are written to an XML file +# which needs to be processed by an external indexer. Doxygen will invoke an +# external search engine pointed to by the SEARCHENGINE_URL option to obtain +# the search results. Doxygen ships with an example indexer (doxyindexer) and +# search engine (doxysearch.cgi) which are based on the open source search engine +# library Xapian. See the manual for configuration details. + +EXTERNAL_SEARCH = NO + +# The SEARCHENGINE_URL should point to a search engine hosted by a web server +# which will returned the search results when EXTERNAL_SEARCH is enabled. +# Doxygen ships with an example search engine (doxysearch) which is based on +# the open source search engine library Xapian. See the manual for configuration +# details. + +SEARCHENGINE_URL = + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed +# search data is written to a file for indexing by an external tool. With the +# SEARCHDATA_FILE tag the name of this file can be specified. + +SEARCHDATA_FILE = searchdata.xml + +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through other +# doxygen projects that are not otherwise connected via tags files, but are +# all added to the same search index. Each project needs to have a tag file set +# via GENERATE_TAGFILE. The search mapping then maps the name of the tag file +# to a relative location where the documentation can be found, +# similar to the +# TAGFILES option but without actually processing the tag file. +# The format is: EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ... + +EXTRA_SEARCH_MAPPINGS = + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = YES + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = letter + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for +# the generated latex document. The footer should contain everything after +# the last chapter. If it is left blank doxygen will generate a +# standard footer. Notice: only use this tag if you know what you are doing! + +LATEX_FOOTER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See +# http://en.wikipedia.org/wiki/BibTeX for more info. + +LATEX_BIB_STYLE = plain + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load style sheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = YES + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# pointed to by INCLUDE_PATH will be searched when a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition that +# overrules the definition found in the source code. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all references to function-like macros +# that are alone on a line, have an all uppercase name, and do not end with a +# semicolon, because these will confuse the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. For each +# tag file the location of the external documentation should be added. The +# format of a tag file without this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths +# or URLs. Note that each tag file must have a unique name (where the name does +# NOT include the path). If a tag file is not located in the directory in which +# doxygen is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option also works with HAVE_DOT disabled, but it is recommended to +# install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = YES + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will use the Helvetica font for all dot files that +# doxygen generates. When you want a differently looking font you can specify +# the font name using DOT_FONTNAME. You need to make sure dot is able to find +# the font, which can be done by putting it in a standard location or by setting +# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. + +DOT_FONTNAME = FreeSans + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 11 + +# By default doxygen will tell dot to use the Helvetica font. +# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to +# set the path where dot can find it. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside +# the class node. If there are many fields or methods and many nodes the +# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS +# threshold limits the number of items for each type to make the size more +# managable. Set this to 0 for no limit. Note that the threshold may be +# exceeded by 50% before the limit is enforced. + +UML_LIMIT_NUM_FIELDS = 10 + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = YES + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = YES + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will generate a graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are svg, png, jpg, or gif. +# If left blank png will be used. If you choose svg you need to set +# HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible in IE 9+ (other browsers do not have this requirement). + +DOT_IMAGE_FORMAT = jpg + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# Note that this requires a modern browser other than Internet Explorer. +# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you +# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible. Older versions of IE do not have SVG support. + +INTERACTIVE_SVG = NO + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the +# \mscfile command). + +MSCFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 1000 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES diff --git a/doc/GLOSSARY.md b/doc/GLOSSARY.md new file mode 100644 index 00000000..f64d0cf3 --- /dev/null +++ b/doc/GLOSSARY.md @@ -0,0 +1,151 @@ +ACCOUNTING GLOSSARY +--- + + Accounting and bookkeeping represent an entire field of human effort and has + evolved its own specialized vocabulary. Accounting hopes to summarize and + add understanding to where the money is going. + +**Account**: A category for grouping together amounts from similar + transactions. Each account has a name, which is usually capitalized, and an + account type. Accounts are often organized into a hierarchy when it helps + understanding. For example, a coffee shop might have Coffee, Merchandise, + and Equipment as accounts but arranged under an Inventory account because + different decisions are made on the total inventory rather than just coffee. + A hierarchy can be part of the account name in Ledger, e.g., + "Assets:Inventory:Coffee". Note that the Ledger software usually creates + the list of accounts on the fly: accounts are created when transactions use + them. + +**Account Type**: Each account has a type of Asset, Liability, Equity, Income, + or Expense. Assets represent something owned, e.g., Cash or Inventory. + Liabilities represent sometime owed, e.g., a Loan or Mortgage. Equity, also + called capital, is everything owned minus everything owed (Assets - + Liabilities). It is the financial measure of how much you are ahead. + Income is money earned somewhere, which puts you more ahead. Expenses is + money spent somewhere, which puts you less ahead. The type of account + determines if a debit represents an increase or decrease in an account. For + example, Inventory is an asset so a transaction debiting Inventory would + increase its value. Assets and Expenses increase with debits and decrease + with credits; Liabilities, Equity, and Income increase with credits and + decrease with debits. + +**Journal**: A record of all the financial transactions of a person or firm. + This data of where money goes can be collated into reports. This used to be + done with a physical book, called a ledger, where each account was on one + page. Each debit or credit in the journal was transferred to the + appropriate account page and the pages were totaled to produce reports. + This process is now done with the Ledger software which creates reports from + the journal. A journal is sometimes called a register. + +**Posting**: A single debit or credit line of a transaction. A posting + comprises an account and the debit or credit amount. It also inherits the + shared description and date from the transaction. In the Ledger software, + a posting may also have metadata and an account state. + + +**Report**: A summary made from a journal of transactions. Each transaction + affects accounts and those effects are collated and totaled. The two most + common reports are the balance sheet, which shows what is owned and owed on + a specific date, and the cash flow statement, which shows how money was + earned and spent over a period. The cash flow statement is also called + a profit and loss statement or an income statement. + +**Transaction**: Our financial lives are recorded as a series of transactions. + Each transaction has a specific date, an equal total of debits and credits + affecting accounts, and some sort of description. For example, "On January + 1, pay $100 with check #243 from Checking to Utilities for my Verizon phone + bill" is a transaction. A credit of $100 decreases my Checking asset, while + a balancing debit of $100 increases my Utility expense. A transaction needs + at least two *postings*, meaning account debits or credits, but can be as + complicated as humans can make finances. + +LEDGER GLOSSARY +--- + +The Ledger software also has its own terms. + +**Automated Transaction**: a command directive that modifies subsequent + transactions that match an expression. An automated transaction can add + additional postings to a transaction, add metadata, or change transaction + amounts. Reports can be filter postings modified or generated by an + automated transaction. + [§ Automated Transactions](http://www.ledger-cli.org/3.0/doc/ledger3.html#Automated-Transactions); + [§ Concrete Example of Automated Transactions](http://www.ledger-cli.org/3.0/doc/ledger3.html#Concrete-Example-of-Automated-Transactions) + +**Command Directive**: a command in a journal file to change how subsequent + lines and transactions in a journal file are processed. Command directives + control processing, set default values for subsequent accounts and + transactions, or override parts of subsequent transactions. A directive + line begins with name of the directive and may have additional arguments or + additional indented lines. The single letters *AbCDhIiNOoY* are aliased to + other command directives, providing compatibility with the ancient past. + The characters **'='** and **'-'** are command directives for a automatic + transactions and periodic transactions, respectively. + [§ Command Directives](http://www.ledger-cli.org/3.0/doc/ledger3.html#Command-Directives) + +**Commodity**: any currency, stock, time or resource to be tracked + numerically. While many people only track money in Ledger, Ledger can track + different resources and manage rules to convert between them. The system is + flexible enough for the needs of very different users. Some track billable + time, converting minutes and hours into dollars. Others track multiple + currencies. Still others track the purchase and sale of stocks. Each + commodity is separate unless a conversion rule is given. + [§ Commodities and Currencies](http://www.ledger-cli.org/3.0/doc/ledger3.html#Commodities-and-Currencies); + [§ Currencies and Commodities](http://www.ledger-cli.org/3.0/doc/ledger3.html#Currency-and-Commodities); + [§ Accounts and Inventories](http://www.ledger-cli.org/3.0/doc/ledger3.html#Accounts-and-Inventories); + [§ Posting Cost](http://www.ledger-cli.org/3.0/doc/ledger3.html#Posting-cost) + *(and next ten sections)*; + [§ Commodity Reporting](http://www.ledger-cli.org/3.0/doc/ledger3.html#Commodity-Reporting) + +**Effective Date**: an optional, second date information item in for a posting + or transaction. Some use the effective date for when work is billed or when + a check has cleared. The `--effective-date` option causes the effective + date to override the transaction's initial date for that report. + [§ Effective Dates](http://www.ledger-cli.org/3.0/doc/ledger3.html#Effective-Dates); + +**Journal File**: the text input file for ledger, sometimes called a register + file. A journal file is a series of transactions, command directives, and + comments. Command directives start with the single word name of the + directive at the beginning of the line and include any following indented + lines. Transactions start with a date a the beginning of the line and + include any indented lines following. The journal file is expected to be + encoded as ASCII or UTF-8 text. + +**Periodic Transaction**: the estimate of a transaction that would occur + periodically, e.g., a monthly expense. These estimates are only used in + budgeting and forecasting reports using the `--budget`, `--forecast`, or + `--unbudgeted` options. + [§ Budgeting and Forecasting](http://www.ledger-cli.org/3.0/doc/ledger3.html#Budgeting-and-Forecasting) + +**Transaction Code**: an optional item in a transaction or posting often used + to record a check number or bank code. Certain custom reports can report + this code. + [§ Codes](http://www.ledger-cli.org/3.0/doc/ledger3.html#Codes); + [§ Format Expressions](http://www.ledger-cli.org/3.0/doc/ledger3.html#Format-Expressions) + +**Transaction Metadata**: a term for comments and tags annotating + a transaction. Comments indented with a transaction will be stored with + each posting of a transaction. Tags are words in comments followed by + colons. Tags can be used as filters in reports and certain tags, "Payee" or + "Value", may affect fields of the transaction. + [§ Metadata](http://www.ledger-cli.org/3.0/doc/ledger3.html#Metadata), + [§ Applying Metadata to every matched posting](http://www.ledger-cli.org/3.0/doc/ledger3.html#Applying-metadata-to-every-matched-posting), + [§ Applying Metadata to the generated posting](http://www.ledger-cli.org/3.0/doc/ledger3.html#Applying-metadata-to-the-generated-posting) + +**Transaction State**: a state of *cleared*, *pending*, or *uncleared* on each + posting. The state is usually set for an entire transaction at once with + a mark after the date. The marks are ***** (cleared), **!** (pending), + or no mark (uncleared). The interpretation of this state is up to the user, + but is typically used in bank reconciliations or differentiating time worked + versus billed. Ledger supports reports and filters based on state. + [§ Transaction State](http://www.ledger-cli.org/3.0/doc/ledger3.html#Transaction-state); + [§ Cleared Report]( http://www.ledger-cli.org/3.0/doc/ledger3.html#Cleared-Report) + +**Virtual Posting**: an annotation posting in a transaction, similar in form + as a regular posting but not required to balance debits and credits. It is + often used to support + [Fund Accounting](http://en.wikipedia.org/wiki/Fund_accounting) and various + reports will collate and summarize virtual postings. Virtual postings + should not be confused with virtual posting costs. + [§ Virtual Postings](http://www.ledger-cli.org/3.0/doc/ledger3.html#Virtual-postings) + [§ Working with Multiple Funds and Accounts](http://www.ledger-cli.org/3.0/doc/ledger3.html#Working-with-multiple-funds-and-accounts) diff --git a/doc/NEWS.md b/doc/NEWS.md new file mode 100644 index 00000000..76f4d983 --- /dev/null +++ b/doc/NEWS.md @@ -0,0 +1,1045 @@ +# Ledger NEWS + +## 3.1.3 (2019-03-31) + +- Properly reject postings with a comment right after the flag (bug #1753) + +- Make sorting order of lot information deterministic (bug #1747) + +- Fix bug in tag value parsing (bug #1702) + +- Remove the `org` command, which was always a hack to begin with (bug #1706) + +- Provide Docker information in README + +- Various small documentation improvements + +## 3.1.2 (2019-02-05) + +- Increase maximum length for regex from 255 to 4095 (bug #981) + +- Initialize periods from from/since clause rather than earliest + transaction date (bug #1159) + +- Check balance assertions against the amount after the posting (bug #1147) + +- Allow balance assertions with multiple posts to same account (bug #1187) + +- Fix period duration of "every X days" and similar statements (bug #370) + +- Make option `--force-color` not require `--color` anymore (bug #1109) + +- Add `quoted_rfc4180` to allow CVS output with RFC 4180 compliant quoting. + +- Add support for `--prepend-format` in accounts command + +- Fix handling of edge cases in trim function (bug #520) + +- Fix auto xact posts not getting applied to account total during + journal parse (bug #552) + +- Transfer `null_post` flags to generated postings + +- Fix segfault when using `--market` with `--group-by` + +- Use `amount_width` variable for budget report + +- Keep pending items in budgets until the last day they apply + +- Fix bug where `.total` used in value expressions breaks totals + +- Make automated transactions work with assertions (bug #1127) + +- Improve parsing of date tokens (bug #1626) + +- Don't attempt to invert a value if it's already zero (bug #1703) + +- Do not parse user-specified init-file twice + +- Fix parsing issue of effective dates (bug #1722, TALOS-2017-0303, + CVE-2017-2807) + +- Fix use-after-free issue with deferred postings (bug #1723, TALOS-2017-0304, + CVE-2017-2808) + +- Fix possible stack overflow in option parsing routine (bug #1222, + CVE-2017-12481) + +- Fix possible stack overflow in date parsing routine (bug #1224, + CVE-2017-12482) + +- Fix use-after-free when using `--gain` (bug #541) + +- Python: Removed double quotes from Unicode values. + +- Python: Ensure that parse errors produce useful `RuntimeErrors` + +- Python: Expose `journal expand_aliases` + +- Python: Expose `journal_t::register_account` + +- Improve bash completion + +- Emacs Lisp files have been moved to https://github.com/ledger/ledger-mode + +- Fix build under MSYS (32-bit). + +- Fix build under Cygwin. + +- Various documentation improvements + +## 3.1.1 (2016-01-11) + +- Added a `--no-revalued` option + +- Improved Embedded Python Support + +- Use `./.ledgerrc` if `~/.ledgerrc` doesn't exist + +- Fixed parsing of transactions with single-character payees and comments + +- Fixed crash when using `-M` with empty result + +- Fixed sorting for option `--auto-match` + +- Fixed treatment of `year 2015` and `Y2014` directives + +- Fixed crash when using `--trace` 10 or above + +- Build fix for boost 1.58, 1.59, 1.60 + +- Build fix for Cygwin + +- Fixed Util and Math tests on Mac OS X + +- Various documentation improvements + +- Examples in the documentation are tested just like unit tests + +- Add continuous integration (https://travis-ci.org/ledger/ledger) + +## 3.1 (2014-10-05) + +- Changed the definition of cost basis to preserve the original cost basis + when a gain or loss is made (if you bought 1 AAA for $10 and then sold + it for $12, ledger would previously take $12 as the cost; the original + cost of $10 is preserved as the cost basis now, which addresses strange + behavior with -B after a capital gain or loss is made). + +- Incorrect automatic Equity:Capital Gains and Equity:Capital Loss entries + are no longer generated when a commodity is sold for loss or profit. + +- Support for virtual posting costs. + +- The option `--permissive` now quiets balance assertions + +- Removed SHA1 files due to license issues and use boost instead. + +- Added option `--no-pager` to disable the pager. + +- Added option `--no-aliases` to completely disable alias expansion + +- Added option `--recursive-aliases` to expand aliases recursively + +- Support payee `uuid` directive. + +- Bug fix: when a status flag (`!` or `*`) is explicitly specified for an + individual posting, it always has a priority over entire transaction + status. + +- Bug fix: don't lose commodity when cost is not separated by whitespace + +- Improved backwards compatibility with ledger 2.x + +- Build fix for GCC 4.9 + +- Build fix for boost 1.56 + +- Many improvements to ledger-mode, including fontification + +- More test cases and unit tests + +- Contrib: Added script to generate commodities from ISO 4217 + +## 3.0 + +Due to the magnitude of changes in 3.0, only changes that affect compatibility +with 2.x files and usage is mentioned here. For a description of new +features, please see the manual. + +- The option `-g` (`--performance`) was removed. + +- The balance report now defaults to showing all relevant accounts. This is + the opposite of 2.x. That is, `bal` in 3.0 does what `-s bal` did in 2.x. + To see 2.6 behavior, use `bal -n` in 3.0. The `-s` option no longer has any + effect on balance reports. + +## 2.6.3 + +- Minor fixes to allow for compilation with gcc 4.4. + +## 2.6.2 + +- Bug fix: Command-line options, such as -O, now override init-file options + such as -V. + +- Bug fix: "cat data | ledger -f -" now works. + +- Bug fix: --no-cache is now honored. Previously, it was writing out a cache + file named "<none>". + +- Bug fix: Using %.2X in a format string now outputs 2 spaces if the state is + cleared. + +## 2.6.1 + +- Added the concept of "balance setting transactions": + + Setting an account's balance + + You can now manually set an account's balance to whatever you want, at + any time. Here's how it might look at the beginning of your Ledger + file: + + 2008/07/27 Starting fresh + Assets:Checking = $1,000.00 + Equity:Opening Balances + + If Assets:Checking is empty, this is no different from omitting the + "=". However, if Assets:Checking did have a prior balance, the amount + of the transaction will be auto-calculated so that the final balance + of Assets:Checking is now $1,000.00. + + Let me give an example of this. Say you have this: + + 2008/07/27 Starting fresh + Assets:Checking $750.00 + Equity:Opening Balances + + 2008/07/27 Starting fresh + Assets:Checking = $1,000.00 + Equity:Adjustments + + These two entries are exactly equivalent to these two: + + 2008/07/27 Starting fresh + Assets:Checking $750.00 + Equity:Opening Balances + + 2008/07/27 Starting fresh + Assets:Checking $250.00 + Equity:Adjustments + + The use of the "=" sign here is that it sets the transaction's amount + to whatever is required to satisfy the assignment. This is the + behavior if the transaction's amount is left empty. + + # Multiple commodities + + As far as commodities go, the = sign only works if the account + balance's commodity matches the commodity of the amount after the + equals sign. However, if the account has multiple commodities, only + the matching commodity is affected. Here's what I mean: + + 2008/07/24 Opening Balance + Assets:Checking = $250.00 ; we force set it + Equity:Opening Balances + + 2008/07/24 Opening Balance + Assets:Checking = EC 250.00 ; we force set it again + Equity:Opening Balances + + This is an error, because $250.00 cannot be auto-balanced to match EC + 250.00. However: + + 2008/07/24 Opening Balance + Assets:Checking = $250.00 ; we force set it again + Assets:Checking EC 100.00 ; and add some EC's + Equity:Opening Balances + + 2008/07/24 Opening Balance + Assets:Checking = EC 250.00 ; we force set the EC's + Equity:Opening Balances + + This is *not* an error, because the latter auto-balancing transaction + only affects the EC 100.00 part of the account's balance; the $250.00 + part is left alone. + + Checking statement balances + + When you reconcile a statement, there are typically one or more + transactions which result in a known balance. Here's how you specify + that in your Ledger data: + + 2008/07/24 Opening Balance + Assets:Checking = $100.00 + Equity:Opening Balances + + 2008/07/30 We spend money, with a known balance afterward + Expenses:Food $20.00 + Assets:Checking = $80.00 + + 2008/07/30 Again we spend money, but this time with all the info + Expenses:Food $20.00 + Assets:Checking $-20.00 = $60.00 + + 2008/07/30 This entry yield an 'unbalanced' error + Expenses:Food $20.00 + Assets:Checking $-20.00 = $30.00 + + The last entry in this set fails to balance with an unbalanced + remainder of $-10.00. Either the entry must be corrected, or you can + have Ledger deal with the remainder automatically: + + 2008/07/30 The fixed entry + Expenses:Food $20.00 + Assets:Checking $-20.00 = $30.00 + Equity:Adjustments + + Conclusion + + This simple feature has all the utility of @check, plus auto-balancing + to match known target balances, plus the ability to guarantee that an + account which uses only one commodity does contain only that + commodity. + + This feature slows down textual parsing slightly, does not affect + speed when loading from the binary cache. + +- The rest of the changes in the version is all bug fixes (around 45 of + them). + +## 2.6.0.90 + +- Gnucash parser is fixed. + +- Fix a memory leak bug in the amount parser. + +- (This feature is from 2.6, but was not documented anywhere): + + Commodities may now specify lot details, to assign in managing set + groups of items, like buying and selling shares of stock. + + For example, let's say you buy 50 shares of AAPL at $10 a share: + + 2007/01/14 Stock purchase + Assets:Brokerage 50 AAPL @ $10 + Assets:Brokerage + + Three months later, you sell this "lot". Based on the original + purchase information, Ledger remembers how much each share was + purchased for, and the date on which it was purchased. This means + you can sell this specific lot by price, by date, or by both. Let's + sell it by price, this time for $20 a share. + + 2007/04/14 Stock purchase + Assets:Brokerage $1000.00 + Assets:Brokerage -50 AAPL {$10} @ $20 + Income:Capital Gains $-500.00 + + Note that the Income:Capital Gains line is now required to balance + the transaction. Because you sold 50 AAPL at $20/share, and because + you are selling shares that were originally valued at $10/share, + Ledger needs to know how you will "balance" this difference. An + equivalent Expenses:Capital Loss would be needed if the selling + price were less than the buying price. + + Here's the same example, this time selling by date and price: + + 2007/04/14 Stock purchase + Assets:Brokerage $1000.00 + Assets:Brokerage -50 AAPL {$10} [2007/01/14] @ $20 + Income:Capital Gains $-500.00 + + If you attempt to sell shares for a date you did not buy them, note + that Ledger will not complain (as it never complains about the + movement of commodities between accounts). In this case, it will + simply create a negative balance for such shares within your + Brokerage account; it's up to you to determine whether you have them + or not. + +- To facilitate lot pricing reports, there are some new reporting + options: + + * --lot-prices Report commodities with different lot prices as if + they were different commodities. Otherwise, Ledger + just gloms all the AAPL shares together. + + * --lot-dates Separate commodities by lot date. Every + transaction that uses the '@' cost specifier will + have an implicit lot date and lot price. + + * --lot-tags Separate commodities by their arbitrary note tag. + Note tags may be specified using (note) after the + commodity. + + * --lots Separate commodities using all lot information. + +## 2.6 + +- The style for eliding long account names (for example, in the + register report) has been changed. Previously Ledger would elide + the end of long names, replacing the excess length with "..". + However, in some cases this caused the base account name to be + missing from the report! + + What Ledger now does is that if an account name is too long, it will + start abbreviating the first parts of the account name down to two + letters in length. If this results in a string that is still too + long, the front will be elided -- not the end. For example: + + Expenses:Cash ; OK, not too long + Ex:Wednesday:Cash ; "Expenses" was abbreviated to fit + Ex:We:Afternoon:Cash ; "Expenses" and "Wednesday" abbreviated + ; Expenses:Wednesday:Afternoon:Lunch:Snack:Candy:Chocolate:Cash + ..:Af:Lu:Sn:Ca:Ch:Cash ; Abbreviated and elided! + + As you can see, it now takes a very deep account name before any + elision will occur, whereas in 2.x elisions were fairly common. + +- In addition to the new elision change mentioned above, the style is + also configurable: + + * --truncate leading ; elide at the beginning + * --truncate middle ; elide in the middle + * --truncate trailing ; elide at end (Ledger 2.x's behavior) + * --truncate abbrev ; the new behavior + + * --abbrev-len 2 ; set length of abbreviations + + These elision styles affect all format strings which have a maximum + width, so they will also affect the payee in a register report, for + example. In the case of non-account names, "abbrev" is equivalent + to "trailing", even though it elides at the beginning for long + account names. + +- Error reporting has been greatly improving, now showing full + contextual information for most error messages. + +- Added --base reporting option, for reporting convertible commodities + in their most basic form. For example, if you read a timeclock file + with Ledger, the time values are reported as hour and minutes -- + whichever is the most compact form. But with --base, Ledger reports + only in seconds. + + NOTE: Setting up convertible commodities is easy; here's how to use + Ledger for tracking quantities of data, where the most compact form + is reported (unless --base is specified): + + C 1.00 Kb = 1024 b + C 1.00 Mb = 1024 Kb + C 1.00 Gb = 1024 Mb + C 1.00 Tb = 1024 Gb + +- Added --ansi reporting option, which shows negative values in the + running total column of the register report as red, using ANSI + terminal codes; --ansi-invert makes non-negative values red (which + makes more sense for the income and budget reports). + + The --ansi functionality is triggered by the format modifier "!", + for example the register reports uses the following for the total + (last) column: + + %!12.80T + + At the moment neither the balance report nor any of the other + reports make use of the ! modifier, and so will not change color + even if --ansi is used. However, you can modify these report format + strings yourself in ~/.ledgerrc if you wish to see red coloring of + negative sums in other places. + +- Added --only predicate, which occurs during transaction processing + between --limit and --display. Here is a summary of how the three + supported predicates are used: + + * --limit "a>100" + + This flag limits computation to *only transactions whose amount + is greater than 100 of a given commodity*. It means that if you + scan your dining expenses, for example, only individual bills + greater than $100 would be calculated by the report. + + * --only "a>100" + + This flag happens much later than --limit, and corresponding + more directly to what one normally expects. If --limit isn't + used, then ALL your dining expenses contribute to the report, + *but only those calculated transactions whose value is greater + than $100 are used*. This becomes important when doing a + monthly costs report, for example, because it makes the + following command possible: + + ledger -M --only "a>100" reg ^Expenses:Food + + This shows only *months* whose amount is greater than 100. If + --limit had been used, it would have been a monthly summary of + all individual dinner bills greater than 100 -- which is a very + different thing. + + * --display "a>100" + + This predicate does not constrain calculation, but only display. + Consider the same command as above: + + ledger -M --display "a>100" reg ^Expenses:Food + + This displays only lines whose amount is greater than 100, *yet + the running total still includes amounts from all transactions*. + This command has more particular application, such as showing + the current month's checking register while still giving a + correct ending balance: + + ledger --display "d>[this month]" reg Checking + + Note that these predicates can be combined. Here is a report that + considers only food bills whose individual cost is greater than + $20, but shows the monthly total only if it is greater than $500. + Finally, we only display the months of the last year, but we + retain an accurate running total with respect to the entire ledger + file: + + ledger -M --limit "a>20" --only "a>200" \ + --display "year == yearof([last year])" reg ^Expenses:Food + +- Added new "--descend AMOUNT" and "--descend-if VALEXPR" reporting + options. For any reports that display valued transactions (i.e., + register, print, etc), you can now descend into the component + transactions that made up any of the values you see. + + For example, say you request a --monthly expenses report: + + $ ledger --monthly register ^Expenses + + Now, in one of the reported months you see $500.00 spent on + Expenses:Food. You can ask Ledger to "descend" into, and show the + component transactions of, that $500.00 by respecifying the query + with the --descend option: + + $ ledger --monthly --descend "\$500.00" register ^Expenses + + The --descend-if option has the same effect, but takes a value + expression which is evaluated as a boolean to locate the desired + reported transaction. + +- Added a "dump" command for creating binary files, which load much + faster than their textual originals. For example: + + ledger -f huge.dat -o huge.cache dump + ledger -f huge.cache bal + + The second command will load significantly faster (usually about six + times on my machine). + +- There have a few changes to value expression syntax. The most + significant incompatibilities being: + + * Equality is now ==, not = + * The U, A, and S functions now requires parens around the argument. + Whereas before At was acceptable, now it must be specified as + A(t). + * The P function now always requires two arguments. The old + one-argument version P(x) is now the same as P(x,m). + + The following value expression features are new: + + * A C-like comma operator is supported, where all but the last term + are ignored. The is significant for the next feature: + * Function definitions are now supported. Scoping is governed + by parentheses. For example: + (x=100, x+10) ; yields 110 as the result + (f(x)=x*2,f(100)) ; yields 200 as the result + * Identifier names may be any length. Along with this support comes + alternate, longer names for all of the current one-letter value + expression variables: + + Old New + --- --- + m now + a amount + a amount + b cost + i price + d date + X cleared + Y pending + R real + L actual + n index + N count + l depth + O total + B cost_total + I price_total + v market + V market_total + g gain + G gain_total + U(x) abs(x) + S(x) quant(x), quantity(x) + comm(x), commodity(x) + setcomm(x,y), set_commodity(x,y) + A(x) mean(x), avg(x), average(x) + P(x,y) val(x,y), value(x,y) + min(x,y) + max(x,y) + +- There are new "parse" and "expr" commands, whose argument is a + single value expression. Ledger will simply print out the result of + evaluating it. "parse" happens before parsing your ledger file, + while "expr" happens afterward. Although "expr" is slower as a + result, any commodities you use will be formatted based on patterns + of usage seen in your ledger file. + + These commands can be used to test value expressions, or for doing + calculation of commoditized amounts from a script. + + A new "--debug" will also dump the resulting parse tree, useful for + submitting bug reports. + +- Added new min(x,y) and max(x,y) value expression functions. + +- Value expression function may now be defined within your ledger file + (or initialization file) using the following syntax: + + @def foo(x)=x*1000 + + This line makes the function "foo" available to all subsequent value + expressions, to all command-line options taking a value expression, + and to the new "expr" command (see above). + +## 2.5 + +- Added a new value expression regexp command: + C// compare against a transaction amount's commodity symbol + +- Added a new "csv" command, for outputting results in CSV format. + +- Ledger now expands ~ in file pathnames specified in environment + variables, initialization files and journal files. + +- Effective dates may now be specified for entries: + + 2004/10/03=2004/09/30 Credit card company + Liabilities:MasterCard $100.00 + Assets:Checking + + This entry says that although the actual transactions occurred on + October 3rd, their effective date was September 30th. This is + especially useful for budgeting, in case you want the transactions + to show up in September instead of October. + + To report using effective dates, use the --effective option. + +- Actual and effective dates may now be specified for individual + transactions: + + 2004/10/03=2004/09/30 Credit card company + Liabilities:MasterCard $100.00 + Assets:Checking ; [2004/10/10=2004/09/15] + + This states that although the actual date of the entry is + 2004/10/03, and the effective date of the entry is 2004/09/30, the + actual date of the Checking transaction itself is 2004/10/10, and + its effective date is 2004/09/15. The effective date is optional + (just specifying the actual date would have read "[2004/10/10]"). + + If no effective date is given for a transaction, the effective date + of the entry is assumed. If no actual date is given, the actual + date of the entry is assumed. The syntax of the latter is simply + [=2004/09/15]. + +- To support the above, there is a new formatting option: "%d". This + outputs only the date (like "%D") if there is no effective date, but + outputs "ADATE=EDATE" if there is one. The "print" report now uses + this. + +- To support the above, the register report may now split up entries + whose component transactions have different dates. For example, + given the following entry: + + 2005/10/15=2005/09/01 iTunes + Expenses:Music $1.08 ; [2005/10/20=2005/08/01] + Liabilities:MasterCard + + The command "ledger register" on this data file reports: + + 2005/10/20 iTunes Expenses:Music $1.08 $1.08 + 2005/10/15 iTunes Liabilities:MasterCard $-1.08 0 + + While the command "ledger --effective register" reports: + + 2005/08/01 iTunes Expenses:Music $1.08 $1.08 + 2005/09/01 iTunes Liabilities:MasterCard $-1.08 0 + + Although it appears as though two entries are being reported, both + transactions belong to the same entry. + +- Individual transactions may now be cleared separately. The old + syntax, which is still supported, clears all transactions in an + entry: + + 2004/05/27 * Book Store + Expenses:Dining $20.00 + Liabilities:MasterCard + + The new syntax allows clearing of just the MasterCard transaction: + + 2004/05/27 Book Store + Expenses:Dining $20.00 + * Liabilities:MasterCard + + NOTE: This changes the output format of both the "emacs" and "xml" + reports. ledger.el uses the new syntax unless the Lisp variable + `ledger-clear-whole-entries' is set to t. + +- Removed Python integration support. + +- Did much internal restructuring to allow the use of libledger.so in + non-command-line environments (such as GUI tools). + +## 2.4.1 + +- Corrected an issue that had inadvertently disabled Gnucash support. + +## 2.4 + +- Both `-$100.00` and `$-100.00` are now equivalent amounts. + +- Simple, inline math (using the operators +-/*, and/or parentheses) + is supported in transactions. For example: + + 2004/05/27 Book Store + Expenses:Dining $20.00 + $2.50 + Liabilities:MasterCard + + This won't register the tax/tip in its own account, but might make + later reading of the ledger file easier. + +- Use of a "catch all" account is now possible, which auto-balances + entries that contain _only one transaction_. For sanity's sake this + is not used to balance all entries, as that would make locating + unbalanced entries a nightmare. Example: + + A Liabilities:MasterCard + + 2004/05/27 Book Store + Expenses:Dining $20.00 + $2.50 + + This is equivalent to the entry in the previous bullet. + +- Entries that contain a single transaction with no amount now always + balance, even if multiple commodities are involved. This means that + the following is now supported, which wasn't previously: + + 2004/06/21 Adjustment + Retirement 100 FUNDA + Retirement 200 FUNDB + Retirement 300 FUNDC + Equity:Adjustments + +- Fixed several bugs relating to QIF parsing, budgeting and + forecasting. + +- The configure process now looks for libexpat in addition to + searching for libxmlparse+libxmltok (how expat used to be packaged). + +## 2.3 + +- The directive "!alias ALIAS = ACCOUNT" makes it possible to use + "ALIAS" as an alternative name for ACCOUNT in a textual ledger file. + You might use this to associate the single word "Bank" with the + checking account you use most, for example. + +- The --version page shows the optional modules ledger was built with. + +- Fixed several minor problems, plus a few major ones dealing with + imprecise date parsing. + +## 2.2 + +- Ledger now compiles under gcc 2.95. + +- Fixed several core engine bugs, and problems with Ledger's XML data + format. + +- Erros in XML or Gnucash data now report the correct line number for + the error, instead of always showing line 1. + +- 'configure' has been changed to always use a combination of both + compile and link tests for every feature, in order to identify + environment problems right away. + +- The "D <COMM>" command, released in 2.1, now requires a commoditized + amount, such as "D $1,000.00". This sets not only the default + commodity, but several flags to be used with all such commodities + (such as whether numbering should be American or European by + default). This entry may be used be many times; the most recent + seen specifies the default for entries that follow. + +- The binary cache now remembers the price history database that was + used, so that if LEDGER_PRICE_DB is silently changed, the cache will + be thrown away and rebuilt. + +- OFX data importing is now supported, using libofx + (http://libofx.sourceforge.net). configure will check if the + library is available. You may need to add CPPFLAGS or LDFLAGS to + the command-line for the appropriate headers and library to be + found. This support is preliminary, and as such is not documented + yet. + +- All journal entries now remember where they were read from. New + format codes to access this information are: %S for source path, %B + for beginning character position, and %E for ending character + position. + +- Added "pricesdb" command, which is identical to "prices" except that + it uses the same format as Ledger's usual price history database. + +- Added "output FILE" command, which attempts to reproduce the input + journal FILE exactly. Meant for future GUI usage. This command + relies on --write-hdr-format and --write-xact-format, instead of + --print-format. + +- Added "--reconcile BALANCE" option, which attempts to reconcile all + matching transactions to the given BALANCE, outputting those that + would need to be "cleared" to match it. Using by the + auto-reconciling feature of ledger.el (see below). + + "--reconcile-date DATE" ignores any uncleared transactions after + DATE in the reconciling algorithm. Since the algorithm is O(n^2) + (where 'n' is the number of uncleared transactions to consider), + this could have a substantial impact. + +- In ledger.el's *Reconcile* mode ('C-c C-r' from a ledger-mode file): + + * 'a' adds a missing transaction + * 'd' deletes the current transaction + * 'r' attempts to auto-reconcile (same as 'C-u C-c C-r') + * 's' or 'C-x C-s' will save the ledger data file and show the + currently cleared balance + * 'C-c C-c' commits the pending transactions, marking them cleared. + + This feature now works with Emacs 21.3. + Also, the reconciler no longer needs to ask "how far back" to go. + +- To support the reconciler, textual entries may now have a "!" flag + (pending) after the date, instead of a "*" flag (cleared). + +- There are a new set of value expression regexp commands: + * c// entry code + * p// payee + * w// short account name + * W// full account name + * e// transaction note + + This makes it possible to display transactions whose comment field + matches a particular text string. For example: + + ledger -l e/{tax}/ reg + + prints out all the transactions with the comment "{tax}", which + might be used to identify items related to a tax report. + +## 2.1 + +- Improved the autoconf system to be smarter about finding XML libs + +- Added --no-cache option, to always ignore any binary cache file + +- `ledger-reconcile' (in ledger.el) no longer asks for a number of days + +- Fixed %.XY format, where X is shorter than the string generated by Y + +- New directive for text files: "D <COMM>" specifies the default commodity + used by the entry command + +## 2.0 + +This version represents a full rewrite, while preserving much of the +original data format and command-line syntax. There are too many new +features to describe in full, but a quick list: value expressions, +complex date masks, binary caching of ledger data, several new +reporting options, a simple way to specify payee regexps, calculation +and display predicates, and two-way Python integration. Ledger also +uses autoconf now, and builds as a library in addition to a +command-line driver. + +### Differences from 1.7 + +- changes in option syntax: + + -d now specifies the display predicate. To give a date mask similar + to 1.7, use the -p (period) option. + + -P now generates the "by payee" report. To specify a price database + to use, use --price-db. + + -G now generates a net gain report. To print totals in a format + consumable by gnuplot, use -J. + + -l now specifies the calculation predicate. To emulate the old + usage of "-l \$100", use: -d "AT>100". + + -N is gone. Instead of "-N REGEX", use: -d "/REGEX/?T>0:T". + + -F now specifies the report format string. The old meaning of -F + now has little use. + + -S now takes a value expression as the sorting criterion. To get + the old meaning of "-S", use "-S d". + + -n now means "collapse entries in the register report". The get the + old meaning of -n in the balance report, use "-T a". + + -p now specifies the reporting period. You can convert commodities + in a report using value expressions. For example, to display hours + at $10 per hour: + + -T "O>={0.01h}?{\$10.00}*O:O" + + Or, to reduce totals, so that every $417 becomes 1.0 AU: + + -T "O>={\$0.01}?{1.0 AU}*(O/{\$417}):O" + +- The use of "+" and "-" in ledger files to specify permanent regexps + has been removed. + +- The "-from" argument is no longer used by the "entry" command. + Simply remove it. + +### Features new to 2.0 + +- The most significant feature to be added is "value expressions". + They are used in many places to indicate what to display, sorting + order, how to calculate totals, etc. Logic and math operators are + supported, as well as simple functions. See the manual. + +- If the environment variable LEDGER_FILE (or LEDGER) is used, a + binary cache of that ledger is kept in ~/.ledger-cache (or the file + given by LEDGER_CACHE). This greatly speeds up subsequent queries. + Happens only if "-f" or "--file" is not used. + +- New 'xml' report outputs an XML version of what "register" would + have displayed. This can be used to manipulate reported data in a + more scriptable way. + + Ledger can also read as input the output from the "xml" report. If + the "xml" report did not contain balanced entries, they will be + balanced by the "<Unknown>" account. For example: + + ledger reg rent + + displays the same results as: + + ledger xml rent | ledger -f - reg rent + +- Regexps given directly after the command name now apply only to + account names. To match on a payee, use "--" to separate the two + kinds of regexps. For example, to find a payee named "John" within + all Expenses accounts, use: + + ledger register expenses -- john + + Note: This command is identical (and internally converted) to: + + ledger -l "/expenses/|//john/" register + +- To include entries from another file into a specific account, use: + + !account ACCOUNT + !include FILE + !end + +- Register reports now show only matching account transactions. Use + "-r" to see "related accounts" -- the account the transfer came from + or went to (This was the old behavior in 1.x, but led to confusion). + "-r" also works with balance reports, where it will total all the + transactions related to your query. + +- Automated transactions now use value expressions for the predicate. + The new syntax is: + + = VALUE-EXPR + TRANSACTIONS... + + Only one VALUE-EXPR is supported (compared to multiple account + regexps before). However, since value expression allow for logic + chaining, there is no loss of functionality. Matching can also be + much more comprehensive. + +- If Boost.Python is installed (libboost_python.a), ledger can support + two-way Python integration. This feature is enabled by passing + --enable-python to the "configure" script before building. Ledger + can then be used as a module (ledger.so), as well as supporting + Python function calls directly from value expressions. See main.py + for an example of driving Ledger from Python. It implements nearly + all the functionality of the C++ driver, main.cc. + + (This feature has yet to mature, and so is being offered as a beta + feature in this release. It is mostly functional, and those curious + are welcome to play with it.) + +- New reporting options: + + * "-o FILE" outputs data to FILE. If "-", output goes to stdout (the + default). + + * -O shows base commodity values (this is the old behavior) + * -B shows basis cost of commodities + * -V shows market value of commodities + * -g reports gain/loss performance of each register item + * -G reports net gain/loss over time + * -A reports average transaction value (arithmetic mean) + * -D reports each transaction's deviation from the average + + * -w uses 132 columns for the register report, rather than 80. Set + the environment variable LEDGER_WIDE for this to be the default. + + * "-p INTERVAL" allows for more flexible period reporting, such as: + + monthly + every week + every 3 quarters + weekly from 12/20 + monthly in 2003 + weekly from last month until dec + + * "-y DATEFMT" changes the date format used in all reports. The + default is "%Y/%m/%d". + + -Y and -W print yearly and weekly subtotals, just as -M prints + monthly subtotals. + + * --dow shows cumulative totals for each day of the week. + + * -P reports transactions grouped by payee + + * -x reports the payee as the commodity; useful in some cases + + * -j and -J replace the previous -G (gnuplot) option. -j reports the + amounts column in a way gnuplot can consume, and -J the totals + column. An example is in "scripts/report". + + * "--period-sort EXPR" sorts transactions within a reporting period. + The regular -S option sorts all reported transactions. + +## 1.7 + +- Pricing histories are now supported, so that ledger remembers the + historical prices of all commodities, and can present register + reports based on past and present market values as well as original + cost basis. See the manual for more details on the new option + switches. + +## 1.6 + +- Ledger 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 ledger files (the commodity + used is "h", for hours); it means that doing things like tracking + billable hours for clients, and invoicing those clients to transfer + hours into dollar values via a receivable account, is now trivial. + See the docs for more on how to do this. + +- Began keeping a NEWS file. :) diff --git a/doc/grammar.y b/doc/grammar.y new file mode 100644 index 00000000..9a5f740b --- /dev/null +++ b/doc/grammar.y @@ -0,0 +1,221 @@ +/** + * @file grammar.y + * @version 3.0 + * @author John Wiegley + * + * @brief Canonical BNF grammar for Ledger data files + * + * Extensions are permitted if: they are not required, and they are + * backwards-compatible with this grammar. + */ + +/* + * There are three special terminals in this grammar, which violate its + * context free nature: + * + * TEXT -- consumes all characters until the next terminal + * or EOL (end of line) + * WHITESPACE -- any amount of whitespace, not including EOL + * STRING -- characters up to the next WHITESPACE or EOL + * + * BIGINT -- a number of any width, matching [0-9]+ + * INT4 -- a four digit wide number + * INT2 -- a two digit wide number + * INT1 -- a one digit wide number + * + * Except for 1) the 'spacer' production (see below), 2) EOL, and 3) the + * WHITESPACE required to begin a posting, whitespace is otherwise + * ignored. + * + * Yes, this grammar is confusing and not so happy for machine readers, + * but it was designed for the human author and reader. Once parsed, + * the contents must be unambiguous, which means they can be output to + * more rigorous formats for other programs to consume. + */ + +/* + * Journals + * + * A journal is a file which primarily contains xacts, among other elements. + */ + +journal: + journal_item journal | + /* epsilon */ + ; + +journal_item: + whitespace + directive | + xact | + ; + +whitespace: + EOL | + WHITESPACE EOL | + ';' TEXT EOL | /* these next four are all ignored */ + '*' TEXT EOL | + ; + +directive: + '@' word_directive EOL | + '!' word_directive EOL | + word_directive EOL | + char_directive EOL + ; + +word_directive: + "include" TEXT | + "account" TEXT | + "end" | + "alias" STRING '=' TEXT | + "def" TEXT | + TEXT WHITESPACE TEXT /* looked up in session (aka maybe Python) */ + ; + +char_directive: + 'i' date time TEXT | /* a timeclock.el "check in" */ + 'I' date time TEXT | + 'o' date time TEXT | /* a timeclock.el "check out" */ + 'O' date time TEXT | + 'h' TEXT EOL | + 'b' TEXT EOL | + 'D' amount | /* sets display parameters for a commodity */ + 'A' TEXT | /* sets the "default balancing account" */ + 'C' commodity '=' amount | /* specifies a commodity conversion */ + 'P' date time commodity amount | /* a pricing history xact */ + 'N' commodity | /* commodity's price is never downloaded */ + 'Y' INT4 | /* sets the default year for date parsing */ + '-' '-' STRING TEXT | /* specify command-line options in the file */ + ; + +date: INT4 date_sep INT2 date_sep INT2 ; +date_opt: '=' date | /* epsilon */ ; +date_sep: '/' | '-' | '.' ; + +time: INT2 ':' INT2 ':' INT2 ; + +commodity: + '"' TEXT '"' | + STRING ; + +/* + * Xacts + * + * Xacts are the atomic units of accounting, which are composed of + * multiple postings between accounts, so long as it all balances in + * the end. + */ + +xact: plain_xact | + periodic_xact | + automated_xact ; + +plain_xact: + date date_opt status_opt code_opt FULLSTRING note_opt EOL + postings ; + +status_opt: status | /* epsilon */ ; +status: '*' | '!' | /* epsilon */ ; + +code_opt: code | /* epsilon */ ; +code: '(' TEXT ')' ; + +spacer: ' ' ' ' | '\t' | ' ' '\t' ; + +note_opt: spacer note | /* epsilon */ ; +note: ';' TEXT ; + +/* ---------------------------------------------------------------------- */ + +periodic_xact: + '~' period_expr note_opt EOL + posting postings ; + +/* + * A period expression has its own sub-grammar, which I don't quite have + * the time to exhaustively describe now. See datetime.cc. It allows + * for lots and lots of things, and is probably horribly ambiguous. + */ + +period_expr: FULLSTRING ; + +/* ---------------------------------------------------------------------- */ + +automated_xact: + '=' value_expr note_opt EOL + posting postings ; + +/* + * Value expressions are a algebraic math expressions very similar to + * XPath (minus the path traversal items). This grammar needs fleshing + * out also, since it's allowed in many places. + */ + +value_expr: FULLSTRING ; + +/* + * There is a serious ambiguity here which the parser resolves as + * follows: if an amount_expr can be parsed as an amount, it's an + * amount; otherwise, it's a value expression. + */ + +quantity: neg_opt BIGINT decimal_opt ; + +neg_opt: '-' | /* epsilon */ ; +decimal_opt: '.' BIGINT | /* epsilon */ ; + +annotation: lot_price_opt lot_date_opt lot_note_opt ; + +lot_date_opt: date | /* epsilon */ ; +lot_date: '[' date ']' ; + +lot_price_opt: price | /* epsilon */ ; +lot_price: '{' amount '}' ; + +lot_note_opt: note | /* epsilon */ ; +lot_note: '(' string ')' ; + +amount: + neg_opt commodity quantity annotation | + quantity commodity annotation ; + +amount_expr: amount | value_expr ; + +/* + * Postings + * + * Postings are the fundamental unit of accounting, and represent + * the movement of commodities to or from an account. Thus, paying off + * your credit card consists of two balancing postings: one that + * withdraws money from your checking account, and another which pays + * money to your credit institution. + */ + +postings: + posting postings | + /* epsilon */ + ; + +posting: + WHITESPACE status_opt account values_opt note_opt EOL; + +account_name: FULLSTRING ; + +values_opt: + spacer amount_expr price_opt | + /* epsilon */ + ; + +price_opt: price | /* epsilon */ ; +price: + '@' amount_expr | + '@@' amount_expr /* in this case, it's the whole price */ + ; + +account: + account_name | + '(' account_name ')' | + '[' account_name ']' ; + +/* grammar.y ends here */ diff --git a/doc/ledger.1 b/doc/ledger.1 new file mode 100644 index 00000000..e9b47315 --- /dev/null +++ b/doc/ledger.1 @@ -0,0 +1,1467 @@ +.Dd March 15, 2019 +.Dt LEDGER 1 +.Os +.Sh NAME +.Nm ledger +.Nd Command-line, double-entry account reporting tool +.Sh SYNOPSIS +.Nm +.Op Ar options +.Op Ar command +.Op Ar arguments +.Sh DESCRIPTION +.Nm +is a command-line accounting tool based on the power and completeness +of double-entry accounting. It is only a reporting tool, which means it never +modifies your data files, but it does offer a large selection of reports, and +different ways to customize them to your needs. +.Sh COMMANDS +.Nm +accepts several top-level commands, each of which generates a different +kind of basic report. Most of them accept a +.Ar report-query +argument, in order to determine what should be reported. To understand the +syntax of a +.Ar report-query , +see the section on +.Sx QUERIES . +In its most basic form, simply specifying one or more strings produces a +report for all accounts containing those strings. +.Pp +If no command is given, +.Nm +enters a +.Tn REPL , +or command loop, allowing several commands to be executed on the same +dataset without reparsing. +.Pp +The following is a complete list of accepted reporting commands: +.Bl -tag -width accounts +.It Ic accounts Oo Ar report-query Oc +List all accounts for postings that match the +.Ar report-query . +.El +.Bl -tag -width balance +.It Ic balance Oo Ar report-query Oc +Print a balance report showing totals for postings that match +.Ar report-query , +and aggregate totals for parents of those accounts. Options most commonly used +with this command are: +.Bl -tag -compact -width "--collapse (-n)" +.It Fl \-basis Pq Fl B +Report in terms of cost basis, not amount or value. This is the only form of +report which is guaranteed to always balance to zero, when no +.Ar report-query +is specified. +Only show totals for the top-most accounts. +.It Fl \-empty Pq Fl E +Show accounts whose total is zero. +.It Fl \-flat +Rather than display a hierarchical tree, flatten the report to show subtotals +for only accounts matching +.Ar report-query . +.It Fl \-no-total +Suppress the summary total shown at the bottom of the report. +.El +.Pp +The synonyms +.Ic bal +and +.Ic b +are also accepted. +.It Ic budget Oo Ar report-query Oc +A special balance report which includes three extra columns: the amount +budgeted during the reporting period, how spending differed from the budget, +and the percentage of budget spent (exceeds 100% if you go over budget). +Note that budgeting requires one or more +.Do +periodic transactions +.Dc +to be defined in your data file(s). See the manual for more information. +.It Ic cleared Oo Ar report-query Oc +A special balance report which adds two extra columns: the cleared balance for +each account, and the date of the most recent cleared posting in that account. +For this accounting to be meaningful, the cleared flag must be set on at least +one posting. See the manual for more information. +.It Ic commodities Oo Ar report-query Oc +List all commodities for postings matching the +.Ar report-query . +.It Ic convert +Reads data from a CSV (comma-separated values) file and generates +.Nm +transactions. +.It Ic csv Oo Ar report-query Oc +Report of postings matching the +.Ar report-query +in CSV format (comma-separated values). Useful for exporting data to a +spreadsheet for further analysis or charting. +.It Ic entry Oo Ar entry-template Oc +Generate and display a new, properly formatted +.Nm +transaction by comparing +the +.Ar entry-template +to the transactions in your data file(s). For more information on draft +templates and using this command to quickly create new transactions, see the +section +.Sx ENTRIES . +.Pp +The synonym +.Ic xact +is also accepted. +.It Ic emacs Oo Ar query Oc +Output posting and transaction data in a format readily consumed by the Emacs +editor, in a series of Lisp forms. This is used by the Emacs ledger-mode to +process reporting data from +.Nm . +.It Ic equity Oo Ar report-query Oc +Print a transaction with a series of postings that balance current totals for +accounts matching the +.Ar report-query +in a special account called +.Li Equity:Opening Balances . +The purpose of this report is to close the books for a prior year, while using +these equity postings to carry forward those balances. +.It Ic payees Oo Ar report-query Oc +List all payees for postings matching the +.Ar report-query . +.It Ic pricemap +Produce a file which can be used to generate a graph with graphviz showing +the relationship of commodities in the +.Nm +file. +.It Ic prices Oo Ar report-query Oc +Report prices for all commodities in postings matching the +.Ar report-query . +The prices are reported with the granularity of a single day. +.It Ic pricedb Oo Ar report-query Oc +Report prices for all commodities in postings matching the +.Ar report-query . +Prices are reported down to the second, using the same format as the +.Pa ~/.pricedb +file. +.It Ic print Oo Ar report-query Oc +Print out the full transactions of any matching postings using the same +format as they would appear in a data file. This can be used to extract +subsets from a +.Nm +file to transfer to other files. +.It Ic push Oo Ar options Oc +In the +.Tn REPL , +push a set of command-line +.Ar options , +so that they will apply to all subsequent reports. +.It Ic pop +In the +.Tn REPL , +pop any option settings that have been +.Sm off +.Ic push +ed. +.Sm on +.It Ic register Oo Ar report-query Oc +List all postings matching the +.Ar report-query . +This is one of the most common commands, and can be used to provide a variety +of useful reports. Options most commonly used +with this command are: +.Pp +.Bl -tag -compact -width "--collapse (-n)" +.It Fl \-average Pq Fl A +Show the running average, rather than a running total. +.It Fl \-current Pq Fl c +Don't show postings beyond the present day. +.It Fl \-exchange Ar commodity Pq Fl X +Render all values in the given +.Ar commodity , +if a price conversion rate can be determined. Rates are always displayed +relative to the date of the posting they are calculated for. This means a +.Ic register +report is a historical value report. For current values, it may be preferable +to use the +.Ic balance +report. +.It Fl \-gain Pq Fl G +Show any gains (or losses) in commodity values over time. +.It Fl \-head Ar number +Only show the top +.Ar number +postings. +.It Fl \-historical Pq Fl H +Value commodities at the time of their acquisition. +.It Fl \-invert +Invert the value of amounts shown. +.It Fl \-market Pq Fl V +Show current market values for all amounts. This is determined in a somewhat +magical fashion. It is probably more straightforward to use +.Fl \-exchange +option. +.It Fl \-period Ar time-period Pq Fl p +Show postings only for the given +.Ar time-period . +.It Fl \-related Pq Fl r +Show postings that are related to those that would have been shown. It has +the effect of displaying the +.Qq other side +of the postings. +.It Fl \-sort Ar value-expression Pq Fl S +Sort postings by evaluating the given +.Ar value-expression . +Note that a comma-separated list of expressions is allowed, in which case each +sorting term is used in order to determine the final ordering. For example, +to search by date and then amount, one would use: +.Dl ledger reg --sort 'date, amount' +The sort order may be controlled with the '-' sign. For example, to sort in +reverse chronological order: +.Dl ledger reg --sort '-date' +.It Fl \-tail Ar number +Only show the last +.Ar number +postings. +.It Fl \-uncleared Pq Fl U +Only show uncleared (i.e., recent) postings. +.El +.Pp +There are also several grouping options that can be useful: +.Pp +.Bl -tag -compact -width "--collapse (-n)" +.It Fl \-by-payee Pq Fl P +Group postings by common payee names. +.It Fl \-daily Pq Fl D +Group postings by day. +.It Fl \-weekly Pq Fl W +Group postings by week (starting on Sundays). +.It Fl \-start-of-week Ar day +Set the start of each report grouped by week to the given +.Ar day . +.It Fl \-monthly Pq Fl M +Group postings by month. +.It Fl \-quarterly +Group postings by fiscal quarter. +.It Fl \-yearly Pq Fl Y +Group postings by year. +.It Fl \-days-of-week +Group postings by the day of the week on which they took place. +.It Fl \-subtotal Pq Fl s +Group all postings together. This is very similar to the totals shown by the +.Ic balance +report. +.El +.Pp +The synonyms +.Ic reg +and +.Ic r +are also accepted. +.It Ic server +This command requires that Python support be active. If so, it starts up an +.Tn HTTP +server listening for requests on port 9000. This provides an alternate +interface to creating and viewing reports. Note that this is very much a +work-in-progress, and will not be fully functional until a later version. +.It Ic select Oo Ar sql-query Oc +List all postings matching the +.Ar sql-query . +This command allows to generate SQL-like queries, e.g.: +.Dl Li ledger select date,amount from posts where account=~/Income/ +.It Ic source +Parse a journal file and checks it for errors. +.Nm +will return success +if no errors are found. +.It Ic stats Oo Ar report-query Oc +Provide summary information about all the postings matching +.Ar report-query . +It provides information such as: +.Bl -bullet -offset indent -compact +.It +Time range of all matching postings +.It +Unique payees +.It +Unique accounts +.It +Postings total +.It +Uncleared postings +.It +Days since last posting +.It +Posts in the last 7 days +.It +Posts in the last 30 days +.It +Posts this month +.El +.It Ic xml Oo Ar report-query Oc +Output data relating to the current report in +.Tn XML +format. It includes all +accounts and commodities involved in the report, plus the postings and the +transactions they are contained in. See the manual for more information. +.El +.Sh OPTIONS +.Bl -tag -width -indent +.It Fl \-abbrev-len Ar INT +Set the minimum length an account can be abbreviated to if it doesn't +fit inside the +.Sy account-width . +If +.Ar INT +is zero, then the +account name will be truncated on the right. If +.Ar INT +is greater +than +.Sy account-width +then the account will be truncated on the +left, with no shortening of the account names in order to fit into the +desired width. +.It Fl \-account Ar EXPR +Prepend +.Ar EXPR +to all accounts reported. That is, the option +.Fl \-account Ar \*q'Personal'\*q +would tack +.Ar Personal: +and +.Fl \-account Ar \*qtag('VAT')\*q +would tack the value of the VAT tag to the beginning of every account +reported in a +.Ic balance +or +.Ic register +report. +.It Fl \-account-width Ar INT +Set the width of the account column in the +.Ic register +report +to +.Ar INT +characters. +.It Fl \-actual Pq Fl L +Report only real transactions, with no automated or virtual +transactions used. +.It Fl \-add-budget +Show only un-budgeted postings. +.It Fl \-amount Ar EXPR Pq Fl t +Apply the given value expression to the posting amount. Using +.Fl \-amount Ar EXPR +you can apply an +arbitrary transformation to the postings. +.It Fl \-amount-data Pq Fl j +On a register report print only the dates and amount of postings. +Useful for graphing and spreadsheet applications. +.It Fl \-amount-width Ar INT +Set the width in characters of the amount column in the +.Ic register +report. +.It Fl \-anon +Anonymize registry output, mostly for sending in bug reports. +.It Fl \-ansi +Use color if the terminal supports it. +Alias for +.Fl \-color +.It Fl \-args-only +Ignore init files and environment variables for the +.Nm +run. +.It Fl \-auto-match +When generating a ledger transaction from a CSV file using the +.Ic convert +command, automatically match an account from the Ledger journal. +.It Fl \-aux-date +Show auxiliary dates for all calculations. +Alias for +.Fl \-effective +.It Fl \-average Pq Fl A +Print average values over the number of transactions instead of +running totals. +.It Fl \-balance-format Ar FMT +Specify the format to use for the +.Ic balance +report. +.It Fl \-base +Reduce convertible commodities down the bottom of the conversion, e.g. +display time in seconds. +.It Fl \-basis Pq Fl B +Report the cost basis on all posting. +Alias for +.Fl \-cost +.It Fl \-begin Ar DATE Pq Fl b +Specify the start +.Ar DATE +of all calculations. Transactions before +that date will be ignored. +.It Fl \-bold-if Ar EXPR +Print the entire line in bold if the given value expression is true. +.It Fl \-budget +Only display budgeted items. In a +.Ic register +report this displays transaction in the budget, in a balance report this +displays accounts in the budget. +.It Fl \-budget-format Ar FMT +Specify the format to use for the +.Ic budget +report. +.It Fl \-by-payee Pq Fl P +Group postings in the register report by common payee names. +.It Fl \-check-payees +Enable strict and pedantic checking for payees as well as accounts, +commodities and tags. +.It Fl \-cleared Pq Fl C +Display only cleared postings. +.It Fl \-cleared-format Ar FMT +Specify the format to use for the +.Ic cleared +report +.It Fl \-collapse Pq Fl n +Print only the top level accounts. +.It Fl \-collapse-if-zero +Collapse the account display only if it has a zero balance. +.It Fl \-color +Use color if the terminal supports it. +Alias for +.Fl \-ansi +.It Fl \-columns Ar INT +Make the +.Ic register +report +.Ar INT +characters wide. By default +.Nm +will use all available columns in your terminal. +.It Fl \-cost +Report the cost basis on all posting. +Alias for +.Fl \-basis . +.It Fl \-count +Direct +.Nm +to report the number of items when appended to the +.Ic commodities , +.Ic accounts +or +.Ic payees +commands. +.It Fl \-csv-format Ar FMT +Format +.Ic csv +report +according to +.Ar FMT . +.It Fl \-current Pq Fl c +Shorthand for +.Fl \-limit Ar "'date <= today'" . +.It Fl \-daily Pq Fl D +Shorthand for +.Fl \-period Ar daily . +.It Fl \-date Ar EXPR +Transform the date of the transaction using +.Ar EXPR . +.It Fl \-date-format Ar DATEFMT Pq Fl y +Print dates using +.Ar DATEFMT . +Refer to +.Xr strftime 3 +for details on the format string syntax. +.It Fl \-datetime-format Ar DATETIMEFMT +Print datetimes using +.Ar DATETIMEFMT . +Refer to +.Xr strftime 3 +for details on the format string syntax. +.It Fl \-date-width Ar INT +Specify the width, in characters, of the date column in the +.Ic register +report. +.It Fl \-day-break +Break up +.Ic register +report of timelog entries that span multiple days by day. +.It Fl \-days-of-week +Group transactions by the days of the week. +Alias for +.Fl \-dow . +.It Fl \-dc +Display register or balance in debit/credit format If you use +.Fl \-dc +with either the +.Ic register +or +.Ic balance +commands, you will now get separate columns for debits and credits. +.It Fl \-debug Ar STR +If +.Nm +has been built with debug options this will provide extra +data during the run. +.It Fl \-decimal-comma +Direct +.Nm +to parse journals using the European standard comma as +decimal separator, vice a period. +.It Fl \-depth Ar INT +Limit the depth of the account tree. In a balance report, for example, +.Fl \-depth Ar 2 +will print balances only for accounts with two levels, i.e. +.Sy Expenses:Entertainment +but not +.Sy Expenses:Entertainment:Dining . +This is a display predicate, which means it only affects display, +not the total calculations. +.It Fl \-detail +Related to +.Ic convert +command. Synonym to +.Fl \-rich-data +option. +.It Fl \-deviation +Report each posting's deviation from the average. It is only meaningful +in the +.Ic register No and Ic prices +reports. +.It Fl \-display Ar EXPR Pq Fl d +Display lines that satisfy the expression +.Ar EXPR . +.It Fl \-display-amount Ar EXPR +Apply a transformation to the +.Em displayed +amount. This occurs after +calculations occur. +.It Fl \-display-total Ar EXPR +Apply a transformation to the +.Em displayed +total. This occurs after +calculations occur. +.It Fl \-dow +Group transactions by the days of the week. +Alias for +.Fl \-days-of-week . +.It Fl \-download +Cause quotes to be automagically downloaded, as needed, by running +a script named +.Em getquote +and expecting that script to return +a value understood by +.Nm . +A sample implementation of a +.Em getquote +script, implemented in Perl, is provided in the +distribution. Downloaded quote price are then appended to the price +database, usually specified using the environment variable +.Ev LEDGER_PRICE_DB . +.It Fl \-effective +Show auxiliary dates for all calculations. Alias for +.Fl \-aux-date . +.It Fl \-empty Pq Fl E +Include empty accounts in report. +.It Fl \-end Ar DATE Pq Fl e +Constrain the report so that transactions on or after +.Ar DATE +are not considered. +.It Fl \-equity +Related to the +.Ic equity +command. Gives current account balances in the form of a register +report. +.It Fl \-exact +Report beginning and ending of periods by the date of the first and last +posting occurring in that period. +.It Fl \-exchange Ar COMMODITY Oo , Ar COMMODITY, ... Oc Pq Fl X +Display values in terms of the given +.Ar COMMODITY . +The latest available price is used. +.It Fl \-explicit +Direct +.Nm +to require pre-declarations for entities (such as accounts, +commodities and tags) rather than taking entities from cleared +transactions as defined. +.It Fl \-file Ar FILE Pq Fl f +Read journal data from +.Ar FILE . +.It Fl \-first Ar INT +Print the first +.Ar INT +entries. Opposite of +.Fl \-last Ar INT . +Alias for +.Fl \-head . +.It Fl \-flat +Force the full names of accounts to be used in the balance report. The +balance report will not use an indented tree. +.It Fl \-force-color +Output TTY color codes even if the TTY doesn't support them. Useful +for TTYs that don't advertise their capabilities correctly. +.It Fl \-force-pager +Force +.Nm +to paginate its output. +.It Fl \-forecast-while Ar EXPR +Continue forecasting while +.Ar VEXPR +is true. +Alias for +.Fl \-forecast . +.It Fl \-forecast-years Ar INT +Forecast at most +.Ar INT +years into the future. +.It Fl \-format Ar FMT Pq Fl F +Use the given format string +.Ar FMT +to print output. +.It Fl \-gain Pq Fl G +Report net gain or loss for commodities that have a price history. +.It Fl \-generated +Include auto-generated postings (such as those from automated +transactions) in the report, in cases where you normally wouldn't want +them. +.It Fl \-group-by Ar EXPR +Group transaction together in the +.Ic register +report. +.Ar EXPR +can be anything, although most common would be +.Ar payee +or +.Ar commodity . +The +.Fn tag +function is also useful here. +.It Fl \-group-title-format Ar FMT +Set the format for the headers that separate reports section of +a grouped report. Only has effect with a +.Fl \-group-by Ar EXPR +register report. +.It Fl \-head Ar INT +Print the first +.Ar INT +entries. Opposite of +.Fl \-tail Ar INT . +Alias for +.Fl \-first +.It Fl \-help +Print this man page. +.It Fl \-immediate +Evaluate calculations immediately rather than lazily. +.It Fl \-import Ar FILE +Import +.Ar FILE +as Python module. +.It Fl \-init-file Ar FILE Pq Fl i +Read +.Ar FILE +before any other +.Nm +file. +This file may not contain any postings, but it may contain option +settings. To specify options in the init file, use the same syntax as +the command-line, but put each option on its own line. +.It Fl \-inject Ar STR +Use +.Ar STR +amounts in calculations. In case you know +what amount a transaction should be, but the actual transaction has the +wrong value you can use metadata +.Ar STR +to specify the expected amount. +.It Fl \-input-date-format Ar DATEFMT +Specify the input date format for journal entries. +.It Fl \-invert +Change the sign of all reported values. +.It Fl \-last Ar INT . +Report only the last +.Ar INT +entries. Opposite of +.Fl \-first Ar INT . +Only useful on a register report. Alias for +.Fl \-tail . +.It Fl \-leeway Ar INT Pq Fl Z +Alias for +.Fl \-price-expr . +.It Fl \-limit Ar EXPR Pq Fl l +Limit postings in calculations. +.It Fl \-lot-dates +Report the date on which each commodity in a balance report was +purchased. +.It Fl \-lot-notes +Report the tag attached to each commodity in a balance report. +.It Fl \-lot-prices +Report the price at which each commodity in a balance report was +purchased. +.It Fl \-lots +Report the date and price at which each commodity was purchased in +a balance report. +.It Fl \-lots-actual +Preserve the uniqueness of commodities so they aren't merged during +reporting without printing the lot annotations. +.It Fl \-market Pq Fl V +Use the latest market value for all commodities. +.It Fl \-master-account Ar STR +Prepend all account names with +.Ar STR +.It Fl \-meta Ar STR +In the register report, prepend the transaction with the value of the given +tag +.Ar STR . +.It Fl \-meta-width Ar INT +Specify the width of the Meta column used for the +.Fl \-meta Ar TAG +options. +.It Fl \-monthly Pq Fl M +Shorthand for +.Fl \-period Ar monthly . +.It Fl \-no-aliases +Aliases are completely ignored. +.It Fl \-no-color +Suppress any color TTY output. +.It Fl \-no-pager +Disables the pager on TTY output. +.It Fl \-no-revalued +Stop +.Nm +from showing +<Revalued> +postings. +.It Fl \-no-rounding +Don't output +.Qq Li <Adjustment> +postings. Note that this will cause the +running total to often not add up! Its main use is for +.Fl \-amount-data Pq Fl j +and +.Fl \-total-data Pq Fl J +reports. +.It Fl \-no-titles +Suppress the output of group titles. +.It Fl \-no-total +Suppress printing the final total line in a balance report. +.It Fl \-now Ar DATE +Use +.Ar DATE +as the current date. This affects the output when using +.Fl \-period , +.Fl \-begin , +.Fl \-end , +or +.Fl \-current +to decide which dates lie in the past or future. +.It Fl \-only Ar EXPR +This is a postings predicate that applies after certain transforms have +been executed, such as periodic gathering. +.It Fl \-options +Display the options in effect for this +.Nm +invocation, along with +their values and the source of those values. +.It Fl \-output Ar FILE Pq Fl o +Redirect the output of +.Nm +to +.Ar FILE . +.It Fl \-pager Ar STR +Use +.Ar STR +as the pager program. +.It Fl \-payee +Sets a value expression for formatting the payee. In the +.Ic register +report this prevents the second entry from having +a date and payee for each transaction. +.It Fl \-payee-width Ar INT +Set the number of columns dedicated to the payee in the register +report to +.Ar INT . +.It Fl \-pedantic +Accounts, tags or commodities not previously declared will cause errors. +.It Fl \-pending +Use only postings that are marked pending. +.It Fl \-percent Pq Fl % +Calculate the percentage value of each account in a balance reports. +Only works for account that have a single commodity. +.It Fl \-period Ar PERIOD Pq Fl p +Define a period expression that sets the time period during which +transactions are to be accounted. For a +.Ic register +report only +the transactions that satisfy the period expression with be displayed. +For a balance report only those transactions will be accounted in the +final balances. +.It Fl \-period-sort +Sort the posting within transactions using the given value expression. +.It Fl \-permissive +Quiet balance assertions. +.It Fl \-pivot Ar TAG +Produce a balance pivot report +.Qq around +the given +.Ar TAG . +.It Fl \-plot-amount-format Ar FMT +Define the output format for an amount data plot. +.It Fl \-plot-total-format Ar FMT +Define the output format for a total data plot. +.It Fl \-prepend-format Ar FMT +Prepend +.Ar FMT +to every line of the output. +.It Fl \-prepend-width Ar INT +Reserve +.Ar INT +spaces at the beginning of each line of the output. +.It Fl \-price Pq Fl I +Use the price of the commodity purchase for performing calculations. +.It Fl \-price-db Ar FILE +.It Fl \-price-exp Ar STR Pq Fl Z +Set the expected freshness of price quotes, in +.Ar INT +minutes. That +is, if the last known quote for any commodity is older than this value, +and if +.Fl \-download +is being used, then the Internet will be +consulted again for a newer price. Otherwise, the old price is still +considered to be fresh enough. +Alias for +.Fl \-leeway . +.It Fl \-prices-format Ar FMT +Set the format for the +.Ic prices +report. +.It Fl \-pricedb-format Ar FMT +Set the format expected for the historical price file. +.It Fl \-primary-date +Show primary dates for all calculations. Alias for +.Fl \-actual-dates +.It Fl \-quantity Pq Fl O +Report commodity totals (this is the default). +.It Fl \-quarterly +Shorthand for +.Fl \-period Ar quarterly . +.It Fl \-raw +In the +.Ic print +report, show transactions using the exact same syntax as +specified by the user in their data file. Don't do any massaging or +interpreting. Can be useful for minor cleanups, like just aligning +amounts. +.It Fl \-real Pq Fl R +Account using only real transactions ignoring virtual and automatic +transactions. +.It Fl \-recursive-aliases +Causes +.Nm +to try to expand aliases recursively, i.e. try to expand +the result of an earlier expansion again, until no more expansions apply. +.It Fl \-register-format Ar FMT +Define the output format for the +.Ic register +report. +.It Fl \-related Pq Fl r +In a register report show the related account. This is the other +.Em side +of the transaction. +.It Fl \-related-all +Show all postings in a transaction, similar to +.Fl \-related +but show both sides of each transaction. +.It Fl \-revalued +Report discrepancy in values for manual reports by inserting +<Revalued> +postings. +This is implied when using +the +.Fl \-exchange Pq Fl X +or +.Fl \-market Pq Fl V +option. +.It Fl \-revalued-only +Show only +<Revalued> +postings. +.It Fl \-revalued-total +Display the sum of the revalued postings as the running total, which serves +to show unrealized capital in a gain/losses report. +.It Fl \-rich-data +When generating a ledger transaction from a CSV file using the +.Ic convert +command, add CSV, Imported, and UUID meta-data. +.It Fl \-seed Ar INT +Set the random seed to +.Ar INT +for the +.Ic generate +command. Used as part of development testing. +.It Fl \-script Ar FILE +Execute a +.Nm +script. +.It Fl \-sort Ar EXPR Pq Fl S +Sort the register report based on the value expression +.Ar EXPR . +.\".It Fl \-sort-all Ar EXPR +.It Fl \-sort-xacts +Sort the posting within transactions using the given value expression. +.It Fl \-start-of-week Ar STR +Use +.Ar STR +as the particular day of the week to start when using the +.Fl \-weekly +option. +.Ar STR +can be day names, their abbreviations like +.Qq Mon , +or the weekday number +starting at 0 for Sunday. +.It Fl \-strict +Accounts, tags or commodities not previously declared will cause warnings. +.It Fl \-subtotal Pq Fl s +Report register as a single subtotal. +.It Fl \-tail Ar INT +Report only the last +.Ar INT +entries. Only useful on a register report. Alias for +.Fl \-last Ar INT +.It Fl \-time-colon +Display the value for commodities based on seconds as hours and minutes. +Thus 8100s will be displayed as 2:15h instead of 2.25h. +.It Fl \-time-report +Add two columns to the +.Ic balance +report to show the earliest checkin and checkout times for timelog entries. +.It Fl \-total Ar EXPR Pq Fl T +Define a value expression used to calculate the total in reports. +.It Fl \-total-data Pq Fl J +Show only dates and totals to format the output for plots. +.It Fl \-total-width Ar INT +Set the width of the total field in the register report. +.It Fl \-trace Ar INT +Enable tracing. The +.Ar INT +specifies the level of trace desired. +.It Fl \-truncate Ar STR +Indicates how truncation should happen when the contents of columns +exceed their width. Valid arguments for +.Ar STR +are +.Ar leading , +.Ar middle , +and +.Ar trailing . +The default is smarter than any of these three, +as it considers sub-names within the account name (that style is +called +.Qq abbreviate ) . +.It Fl \-unbudgeted +Show only un-budgeted postings. +.It Fl \-uncleared Pq Fl U +Use only uncleared transactions in calculations and reports. +.It Fl \-unrealized +Show generated unrealized gain and loss accounts in the balance +report. +.It Fl \-unrealized-gains +Allow the user to specify what account name should be used for +unrealized gains. Defaults to +.Sy "Equity:Unrealized Gains" . +Often set in one's +.Pa ~/.ledgerrc +file to change the default. +.It Fl \-unrealized-losses +Allow the user to specify what account name should be used for +unrealized losses. Defaults to +.Sy "Equity:Unrealized Losses" . +Often set in one's +.Pa ~/.ledgerrc +file to change the default. +.It Fl \-unround +Perform all calculations without rounding and display results to full +precision. +.It Fl \-values +Show the values used by each tag when used in combination with the +.Ic tags +command. +.It Fl \-value-expr Ar EXPR +Set a global value expression annotation. +.It Fl \-verbose +Print detailed information on the execution of +.Nm . +.It Fl \-verify +Enable additional assertions during run-time. This causes a significant +slowdown. When combined with +.Fl \-debug Ar CODE +.Nm +will produce memory trace information. +.It Fl \-verify-memory +Verify that every constructed object is properly destructed. This is for +debugging purposes only. +.It Fl \-version +Print version information and exit. +.It Fl \-weekly Pq Fl W +Shorthand for +.Fl \-period Ar weekly . +.It Fl \-wide Pq Fl w +Assume 132 columns instead of the TTY width. +.It Fl \-yearly Pq Fl Y +Shorthand for +.Fl \-period Ar yearly . +.El +.Sh PRE-COMMANDS +Pre-commands are useful when you aren't sure how a command or option +will work. The difference between a pre-command and a regular command +is that pre-commands ignore the journal data file completely, nor is +the user's init file read. +.Bl -tag -width -indent +.It Ic args No / Ic query +Evaluate the given arguments and report how +.Nm +interprets it against the following model transaction: +.Bd -literal -offset indent +2004/05/27 Book Store + ; This note applies to all postings. :SecondTag: + Expenses:Books 20 BOOK @ $10 + ; Metadata: Some Value + ; Typed:: $100 + $200 + ; :ExampleTag: + ; Here follows a note describing the posting. + Liabilities:MasterCard $-200.00 +.Ed +.It Ic eval +Evaluate the given value expression against the model transaction. +.It Ic format +Print details of how +.Nm +uses the given formatting description and +apply it against a model transaction. +.It Ic parse No / Ic expr +Print details of how +.Nm +uses the given value expression description +and apply it against a model transaction. +.It Ic generate +Randomly generates syntactically valid +.Nm +data from a seed. Used +by the GenerateTests harness for development testing. +.It Ic period +Evaluate the given period and report how +.Nm +interprets it. +.\".It Ic script +.It Ic template +Shows the insertion template that the +.Ic xact +command generates. This is a debugging command. +.El +.Sh QUERIES +The syntax for reporting queries can get somewhat complex. It is a series of +query terms with an implicit OR operator between them. The following terms +are accepted: +.Bl -tag -width "term and term" +.It Ar regex +A bare string is taken as a regular expression matching the full account name. +Thus, to report the current balance for all assets and liabilities, you would +use: +.Pp +.Dl ledger bal asset liab +.It Ic payee Ar regex Pq Ic \&@ Ns Ar regex +Query on the payee, rather than the account. +.It Ic tag Ar regex Pq Ic \&% Ns Ar regex +.It Ic note Ar regex Pq Ic \&= Ns Ar regex +Query on anything found in an item's note. +.It Ic code Ar regex Pq Ic \&# Ns Ar regex +Query on the xact's optional code (which can be any string the user wishes). +.It Ar term Cm and Ar term +Query terms are joined by an implicit OR operator. You can change this to AND +by using the +.Cm and +keyword. For example, to show food expenditures occurring at +Shakee's Pizza, you could say: +.Pp +.Dl Li ledger reg food and @Shakee +.It Ar term Cm or Ar term +When you wish to be more explicit, use the OR operator. +.It Ic show +.It Cm not Ar term +Reverse the logical meaning of the following term. This can be used with +parentheses to great effect: +.Pp +.Dl Li ledger reg food and @Shakee and not dining +.It \&( Ar term No \&) +If you wish to mix OR and AND operators, it is often helpful to surround +logical units with parentheses. \fBNOTE\fR: Because of the way some shells +interpret parentheses, you should always escape them: +.Pp +.Dl Li ledger bal \e( assets or liab \e) and not food +.El +.Sh EXPRESSIONS +.Bl -tag -width "partial_account" +.It Fn abs value +Return the absolute value of the given +.Ar value . +.It Sy account +Return the posting's account. +.It Sy account_base +Return the base account, i.e. everything after the last account delimiter ':'. +.\".It Sy account_amount +.It Sy actual +.\" Is there a difference between real and actual? +Return true if the transaction is real, i.e not a automated or virtual +transaction, false otherwise. +.It Sy amount +Return the amount of the posting. +.It Sy amount_expr +Return the calculated amount of the posting according to the +.Fl \-amount +option. +.It Fn ansify_if value color bool +Render the given +.Ar value +as a string, applying the proper ANSI escape codes to display it in the given +.Ar color +if +.Ar bool +is true. It typically checks the value of the option +.Fl \-color , +for example: +.Dl Li ansify_if(amount, "blue", options.color) +.It Sy beg_line +Line number where entry for posting begins. +.It Sy beg_pos +Character position where entry for posting begins. +.\".It Sy calculated +.It Fn ceiling value +Return the next integer of +.Ar value +toward +infinity. +.It Sy cleared +Return true if the posting was cleared, false otherwise. +.It Sy code +Return the transaction code, the string between the parenthesis after the date. +.\".It Sy comment +.It Fn commodity value +Return the commodity of +.Ar value +or the posting amount when +.Ar value +was not specified. +.\".It Sy cost +.\".It Sy count +.It Sy date +Return the date of the posting. +.\".It Sy depth +.\".It Sy depth_spacer +.\".It Sy display_amount +.\".It Sy display_total +.It Sy end_line +Line number where entry for posting ends. +.It Sy end_pos +Character position where entry for posting ends. +.It Fn floor value +Return the next integer of +.Ar value +toward -infinity. +.It Sy filename +The name of the +.Nm +data file from whence the posting came. +.It Fn format string +Evaluate +.Ar string +as format just like the +.Fl \-format +option. +.It Fn format_date date format +Return the +.Ar date +as a string using +.Ar format . +Refer to +.Xr strftime 3 +for format string details. +.It Fn format_datetime datetime format +Return the +.Ar datetime +as a string using +.Ar format . +Refer to +.Xr strftime 3 +for format string details. +.It Fn get_at seq index +Return value at +.Ar index +from +.Ar seq . +Used internally to construct different reports. +.It Fn has_meta +Return true if the posting has metadata named +.Ar tag , +false otherwise. +.It Fn has_tag tag +Return true if the posting has metadata named +.Ar tag , +false otherwise. +.It Fn is_seq value +Return true if +.Ar value +is a sequence. Used internally. +.It Fn join value +Replace all newlines in +.Ar value +with +.Li \en . +.It Fn justify value first_width latter_width right_justify colorize +Right or left justify the string representing +.Ar value . +The width of the field in the first line is given by +.Ar first_width . +For subsequent lines the width is given by +.Ar latter_width . +If +.Ar latter_width +is -1, +.Ar first_width +is used for all lines. +If +.Ar right_justify +is true then the field is right justified within the width of the field. If it +is false, then the field is left justified and padded to the full width of the +field. If +.Ar colorize +is true, then ledger will honor color settings. +.It Fn market value datetime +Return the price of +.Ar value +at +.Ar datetime . +Note that +.Ar datetime +must be surrounded by brackets in order to be parsed correctly, e.g. +.Bq 2012/03/23 . +.It Fn meta +Return the value of metadata named +.Ar name . +.It Sy note +Return the note for the posting. +.It Sy now +Return the current datetime. +.\".It Sy null +.It Sy options +A variable that allows access to the values of the given command-line options +using the long option names, e.g. to see whether +.Fl \-daily Pq Fl D +was given use +.Sy option.daily . +.\" .It Sy partial_account +.It Sy payee +Return the payee of the posting. +.It Fn percent value_a value_b +Return the percentage of +.Ar value_a +in relation to +.Ar value_b +(used as 100%). +.It Sy pending +Return true if the posting is marked as pending, false otherwise. +.It Fn percent value_a value_b +Return the percentage of +.Ar value_a +in relation to +.Ar value_b . +.\".It Sy post +.\" A variable scope +.It Fn print value +Print +.Ar value +to stdout. +Used internally for debugging. +.It Fn quantity value +Return the quantity of +.Ar value +for values that have a per-unit cost. +.It Fn quoted expression +Surround +.Ar expression +with double-quotes. +.It Fn quoted_rfc4180 expression +Surround +.Ar expression +with double-quotes, compatible with rfc 4180. +.It Sy real +.\" Is there a difference between real and actual? +Return true if the transaction is real, i.e not a automated or virtual +transaction, false otherwise. +.\".It Sy rounded +.It Fn roundto value n +Return +.Ar value +rounded to +.Ar n +digits. Does not affect formatting. +.It Sy should_bold +Return true if expression given to +.Fl \-bold-if +evaluates to true. Internal use only! +.It Fn scrub value +Clean +.Ar value +using various transformations such as round, +stripping value annotations, and more. +.\".It Sy status +.It Fn strip value +Strip value annotation from +.Ar value . +.\".It Sy subcount +.It Fn tag name +Return the value of tag named +.Ar name . +.It Fn to_amount value +Convert +.Ar value +to an amount. Internal use only! +.It Fn to_balance value +Convert +.Ar value +to a balance. Internal use only! +.It Fn to_boolean value +Convert +.Ar value +to a boolean. Internal use only! +.It Fn to_date value +Convert +.Ar value +to a date. Internal use only! +.It Fn to_datetime value +Convert +.Ar value +to a datetime. Internal use only! +.It Fn to_int value +Return the integer value for +.Ar value . +.It Fn to_mask value +Convert +.Ar value +to a mask. Internal use only! +.It Fn to_sequence value +Convert +.Ar value +to a sequence. Internal use only! +.It Fn to_string value +Convert +.Ar value +to a character string. +.It Sy today +Return today's date. +.It Sy total +Return the total of the posting. +.It Sy total_expr +Return the calculated total of the posting according to the +.Fl \-total +option. +.It Fn trim value +Trim leading and trailing whitespace from +.Ar value . +.It Fn truncated string total_len account_len +Truncate +.Ar string +to +.Ar total_len +ensuring that each account is at least +.Ar account_len +long. +.\".It Sy uncleared +.It Sy virtual +Return true if the transaction is virtual, e.g automated, false otherwise. +.\".It Sy xact +.\" A variable scope +.El +.\".Sh ENTRIES +.\".Sh FORMATS +.Sh DEBUG COMMANDS +In addition to the regular reporting commands, +.Nm +also accepts several +debug commands: +.Bl -tag -width balance +.It Ic args Oo Ar report-query Oc +Display complete analysis of how +.Nm +interpreted the given +.Ar report-query . +Useful if you want to understand how report queries are translated into value +expressions. +.It Ic eval Oo Ar value-expression Oc +Evaluate the given +.Ar value-expression +and prints the result. For more on value expressions, see the section +.Sx EXPRESSIONS . +.It Ic format Oo Ar format-string Oc +Display an analysis of how +.Ar format-string +was parsed, and what it would look like applied to a sample transaction. For +more on format strings, see the section +.Sx FORMATS . +.It Ic generate +Generate 50 randomly composed yet valid +.Nm +transactions. +.It Ic parse Oo Ar value-expression Oc +Parse the given +.Ar value-expression +and display an analysis of the expression tree and its evaluated value. For +more on value expressions, see the section +.Sx EXPRESSIONS . +.It Ic python Oo Ar file Oc +Invoke a Python interpreter to read the given +.Ar file . +What is special about this is that the +.Nm +module is builtin, not read from +disk, so it doesn't require +.Nm +to be installed anywhere, or the shared +library variants to be built. +.It Ic reload +Reload all data files for the current session immediately. +Can only be used in the +.Tn REPL . +.It Ic template Oo Ar draft-template Oc +Display information about how +.Ar draft-template +was parsed. See the section on +.Sx DRAFTS . +.El +.Sh ENVIRONMENT +Every option to +.Nm +may be set using an environment variable if the option has a long name. +For example setting the environment variable +.Ev LEDGER_DATE_FORMAT="%d.%m.%Y' +will have the same effect as specifying +.Fl \-date-format Ar '%d.%m.%Y' +on the command-line. Options on the command-line always take precedence over +environment variable settings, however. +.Sh FILES +.Bl -tag -width -indent +.It Pa ~/.ledgerrc +Your personal +.Nm +initializations. +.El +.Sh SEE ALSO +.Xr beancount 1 , +.Xr hledger 1 + +The full documentation for +.Nm +is maintained as a Texinfo manual. If the +.Nm info +program is installed on your system, the command +.Dl info ledger3 +should give you access to the complete manual. +.Sh AUTHORS +.An "John Wiegley" +.Aq johnw@newartisans.com +.\" .Sh BUGS \" Document known, unremedied bugs +.\" .Sh HISTORY \" Document history if command behaves in a unique manner diff --git a/doc/ledger3.texi b/doc/ledger3.texi new file mode 100644 index 00000000..3d805100 --- /dev/null +++ b/doc/ledger3.texi @@ -0,0 +1,10403 @@ +\input texinfo @c -*-texinfo-*- + +@setfilename ledger3.info +@include version.texi +@set FIXME:UNDOCUMENTED @sc{undocumented}! Please help by contributing documentation for this feature. +@set InternalUseOnly For internal use only. +@settitle Ledger: Command-Line Accounting + +@c Before release, run C-u C-c C-u C-a (texinfo-all-menus-update with +@c a prefix arg). This updates the node pointers, which texinfmt.el +@c needs. + +@c | Formating | Indexing | | +@c | | @cindex | concept | +@c | @command | @findex | Ledger CLI Command (like balance) | +@c | @option | @findex | Ledger CLI Option (like --market) | +@c | @var | | Ledger CLI option Variable (like -f FILE) | +@c | | | Ledger file Syntax | +@c | @samp | | Valued example or single char | +@c | @file | | File, Buffer | +@c | @file | | Program (like ledger, report, acprep) | + +@c Restructuring manual ideas +@c http://beyondgrep.com/documentation/ack-2.04-man.html + +@c How to make documented ledger examples validate automatically. +@c +@c The test/DocTests.py script will be run along with the other tests +@c when using ctest or acprep check. +@c The script parses the texinfo file and looks for three kinds of +@c specially marked @smallexamples, then it will run the ledger +@c command from the example, and compare the results with the output +@c from the documentation. +@c +@c To specially mark a @smallexample append @c command:UUID, where +@c UUID is the first 7 digits from the commands sha1sum, e.g.: +@c +@c @smallexample @c command:CDE330A +@c $ ledger -f sample.dat reg expenses +@c @end smallexample +@c +@c Then DocTests.py will look for corresponding documented output, +@c which may appear anywhere in the file, and is marked with +@c @smallexample @c output:UUID where UUID is the UUID from the +@c corresponding ledger command example, e.g.: +@c +@c @smallexample @c output:CDE330A +@c 04-May-27 Book Store Expenses:Books $20.00 $20.00 +@c Expenses:Cards $40.00 $60.00 +@c Expenses:Docs $30.00 $90.0 +@c @end smallexample +@c +@c Now where does this data in sample.dat come from? +@c DocTests.py is a bit smart about ledger's file argument, since +@c it will check if the given filename exists in the test/input/ +@c directory. +@c +@c Sometimes the journal data for an example is specified within +@c the documentation itself, in that case the journal example data +@c needs to be specially marked as well using @smallexample @c input:UUID, +@c again with the UUID being the UUID of the corresponding ledger example +@c command. If multiple inputs with the same UUID are found they will be +@c concatenated together and given as one set of data to the example command. +@c +@c @smallexample @c input:35CB2A3 +@c 2014/02/09 The Italian Place +@c Expenses:Food:Dining $ 36.84 +@c Assets:Cash +@c @end smallexample +@c +@c @smallexample @c command:35CB2A3 +@c $ ledger -f inline.dat accounts +@c @end smallexample +@c +@c @smallexample @c output:35CB2A3 +@c Assets:Cash +@c Expenses:Food:Dining +@c @end smallexample +@c +@c To use different example commands with the same input from the documentation +@c add with_input:UUID to the example command, where UUID is the UUID of the input, +@c e.g.: +@c +@c @smallexample @c command:94FD2B6,with_input:35CB2A3 +@c $ ledger -f inline.dat bal expenses +@c @end smallexample +@c +@c @smallexample @c output:94FD2B6 +@c $ 36.84 Expenses:Food:Dining +@c @end smallexample +@c +@c To pass additional input to ledger for certain commands, e.g. convert add +@c with_file:filename to the example command and add a file:UUID to an example +@c that holds the additional input, where UUID is the UUID of the command, +@c e.g.: +@c +@c @smallexample @c file:download.csv +@c 767718,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-8.80,,00001640.04,, +@c @end smallexample +@c +@c @smallexample @c command:94FD2B6,with_file:download.csv +@c $ ledger -f sample.dat convert download.csv +@c @end smallexample +@c +@c Additionally DocTests.py will pass --args-only and --columns 80 to ledger +@c to ignore any default arguments from the environment or .ledgerrc. +@c +@c To manually run the tests in this file run: +@c $ ./test/DocTests.py -vv --ledger ./ledger --file ./doc/ledger3.texi + + +@copying + +Copyright @copyright{} 2003--2019, John Wiegley. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +@itemize + +@item +Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +@item +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. + +@item +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. + +@end itemize + +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. + +@end copying + +@dircategory User Applications +@direntry +* Ledger3: (ledger3). Command-Line Accounting +@end direntry + +@documentencoding UTF-8 + +@iftex +@finalout +@end iftex + +@titlepage +@title Ledger: Command-Line Accounting +@subtitle For Version @value{VERSION} of Ledger +@author John Wiegley +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@contents + +@ifnottex + +@node Top, Introduction to Ledger, (dir), (dir) +@top Overview + +Ledger is a command-line accounting tool that provides double-entry +accounting based on a text journal. It provides no bells or whistles, +and returns the user to the days before user interfaces were even a +twinkling in their fathers' CRTs. + +@end ifnottex + +@menu +* Introduction to Ledger:: +* Ledger Tutorial:: +* Principles of Accounting with Ledger:: +* Keeping a Journal:: +* Transactions:: +* Building Reports:: +* Reporting Commands:: +* Command-Line Syntax:: +* Budgeting and Forecasting:: +* Time Keeping:: +* Value Expressions:: +* Format Strings:: +* Extending with Python:: +* Ledger for Developers:: +* Major Changes from version 2.6:: +* Example Journal File:: +* Miscellaneous Notes:: +* Concepts Index:: +* Commands & Options Index:: +@end menu + +@node Introduction to Ledger, Ledger Tutorial, Top, Top +@chapter Introduction to Ledger + +@menu +* Fat-free Accounting:: +* Building the program:: +* Getting help:: +* Third-Party Ledger Tutorials:: +@end menu + +@node Fat-free Accounting, Building the program, Introduction to Ledger, Introduction to Ledger +@section Fat-free Accounting + +Ledger is an accounting tool with the moxie to exist. It provides no +bells or whistles, and returns the user to the days before user +interfaces were even a twinkling in their father's CRT. + +What it does offer is a double-entry accounting journal with all the +flexibility and muscle of its modern day cousins, without any of the +fat. Think of it as the Bran Muffin of accounting tools. + +To use it, you need to start keeping a journal. This is the basis of +all accounting, and if you haven't started yet, now is the time to +learn. The little booklet that comes with your checkbook is a journal, +so we'll describe double-entry accounting in terms of that. + +@c If you use another GUI accounting program like GnuCash, the vast +@c majority of its functionality is geared towards helping you keep +@c a journal. + +A checkbook journal records debits (subtractions, or withdrawals) and +credits (additions, or deposits) with reference to a single account: +the checking account. Where the money comes from, and where it goes +to, are described in the payee field, where you write the person or +company's name. The ultimate aim of keeping a checkbook journal is to +know how much money is available to spend. That's really the aim of +all journals. + +@cindex postings +What computers add is the ability to walk through these postings, +and tell you things about your spending habits; to let you devise +budgets and get control over your spending; to squirrel away money +into virtual savings account without having to physically move money +around; etc. As you keep your journal, you are recording information +about your life and habits, and sometimes that information can start +telling you things you aren't aware of. Such is the aim of all good +accounting tools. + +The next step up from a checkbook journal, is a journal that keeps +track of all your accounts, not just checking. In such a journal, you +record not only who gets paid---in the case of a debit---but where the +money came from. In a checkbook journal, it's assumed that all the +money comes from your checking account. But in a general journal, you +write postings in two lines: the source account and target account. +@emph{There must always be a debit from at least one account for every +credit made to another account}. This is what is meant by +``double-entry'' accounting: the journal must always balance to zero, +with an equal number of debits and credits. + +For example, let's say you have a checking account and a brokerage +account, and you can write checks from both of them. Rather than keep +two checkbooks, you decide to use one journal for both. In this +general journal you need to record a payment to Pacific Bell for your +monthly phone bill, and a transfer (via check) from your brokerage +account to your checking account. The Pacific Bell bill is $23.00, +let's say, and you want to pay it from your checking account. In the +general journal you need to say where the money came from, in addition +to where it's going to. These transactions might look like this: + +@smallexample +9/29 Pacific Bell $23.00 $23.00 + Checking $-23.00 0 +9/30 Checking $100.00 $100.00 + (123) Brokerage $-100.00 0 +@end smallexample + +The posting must balance to $0: $23 went to Pacific Bell, $23 came +from Checking. The next entry shows check number 123 written against +your brokerage account, transferring money to your checking account. +There is nothing left over to be accounted for, since the money has +simply moved from one account to another in both cases. This is the +basis of double-entry accounting: money never pops in or out of +existence; it is always a posting from one account to another. + +Keeping a general journal is the same as keeping two separate +journals: One for Pacific Bell and one for Checking. In that case, +each time a payment is written into one, you write a corresponding +withdrawal into the other. This makes it easier to write in +a ``running balance'', since you don't have to look back at the last +time the account was referenced---but it also means having a lot of +journal books, if you deal with multiple accounts. + +@cindex account, meaning of +@cindex meaning of account +Here is a good place for an aside on the use of the word ``account''. +Most private people consider an account to be something that holds +money at an institution for them. Ledger uses a more general +definition of the word. An account is anywhere money can go. Other +finance programs use ``categories'', Ledger uses accounts. So, for +example, if you buy some groceries at Trader Joe's, then more groceries +at Whole Food Market, you might assign the transactions like this + +@smallexample @c input:validate +2011/03/15 Trader Joe's + Expenses:Groceries $100.00 + Assets:Checking +2011/03/15 Whole Food Market + Expenses:Groceries $75.00 + Assets:Checking +@end smallexample + +In both cases the money goes to the @samp{Groceries} account, even +though the payees were different. You can set up your accounts in any +way you choose. + +Enter the beauty of computerized accounting. The purpose of the +Ledger program is to make general journal accounting simple, by +keeping track of the balances for you. Your only job is to enter the +postings. If an individual posting does not balance, Ledger displays +an error and indicates the incorrect posting.@footnote{In some special +cases, it automatically balances this transaction for you.} + +In summary, there are two aspects of Ledger use: updating the journal +data file, and using the Ledger tool to view the summarized result of +your transactions. + +And just for the sake of example---as a starting point for those who +want to dive in head-first---here are the journal transactions from +above, formatted as the Ledger program wishes to see them: + +@smallexample @c input:48DDF26 +2004/09/29 Pacific Bell + Expenses:Pacific Bell $23.00 + Assets:Checking +@end smallexample + +The account balances and registers in this file, if saved as +@file{ledger.dat}, could be reported using: + +@smallexample @c command:48DDF26 +$ ledger -f ledger.dat balance +@end smallexample + +@smallexample @c output:48DDF26 + $-23.00 Assets:Checking + $23.00 Expenses:Pacific Bell +-------------------- + 0 +@end smallexample + +Or + +@smallexample @c command:8C7295F,with_input:48DDF26 +$ ledger -f ledger.dat register checking +@end smallexample + +@smallexample @c output:8C7295F +04-Sep-29 Pacific Bell Assets:Checking $-23.00 $-23.00 +@end smallexample + +And even: + +@smallexample @c command:BB32EF2,with_input:48DDF26 +$ ledger -f ledger.dat register Bell +@end smallexample + +@smallexample @c output:BB32EF2 +04-Sep-29 Pacific Bell Expenses:Pacific Bell $23.00 $23.00 +@end smallexample + +An important difference between Ledger and other finance packages is +that Ledger will never alter your input file. You can create and edit +that file in any way you prefer, but Ledger is only for analyzing the +data, not for altering it. + +@node Building the program, Getting help, Fat-free Accounting, Introduction to Ledger +@section Building the program + +Ledger is written in ANSI C++, and should compile on any unix platform. +The easiest way to build and install ledger is to use the prepared +acprep script, that does a lot of the footwork: + +@smallexample + # to install missing dependencies + ./acprep dependencies + # building ledger + ./acprep update + # to run the actual installation + make install +@end smallexample + +See the `help` subcommand to `acprep`, which explains some of its many +options. You can run `make check` to confirm the result, and `make +install` to install. If these intructions do not work for you can check the +`INSTALL.md` in the source directory for more up do date build instructions. + +@node Getting help, Third-Party Ledger Tutorials, Building the program, Introduction to Ledger +@section Getting help +@findex help + +Ledger has a complete online help system based on GNU Info. This manual +can be searched directly from the command-line using @code{info ledger}, +which will bring up this entire manual in your TTY. Alternatively, the +shorter man page can be accessed from the command-line either via +@code{man ledger} or @code{ledger --help} + +If you need help on how to use Ledger, or run into problems, you can +join the Ledger mailing list at +@url{http://groups.google.com/group/ledger-cli}. + +You can also find help in the @code{#ledger} channel on the IRC server +@code{irc.freenode.net}. + +@node Third-Party Ledger Tutorials, , Getting help, Introduction to Ledger +@section Third-Party Ledger Tutorials + +There are plenty of people using Ledger for accounting applications. +Some have documented how they use Ledger's features to solve their +accounting problems. + +One such tutorial, specifically designed for non-profit charities that seek +to use Ledger, can be found at +@url{https://k.sfconservancy.org/NPO-Accounting/npo-ledger-cli} (with a copy on GitHub also +available at @url{https://github.com/conservancy/npo-ledger-cli/}). If +you're looking for information about how to use Ledger's tagging system to +handle invoicing, track expenses by program targets, and other such concepts, +you might find the tutorial useful. (Some of the auditor reporting scripts +that relate to the aformentioned Ledger setup can be found +@var{contrib/non-profit-audit-reports/} in Ledger's own source repository.) + +@node Ledger Tutorial, Principles of Accounting with Ledger, Introduction to Ledger, Top +@chapter Ledger Tutorial +@cindex tutorial + +@menu +* Start a Journal File:: +* Run a Few Reports:: +@end menu + +@node Start a Journal File, Run a Few Reports, Ledger Tutorial, Ledger Tutorial +@section Start a Journal File +@cindex journals + +A journal is a record of your financial transactions and will be central +to using Ledger. For now we just want to get a taste of what Ledger can +do. An example journal is included with the source code distribution, +called @file{drewr3.dat} (@pxref{Example Journal File}). Copy it +someplace convenient and open up a terminal window in that directory. + +If you would rather start with your own journal right away please +@pxref{Keeping a Journal}. + +@node Run a Few Reports, , Start a Journal File, Ledger Tutorial +@section Run a Few Reports + +@menu +* Balance Report:: +* Register Report:: +* Cleared Report:: +* Using the Windows Command-Line:: +@end menu + +Please note that as a command-line program, Ledger is controlled from +your shell. There are several different command shells that all +behave slightly differently with respect to some special characters. +In particular, the ``bash'' shell will interpret @samp{$} signs +differently than ledger and they must be escaped to reach the actual +program. Another example is ``zsh'', which will interpret @samp{^} +differently than ledger expects. In all cases that follow you should +take that into account when entering the command-line arguments as given. +There are too many variations between shells to give concrete examples +for each. + +@node Balance Report, Register Report, Run a Few Reports, Run a Few Reports +@subsection Balance Report +@cindex balance report +@findex balance + +To find the balances of all of your accounts, run this command: + +@smallexample @c command:1071890 +$ ledger -f drewr3.dat balance +@end smallexample + +Ledger will generate: + +@smallexample @c output:1071890 + $ -3,804.00 Assets + $ 1,396.00 Checking + $ 30.00 Business + $ -5,200.00 Savings + $ -1,000.00 Equity:Opening Balances + $ 6,654.00 Expenses + $ 5,500.00 Auto + $ 20.00 Books + $ 300.00 Escrow + $ 334.00 Food:Groceries + $ 500.00 Interest:Mortgage + $ -2,030.00 Income + $ -2,000.00 Salary + $ -30.00 Sales + $ -63.60 Liabilities + $ -20.00 MasterCard + $ 200.00 Mortgage:Principal + $ -243.60 Tithe +-------------------- + $ -243.60 +@end smallexample + +@noindent +Showing you the balance of all accounts. Options and search terms can +pare this down to show only the accounts you want. + +A more useful report is to show only your Assets and Liabilities: + +@smallexample @c command:5BF4D8E +$ ledger -f drewr3.dat balance Assets Liabilities +@end smallexample + +@smallexample @c output:5BF4D8E + $ -3,804.00 Assets + $ 1,396.00 Checking + $ 30.00 Business + $ -5,200.00 Savings + $ -63.60 Liabilities + $ -20.00 MasterCard + $ 200.00 Mortgage:Principal + $ -243.60 Tithe +-------------------- + $ -3,867.60 +@end smallexample + +@node Register Report, Cleared Report, Balance Report, Run a Few Reports +@subsection Register Report +@cindex register report +@findex register + +To show all transactions and a running total: + +@smallexample @c command:66E3A2C +$ ledger -f drewr3.dat register +@end smallexample + +@noindent +Ledger will generate: + +@smallexample @c output:66E3A2C +10-Dec-01 Checking balance Assets:Checking $ 1,000.00 $ 1,000.00 + Equit:Opening Balances $ -1,000.00 0 +10-Dec-20 Organic Co-op Expense:Food:Groceries $ 37.50 $ 37.50 + Expense:Food:Groceries $ 37.50 $ 75.00 + Expense:Food:Groceries $ 37.50 $ 112.50 + Expense:Food:Groceries $ 37.50 $ 150.00 + Expense:Food:Groceries $ 37.50 $ 187.50 + Expense:Food:Groceries $ 37.50 $ 225.00 + Assets:Checking $ -225.00 0 +10-Dec-28 Acme Mortgage Lia:Mortgage:Principal $ 200.00 $ 200.00 + Expe:Interest:Mortgage $ 500.00 $ 700.00 + Expenses:Escrow $ 300.00 $ 1,000.00 + Assets:Checking $ -1,000.00 0 +11-Jan-02 Grocery Store Expense:Food:Groceries $ 65.00 $ 65.00 + Assets:Checking $ -65.00 0 +11-Jan-05 Employer Assets:Checking $ 2,000.00 $ 2,000.00 + Income:Salary $ -2,000.00 0 + (Liabilities:Tithe) $ -240.00 $ -240.00 +11-Jan-14 Bank Assets:Savings $ 300.00 $ 60.00 + Assets:Checking $ -300.00 $ -240.00 +11-Jan-19 Grocery Store Expense:Food:Groceries $ 44.00 $ -196.00 + Assets:Checking $ -44.00 $ -240.00 +11-Jan-25 Bank Assets:Checking $ 5,500.00 $ 5,260.00 + Assets:Savings $ -5,500.00 $ -240.00 +11-Jan-25 Tom's Used Cars Expenses:Auto $ 5,500.00 $ 5,260.00 + Assets:Checking $ -5,500.00 $ -240.00 +11-Jan-27 Book Store Expenses:Books $ 20.00 $ -220.00 + Liabilities:MasterCard $ -20.00 $ -240.00 +11-Dec-01 Sale Asse:Checking:Business $ 30.00 $ -210.00 + Income:Sales $ -30.00 $ -240.00 + (Liabilities:Tithe) $ -3.60 $ -243.60 +@end smallexample + +@noindent +To limit this to a more useful subset, simply add the accounts you are +interested in seeing transactions for: + +@cindex accounts, limiting by +@cindex limiting by accounts + +@smallexample @c command:96B0EB3 +$ ledger -f drewr3.dat register Groceries +@end smallexample + +@smallexample @c output:96B0EB3 +10-Dec-20 Organic Co-op Expense:Food:Groceries $ 37.50 $ 37.50 + Expense:Food:Groceries $ 37.50 $ 75.00 + Expense:Food:Groceries $ 37.50 $ 112.50 + Expense:Food:Groceries $ 37.50 $ 150.00 + Expense:Food:Groceries $ 37.50 $ 187.50 + Expense:Food:Groceries $ 37.50 $ 225.00 +11-Jan-02 Grocery Store Expense:Food:Groceries $ 65.00 $ 290.00 +11-Jan-19 Grocery Store Expense:Food:Groceries $ 44.00 $ 334.00 +@end smallexample + +@noindent +Which matches the balance reported for the @samp{Groceries} account: + +@smallexample @c command:AECD64E +$ ledger -f drewr3.dat balance Groceries +@end smallexample + +@smallexample @c output:AECD64E + $ 334.00 Expenses:Food:Groceries +@end smallexample + +@noindent +If you would like to find transaction to only a certain payee use +@samp{payee} or @samp{@@}: + +@smallexample @c command:C6BC57E +$ ledger -f drewr3.dat register payee "Organic" +@end smallexample + +@smallexample @c output:C6BC57E +10-Dec-20 Organic Co-op Expense:Food:Groceries $ 37.50 $ 37.50 + Expense:Food:Groceries $ 37.50 $ 75.00 + Expense:Food:Groceries $ 37.50 $ 112.50 + Expense:Food:Groceries $ 37.50 $ 150.00 + Expense:Food:Groceries $ 37.50 $ 187.50 + Expense:Food:Groceries $ 37.50 $ 225.00 + Assets:Checking $ -225.00 0 +@end smallexample + +@node Cleared Report, Using the Windows Command-Line, Register Report, Run a Few Reports +@subsection Cleared Report +@cindex cleared report +@findex cleared + +A very useful report is to show what your obligations are versus what +expenditures have actually been recorded. It can take several days for +a check to clear, but you should treat it as money spent. The +@command{cleared} report shows just that (note that the +@command{cleared} report will not format correctly for accounts that +contain multiple commodities): + +@smallexample @c command:B86F6A6 +$ ledger -f drewr3.dat cleared +@end smallexample + +@smallexample @c output:B86F6A6 + $ -3,804.00 $ 775.00 Assets + $ 1,396.00 $ 775.00 10-Dec-20 Checking + $ 30.00 0 Business + $ -5,200.00 0 Savings + $ -1,000.00 $ -1,000.00 10-Dec-01 Equity:Opening Balances + $ 6,654.00 $ 225.00 Expenses + $ 5,500.00 0 Auto + $ 20.00 0 Books + $ 300.00 0 Escrow + $ 334.00 $ 225.00 10-Dec-20 Food:Groceries + $ 500.00 0 Interest:Mortgage + $ -2,030.00 0 Income + $ -2,000.00 0 Salary + $ -30.00 0 Sales + $ -63.60 0 Liabilities + $ -20.00 0 MasterCard + $ 200.00 0 Mortgage:Principal + $ -243.60 0 Tithe +---------------- ---------------- --------- + $ -243.60 0 +@end smallexample + +@noindent +The first column shows the outstanding balance, the second column +shows the ``cleared'' balance. + +@node Using the Windows Command-Line, , Cleared Report, Run a Few Reports +@subsection Using the Windows Command-Line +@cindex windows cmd.exe +@cindex currency symbol display on windows + +Using ledger under the windows command shell has one significant +limitation. CMD.EXE is limited to standard ASCII characters and as +such cannot display any currency symbols other than dollar signs +@samp{$}. + +@node Principles of Accounting with Ledger, Keeping a Journal, Ledger Tutorial, Top +@chapter Principles of Accounting with Ledger + +@menu +* Accounting with Ledger:: +* Stating where money goes:: +* Assets and Liabilities:: +* Commodities and Currencies:: +* Accounts and Inventories:: +* Understanding Equity:: +* Dealing with Petty Cash:: +* Working with multiple funds and accounts:: +@end menu + +@node Accounting with Ledger, Stating where money goes, Principles of Accounting with Ledger, Principles of Accounting with Ledger +@section Accounting with Ledger +@cindex double-entry accounting + +Accounting is simply tracking your money. It can range from nothing, +and just waiting for automatic overdraft protection to kick in, or +not, to a full-blown double-entry accounting system. Ledger +accomplishes the latter. With ledger you can handle your personal +finances or your business's. Double-entry accounting scales. + +@node Stating where money goes, Assets and Liabilities, Accounting with Ledger, Principles of Accounting with Ledger +@section Stating where money goes +@cindex credits and debits + +Accountants will talk of ``credits'' and ``debits'', but the meaning +is often different from the layman's understanding. To avoid +confusion, Ledger uses only subtractions and additions, although the +underlying intent is the same as standard accounting principles. + +Recall that every posting will involve two or more accounts. +Money is transferred from one or more accounts to one or more other +accounts. To record the posting, an amount is @emph{subtracted} +from the source accounts, and @emph{added} to the target accounts. + +In order to write a Ledger transaction correctly, you must determine +where the money comes from and where it goes to. For example, when +you are paid a salary, you must add money to your bank account and +also subtract it from an income account: + +@smallexample @c input:validate +9/29 My Employer + Assets:Checking $500.00 + Income:Salary $-500.00 +@end smallexample + +@cindex income is negative +@cindex why is income negative + +Why is the Income a negative figure? When you look at the balance +totals for your ledger, you may be surprised to see that Expenses are +a positive figure, and Income is a negative figure. It may take some +getting used to, but to properly use a general ledger you must think +in terms of how money moves. Rather than Ledger ``fixing'' the minus +signs, let's understand why they are there. + +When you earn money, the money has to come from somewhere. Let's call +that somewhere ``society''. In order for society to give you an +income, you must take money away (withdraw) from society in order to +put it into (make a payment to) your bank. When you then spend that +money, it leaves your bank account (a withdrawal) and goes back to +society (a payment). This is why Income will appear negative---it +reflects the money you have drawn from society---and why Expenses will +be positive---it is the amount you've given back. These additions and +subtractions will always cancel each other out in the end, because you +don't have the ability to create new money: it must always come from +somewhere, and in the end must always leave. This is the beginning of +economy, after which the explanation gets terribly difficult. + +Based on that explanation, here's another way to look at your balance +report: every negative figure means that that account or person or +place has less money now than when you started your ledger; and every +positive figure means that that account or person or place has more +money now than when you started your ledger. Make sense? + +@node Assets and Liabilities, Commodities and Currencies, Stating where money goes, Principles of Accounting with Ledger +@section Assets and Liabilities +@cindex assets and liabilities +@cindex debts are 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 transaction: + +@smallexample @c input:6B43DD4 +2004/09/29 My Employer + Assets:Checking $500.00 + Income:Salary +@end smallexample + +Money, here, comes from an Income account belonging to @samp{My +Employer}, and is transferred to your checking account. The money is +now yours, which makes it an Asset. + +Liabilities track money owed to others. This can happen when you +borrow money to buy something, or if you owe someone money. Here is +an example of increasing a MasterCard liability by spending money with +it: + +@smallexample @c input:6B43DD4 +2004/09/30 Restaurant + Expenses:Dining $25.00 + Liabilities:MasterCard +@end smallexample + +The Dining account balance now shows $25 spent on Dining, and +a corresponding $25 owed on the MasterCard---and therefore shown as +$-25.00. The MasterCard liability shows up as negative because it +offsets the value of your assets. + +The combined total of your Assets and Liabilities is your net worth. +So to see your current net worth, use this command: + +@smallexample @c command:6B43DD4 +$ ledger balance ^assets ^liabilities +@end smallexample + +@smallexample @c output:6B43DD4 + $500.00 Assets:Checking + $-25.00 Liabilities:MasterCard +-------------------- + $475.00 +@end smallexample + +In a similar vein, your Income accounts show up negative, because they +transfer money @emph{from} an account in order to increase your +assets. Your Expenses show up positive because that is where the +money went to. The combined total of Income and Expenses is your cash +flow. A positive cash flow means you are spending more than you make, +since income is always a negative figure. To see your current cash +flow, use this command: + +@smallexample @c command:DB128F3,with_input:6B43DD4 +$ ledger balance ^income ^expenses +@end smallexample + +@smallexample @c output:DB128F3 + $25.00 Expenses:Dining + $-500.00 Income:Salary +-------------------- + $-475.00 +@end smallexample + +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 +your monthly automobile expenses: + +@smallexample @c command:DB524E4 +$ ledger -M register -f drewr3.dat expenses:auto +@end smallexample + +@smallexample @c output:DB524E4 +11-Jan-01 - 11-Jan-31 Expenses:Auto $ 5,500.00 $ 5,500.00 +@end smallexample + +This assumes, of course, that you use account names like +@samp{Expenses:Auto:Gas} and @samp{Expenses:Auto:Repair}. + +@menu +* Tracking reimbursable expenses:: +@end menu + +@node Tracking reimbursable expenses, , Assets and Liabilities, Assets and Liabilities +@subsection Tracking reimbursable expenses +@cindex reimbursable expense tracking + +Sometimes you will want to spend money on behalf of someone else, which +will eventually get repaid. Since the money is still @emph{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 @emph{to} your Assets:Reimbursements, using a different account for +each person or business that you spend money for. For example: + +@smallexample @c input:validate +2004/09/29 Circuit City + Assets:Reimbursements:Company XYZ $100.00 + Liabilities:MasterCard +@end smallexample + +This shows $100.00 spent on a MasterCard at Circuit City, with the +expense was made on behalf of Company XYZ. Later, when Company XYZ +pays the amount back, the money will transfer from that reimbursement +account back to a regular asset account: + +@smallexample @c input:validate +2004/09/29 Company XYZ + Assets:Checking $100.00 + Assets:Reimbursements:Company XYZ +@end smallexample + +This deposits the money owed from Company XYZ into a checking account, +presumably because they paid the amount 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 posting is best handled with mirrored postings 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 postings, so those are what is shown here. First, the +personal transaction, which shows the need for reimbursement: + +@smallexample @c input:validate +2004/09/29 Circuit City + Assets:Reimbursements:Company XYZ $100.00 + Liabilities:MasterCard +@end smallexample + +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 +transaction should be immediately followed by an equivalent +transaction, which shows the kind of expense, and also notes the fact +that $100.00 is now payable to you: + +@smallexample @c input:validate +2004/09/29 Circuit City + Company XYZ:Expenses:Computer:Software $100.00 + Company XYZ:Accounts Payable:Your Name +@end smallexample + +This second transaction 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 transactions can also be merged, to make things a little +clearer. Note that all amounts must be specified now: + +@smallexample @c input:validate +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 +@end smallexample + +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: + +@smallexample @c input:validate +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 +@end smallexample + +And now the reimbursements account is paid off, accounts payable is paid +off, and $100.00 has been effectively transferred from the company's +checking account to your personal checking account. The money simply +``waited''---in both @samp{Assets:Reimbursements:Company XYZ}, and +@samp{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 transactions 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 postings in +separate files, just separate the two transactions that were joined +above. For example, for both the expense and the pay-back shown +above, the following four transactions would be created. Two in your +personal ledger file: + +@smallexample @c input:990E0D1 +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 +@end smallexample + +And two in your company ledger file: + +@smallexample @c input:990E0D1 +apply account 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 + +end apply account +@end smallexample + +(Note: The @code{apply account} above means that all accounts +mentioned in the file are children of that account. In this case it +means that all activity in the file relates to Company XYZ). + +After creating these transactions, 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. + +@smallexample @c command:990E0D1 +$ ledger balance --no-total +@end smallexample + +@smallexample @c output:990E0D1 + $100.00 Assets:Checking + 0 Company XYZ + $-100.00 Assets:Checking + $100.00 Expenses:Computer:Software + $-100.00 Liabilities:MasterCard +@end smallexample + +@node Commodities and Currencies, Accounts and Inventories, Assets and Liabilities, Principles of Accounting with Ledger +@section Commodities and Currencies + +Ledger makes no assumptions about the commodities you use; it only +requires that you specify a commodity. The commodity may be any +non-numeric string that does not contain a period, comma, forward +slash or at-sign. It may appear before or after the amount, although +it is assumed that symbols appearing before the amount refer to +currencies, while non-joined symbols appearing after the amount refer +to commodities. Here are some valid currency and commodity +specifiers: + +@smallexample +$20.00 ; currency: twenty US dollars +40 AAPL ; commodity: 40 shares of Apple stock +60 DM ; currency: 60 Deutsch Mark +£50 ; currency: 50 British pounds +50 EUR ; currency: 50 Euros (or use appropriate symbol) +@end smallexample + +Ledger will examine the first use of any commodity to determine how +that commodity should be printed on reports. It pays attention to +whether the name of commodity was separated from the amount, whether +it came before or after, the precision used in specifying the amount, +whether thousand marks were used, etc. This is done so that printing +the commodity looks the same as the way you use it. + +An account may contain multiple commodities, in which case it will +have separate totals for each. For example, if your brokerage account +contains both cash, gold, and several stock quantities, the balance +might look like: + +@smallexample + $200.00 +100.00 AU + AAPL 40 + BORL 100 + FEQTX 50 Assets:Brokerage +@end smallexample + +This balance report shows how much of each commodity is in your +brokerage account. + +Sometimes, you will want to know the current street value of your +balance, and not the commodity totals. For this to happen, you must +specify what the current price is for each commodity. The price can +be any commodity, in which case the balance will be computed in terms +of that commodity. The usual way to specify prices is with a price +history file, which might look like this: + +@smallexample @c input:validate +P 2004/06/21 02:18:01 FEQTX $22.49 +P 2004/06/21 02:18:01 BORL $6.20 +P 2004/06/21 02:18:02 AAPL $32.91 +P 2004/06/21 02:18:02 AU $400.00 +@end smallexample + +@findex --price-db @var{FILE} +@findex --market +Specify the price history to use with the @option{--price-db @var{FILE}} +option, with the @option{--market (-V)} option to report in terms of +current market value: + +@smallexample +$ ledger --price-db prices.db -V balance brokerage +@end smallexample + +The balance for your brokerage account will be reported in US dollars, +since the prices database uses that currency. + +@smallexample +$40880.00 Assets:Brokerage +@end smallexample + +You can convert from any commodity to any other commodity. Let's say +you had $5000 in your checking account, and for whatever reason you +wanted to know how many ounces of gold that would buy, in terms of the +current price of gold: + +@smallexample +$ ledger -T "@{1 AU@}*(O/P@{1 AU@})" balance checking +@end smallexample + +Although the total expression appears complex, it is simply saying +that the reported total should be in multiples of AU units, where the +quantity is the account total divided by the price of one AU. Without +the initial multiplication, the reported total would still use the +dollars commodity, since multiplying or dividing amounts always keeps +the left value's commodity. The result of this command might be: + +@smallexample +14.01 AU Assets:Checking +@end smallexample + +@menu +* Commodity price histories:: +* Commodity equivalences:: +@end menu + +@node Commodity price histories, Commodity equivalences, Commodities and Currencies, Commodities and Currencies +@subsection Commodity price histories + +Whenever a commodity is purchased using a different commodity (such as +a share of common stock using dollars), it establishes a price for +that commodity on that day. It is also possible, by recording price +details in a ledger file, to specify other prices for commodities at +any given time. Such price transactions might look like those below: + +@smallexample @c input:validate +P 2004/06/21 02:17:58 TWCUX $27.76 +P 2004/06/21 02:17:59 AGTHX $25.41 +P 2004/06/21 02:18:00 OPTFX $39.31 +P 2004/06/21 02:18:01 FEQTX $22.49 +P 2004/06/21 02:18:02 AAPL $32.91 +@end smallexample + +By default, ledger will not consider commodity prices when generating +its various reports. It will always report balances in terms of the +commodity total, rather than the current value of those commodities. +To enable pricing reports, use one of the commodity reporting options. + +@node Commodity equivalences, , Commodity price histories, Commodities and Currencies +@subsection Commodity equivalences + +Sometimes a commodity has several forms which are all equivalent. An +example of this is time. Whether tracked in terms of minutes, hours +or days, it should be possible to convert between the various forms. +Doing this requires the use of commodity equivalences. + +For example, you might have the following two postings, one which +transfers an hour of time into a @samp{Billable} account, and another +which decreases the same account by ten minutes. The resulting report +will indicate that fifty minutes remain: + +@smallexample @c input:DF3FEBE +2005/10/01 Work done for company + Billable:Client 1h + Project:XYZ + +2005/10/02 Return ten minutes to the project + Project:XYZ 10m + Billable:Client +@end smallexample + +Reporting the balance for this ledger file produces: + +@smallexample @c command:DF3FEBE +$ ledger --no-total balance Billable Project +@end smallexample + +@smallexample @c output:DF3FEBE + 50.0m Billable:Client + -50.0m Project:XYZ +@end smallexample + +@findex C + +This example works because ledger already knows how to handle seconds, +minutes and hours, as part of its time tracking support. Defining +other equivalences is simple. The following is an example that +creates data equivalences, helpful for tracking bytes, kilobytes, +megabytes, and more: + +@smallexample @c input:validate +C 1.00 Kb = 1024 b +C 1.00 Mb = 1024 Kb +C 1.00 Gb = 1024 Mb +C 1.00 Tb = 1024 Gb +@end smallexample + +Each of these definitions correlates a commodity (such as @samp{Kb}) +and a default precision, with a certain quantity of another commodity. +In the above example, kilobytes are reported with two decimal places +of precision and each kilobyte is equal to 1024 bytes. + +Equivalence chains can be as long as desired. Whenever a commodity +would report as a decimal amount (less than @samp{1.00}), the next +smallest commodity is used. If a commodity could be reported in terms +of a higher commodity without resulting to a partial fraction, then +the larger commodity is used. + +@node Accounts and Inventories, Understanding Equity, Commodities and Currencies, Principles of Accounting with Ledger +@section Accounts and Inventories + +Since Ledger's accounts and commodity system is so flexible, you can +have accounts that don't really exist, and use commodities that no one +else recognizes. For example, let's say you are buying and selling +various items in EverQuest, and want to keep track of them using a +ledger. Just add items of whatever quantity you wish into your +EverQuest account: + +@smallexample @c input:48F4E47 +9/29 Get some stuff at the Inn + Places:Black's Tavern -3 Apples + Places:Black's Tavern -5 Steaks + EverQuest:Inventory +@end smallexample + +Now your EverQuest:Inventory has 3 apples and 5 steaks in it. The +amounts are negative, because you are taking @emph{from} Black's +Tavern in order to add to your Inventory account. Note that you don't +have to use @samp{Places:Black's Tavern} as the source account. You +could use @samp{EverQuest:System} to represent the fact that you +acquired them online. The only purpose for choosing one kind of +source account over another is to generate more informative reports +later on. The more you know, the better the analysis you can perform. + +If you later sell some of these items to another player, the +transaction would look like: + +@smallexample @c input:48F4E47 +10/2 Sturm Brightblade + EverQuest:Inventory -2 Steaks + EverQuest:Inventory 15 Gold +@end smallexample + +Now you've turned 2 steaks into 15 gold, courtesy of your customer, +Sturm Brightblade. + +@smallexample @c command:48F4E47 +$ ledger balance EverQuest +@end smallexample + +@smallexample @c output:48F4E47 + 3 Apples + 15 Gold + 3 Steaks EverQuest:Inventory +@end smallexample + +@node Understanding Equity, Dealing with Petty Cash, Accounts and Inventories, Principles of Accounting with Ledger +@section Understanding Equity + +The most confusing transaction in any ledger will be your equity +account---because starting balances can't come out of nowhere. + +When you first start your ledger, you will likely already have money +in some of your accounts. Let's say there's $100 in your checking +account; then add a transaction to your ledger to reflect this amount. +Where will the money come from? The answer: your equity. + +@smallexample @c input:validate +10/2 Opening Balance + Assets:Checking $100.00 + Equity:Opening Balances +@end smallexample + +But what is equity? You may have heard of equity when people talked +about house mortgages, as ``the part of the house that you own''. +Basically, equity is like the value of something. If you own a car +worth $5000, then you have $5000 in equity in that car. In order to +turn that car (a commodity) into a cash flow, or a credit to your bank +account, you will have to debit the equity by selling it. + +When you start a ledger, you probably already have a net worth. +Your net worth is your current equity. By transferring the money in +the ledger from your equity to your bank accounts, you are crediting +the ledger account based on your prior equity. That is why, when you +look at the balance report, you will see a large negative number for +Equity that never changes: Because that is what you were worth (what +you debited from yourself in order to start the ledger) before the +money started moving around. If the total positive value of your +assets is greater than the absolute value of your starting equity, it +means you are making money. + +Clear as mud? Keep thinking about it. Until you figure it out, put +@code{not Equity} at the end of your balance command, to remove the +confusing figure from the total. + +@node Dealing with Petty Cash, Working with multiple funds and accounts, Understanding Equity, Principles of Accounting with Ledger +@section Dealing with Petty Cash + +Something that stops many people from keeping a ledger at all is the +insanity of tracking small cash expenses. They rarely generate a +receipt, and there are often a lot of small postings, rather than +a few large ones, as with checks. + +One solution is: don't bother. Move your spending to a debit card, +but in general ignore cash. Once you withdraw it from the ATM, mark +it as already spent to an @samp{Expenses:Cash} category: + +@smallexample @c input:validate +2004/03/15 ATM + Expenses:Cash $100.00 + Assets:Checking +@end smallexample + +If at some point you make a large cash expense that you want to track, +just @emph{move} the amount of the expense from @samp{Expenses:Cash} +into the target account: + +@smallexample @c input:validate +2004/03/20 Somebody + Expenses:Food $65.00 + Expenses:Cash +@end smallexample + +This way, you can still track large cash expenses, while ignoring all +of the smaller ones. + +@node Working with multiple funds and accounts, , Dealing with Petty Cash, Principles of Accounting with Ledger +@section Working with multiple funds and accounts + +There are situations when the accounts you're tracking are different +between your clients and the financial institutions where money is +kept. An example of this is working as the treasurer for a religious +institution. From the secular point of view, you might be working +with three different accounts: + +@itemize +@item Checking +@item Savings +@item Credit Card +@end itemize + +From a religious point of view, the community expects to divide its +resources into multiple ``funds'', from which it makes purchases or +reserves resources for later: + +@itemize +@item School fund +@item Building fund +@item Community fund +@end itemize + +The problem with this kind of setup is that, when you spend money, it +comes from two or more places at once: the account and the fund. And +yet, the correlation of amounts between funds and accounts is rarely +one-to-one. What if the school fund has @samp{$500.00}, but +@samp{$400.00} of that comes from Checking, and @samp{$100.00} from +Savings? + +Traditional finance packages require that the money reside in only one +place. But there are really two ``views'' of the data: from the +account point of view and from the fund point of view---yet both sets +should reflect the same overall expenses and cash flow. It's simply +where the money resides that differs. + +This situation can be handled in one of two ways. The first is using +virtual postings to represent the fact that money is moving to and +from two kind of accounts at the same time: + +@smallexample @c input:396F24E +2004/03/20 Contributions + Assets:Checking $500.00 + Income:Donations + +2004/03/25 Distribution of donations + [Funds:School] $300.00 + [Funds:Building] $200.00 + [Assets:Checking] $-500.00 +@end smallexample + +The use of square brackets in the second transaction ensures that the +virtual postings balance to zero. Now money can be spent directly +from a fund at the same time as money is drawn from a physical +account: + +@smallexample @c input:396F24E +2004/03/25 Payment for books (paid from Checking) + Expenses:Books $100.00 + Assets:Checking $-100.00 + (Funds:School) $-100.00 +@end smallexample + +The use of round brackets creates a virtual posting without ensuring +a balance to zero. When reports are generated, by default they'll +appear in terms of the funds. In this case, you will likely want to +mask out your @samp{Assets} account, because otherwise the balance +won't make much sense: + +@smallexample @c command:396F24E +$ ledger --no-total bal not ^Assets +@end smallexample + +@smallexample @c output:396F24E + $100.00 Expenses:Books + $400.00 Funds + $200.00 Building + $200.00 School + $-500.00 Income:Donations +@end smallexample + +@findex --real +If the @option{--real} option is used, the report will be in terms of +the real accounts: + +@smallexample @c command:2F1CB75,with_input:396F24E +$ ledger --real --no-total bal +@end smallexample + +@smallexample @c output:2F1CB75 + $400.00 Assets:Checking + $100.00 Expenses:Books + $-500.00 Income:Donations +@end smallexample + +If more asset accounts are needed as the source of a posting, just +list them as you would normally, for example: + +@smallexample @c input:validate +2004/03/25 Payment for books (paid from Checking) + Expenses:Books $100.00 + Assets:Checking $-50.00 + Liabilities:Credit Card $-50.00 + (Funds:School) $-100.00 +@end smallexample + +The second way of tracking funds is to use transaction codes. In this +respect the codes become like virtual accounts that embrace the entire +set of postings. Basically, we are associating a transaction with a +fund by setting its code. Here are two transactions that deposit money +into, and spend money from, the @samp{Funds:School} fund: + +@smallexample @c input:AD068BA +2004/03/25 (Funds:School) Donations + Assets:Checking $100.00 + Income:Donations + +2004/03/25 (Funds:Building) Donations + Assets:Checking $20.00 + Income:Donations + +2004/04/25 (Funds:School) Payment for books + Expenses:Books $50.00 + Assets:Checking +@end smallexample + +Note how the accounts now relate only to the real accounts, and any +balance or register reports will reflect this. That the transactions +relate to a particular fund is kept only in the code. + +@findex --payee=code +@findex --by-payee +How does this become a fund report? By using the +@option{--payee=code} option, you can generate a register report +where the payee for each posting shows the code. Alone, this is not +terribly interesting; but when combined with the @option{--by-payee +(-P)} option, you will now see account subtotals for any postings +related to a specific fund. So, to see the current monetary balances of +all funds, the command would be: + +@smallexample @c command:AD068BA +$ ledger --payee=code -P reg ^Assets +@end smallexample + +@smallexample @c output:AD068BA +04-Mar-25 Funds:Building Assets:Checking $20.00 $20.00 +04-Mar-25 Funds:School Assets:Checking $50.00 $70.00 +@end smallexample + +Or to see a particular fund's expenses, the @samp{School} fund in this +case: + +@smallexample @c command:E30B2FC,with_input:AD068BA +$ ledger --payee=code -P reg ^Expenses and code School +@end smallexample + +@smallexample @c output:E30B2FC +04-Apr-25 Funds:School Expenses:Books $50.00 $50.00 +@end smallexample + +Both approaches yield different kinds of flexibility, depending on how +you prefer to think of your funds: as virtual accounts, or as tags +associated with particular transactions. Your own tastes will decide +which is best for your situation. + +@node Keeping a Journal, Transactions, Principles of Accounting with Ledger, Top +@chapter Keeping a Journal + +The most important part of accounting is keeping a good journal. If +you have a good journal, tools can be written to work whatever +mathematical tricks you need to better understand your spending +patterns. Without a good journal, no tool, however smart, can help +you. + +The Ledger program aims at making journal transactions as simple as +possible. Since it is a command-line tool, it does not provide a user +interface for keeping a journal. If you require an user interface to +maintain journal transactions GnuCash is a good alternative. + +If you are not using GnuCash, but a text editor to maintain your +journal, read on. Ledger has been designed to make data transactions +as simple as possible, by keeping the journal format easy, and also by +automagically determining as much information as possible based on the +nature of your transactions. + +For example, you do not need to tell Ledger about the accounts you +use. Any time Ledger sees a posting involving an account it knows +nothing about, it will create it@footnote{This also means if you +misspell an account it will end up getting counted separately from +what you intended. An Emacs major mode +@uref{https://github.com/ledger/ledger-mode/, ledger-mode} provides +tab completion for automatically filling in account names.}. If you +use a commodity that is new to Ledger, it will create that commodity, +and determine its display characteristics (placement of the symbol +before or after the amount, display precision, etc.) based on how you +used the commodity in the posting. + +@menu +* The Most Basic Entry:: +* Starting up:: +* Structuring your Accounts:: +* Commenting on your Journal:: +* Currency and Commodities:: +* Keeping it Consistent:: +* Journal Format:: +* Converting from other formats:: +* Archiving Previous Years:: +@end menu + +@node The Most Basic Entry, Starting up, Keeping a Journal, Keeping a Journal +@section The Most Basic Entry + +Here is the Pacific Bell example from above, given as a Ledger +posting, with the addition of a check number: + +@smallexample @c input:validate +9/29 (1023) Pacific Bell + Expenses:Utilities:Phone $23.00 + Assets:Checking $-23.00 +@end smallexample + +As you can see, it is very similar to what would be written on paper, +minus the computed balance totals, and adding in account names that +work better with Ledger's scheme of things. In fact, since Ledger is +smart about many things, you don't need to specify the balanced +amount, if it is the same as the first line: + +@smallexample @c input:validate +9/29 (1023) Pacific Bell + Expenses:Utilities:Phone $23.00 + Assets:Checking +@end smallexample + +For this transaction, Ledger will figure out that $-23.00 must come +from @samp{Assets:Checking} in order to balance the transaction. + +Also note the structure of the account entries. There is an implied +hierarchy established by separating with colons (@pxref{Structuring your +Accounts}). + +@cindex spaces in postings +@cindex posting format details + +@strong{The format is very flexible and it isn't necessary that you +indent and space out things exactly as shown. The only requirements +are that the start of the transaction (the date typically) is at the +beginning of the first line of the transaction, and the accounts are +indented by at least one space. If you omit the leading spaces in the +account lines Ledger will generate an error. There must be at least +two spaces, or a tab, between the amount and the account. If you do +not have adequate separation between the amount and the account Ledger +will give an error and stop calculating.} + +@node Starting up, Structuring your Accounts, The Most Basic Entry, Keeping a Journal +@section Starting up +@cindex initial equity +@cindex beginning ledger +@cindex opening balance + +Unless you have recently arrived from another planet, you already have +a financial state. You need to capture that financial state so that +Ledger has a starting point. + +At some convenient point in time you knew the balances and outstanding +obligation of every financial account you have. Those amounts form the +basis of the opening entry for ledger. For example if you chose the +beginning of 2011 as the date to start tracking finances with ledger, +your opening balance entry could look like this: + +@smallexample @c input:validate +2011/01/01 * Opening Balance + Assets:Joint Checking $800.14 + Assets:Other Checking $63.44 + Assets:Savings $2805.54 + Assets:Investments:401K:Deferred 100.0000 VIFSX @@ $80.5227 + Assets:Investments:401K:Matching 50.0000 VIFSX @@ $83.7015 + Assets:Investments:IRA 250.0000 VTHRX @@ $20.5324 + Liabilities:Mortgage $-175634.88 + Liabilities:Car Loan $-3494.26 + Liabilities:Visa -$1762.44 + Equity:Opening Balances +@end smallexample + +There is nothing special about the name ``Opening Balances'' as the +payee of the account name, anything convenient that you understand will +work. + +@node Structuring your Accounts, Commenting on your Journal, Starting up, Keeping a Journal +@section Structuring your Accounts +@cindex accounts, naming +@cindex naming accounts + +There really are no requirements for how you do this, but to preserve +your sanity we suggest some very basic structure to your accounting +system. + +At the highest level you have five sorts of accounts: + +@enumerate +@item Expenses: where money goes, +@item Assets: where money sits, +@item Income: where money comes from, +@item Liabilities: money you owe, +@item Equity: the real value of your property. +@end enumerate + +Starting the structure off this way will make it simpler for you to get +answers to the questions you really need to ask about your finances. + +Beneath these top level accounts you can have any level of detail you +desire. For example, if you want to keep specific track of how much +you spend on burgers and fries, you could have the following: + +@smallexample @c input:validate +Expenses:Food:Hamburgers and Fries +@end smallexample + +@node Commenting on your Journal, Currency and Commodities, Structuring your Accounts, Keeping a Journal +@section Commenting on your Journal +@cindex comments, characters +@cindex block comments +@cindex comments, block + +Comments are generally started using a @samp{;}. However, in order to +increase compatibility with other text manipulation programs and +methods, four additional comment characters are valid if used at the +beginning of a line: @samp{#}, @samp{|}, and @samp{*} and @samp{%}. + +Block comments can be made by use @code{comment} ... @code{end +comment}. + +@smallexample @c input:validate +; This is a single line comment, +# and this, +% and this, +| and this, +* and this. + +comment + This is a block comment with + multiple lines +end comment +@end smallexample + +There are several forms of comments within a transaction, for example: + +@smallexample @c input:validate +; this is a global comment that is not applied to a specific transaction +; it can start with any of the five characters but is not included in the +; output from 'print' or 'output' + +2011/12/11 Something Sweet + ; German Chocolate Cake + ; :Broke Diet: + Expenses:Food $10.00 ; Friends: The gang + Assets:Credit Union:Checking +@end smallexample + +@noindent +The first comment is global and Ledger will not attach it to any +specific transactions. The comments within the transaction must all +start with @samp{;} and are preserved as part of the transaction. The +@samp{:} indicates meta-data and tags (@pxref{Metadata}). + +@node Currency and Commodities, Keeping it Consistent, Commenting on your Journal, Keeping a Journal +@section Currency and Commodities +@cindex currency +@cindex commodity + +Ledger is agnostic when it comes to how you value your accounts. +Dollars, Euros, Pounds, Francs, Shares etc. are all just ``commodities''. +Holdings in stocks, bonds, mutual funds and other financial +instruments can be labeled using whatever is convenient for you (stock +ticker symbols are suggested for publicly traded assets).@footnote{You +can track @emph{anything}, even time or distance traveled. As long as +it cannot be created or destroyed inside your accounting system.} + +For the rest of this manual, we will only use the word ``commodities'' +when referring to the units on a transaction value. + +This is fundamentally different than many common accounting packages, +which assume the same currency throughout all of your accounts. This +means if you typically operate in Euros, but travel to the US and have +some expenses, you would have to do the currency conversion +@emph{before} you made the entry into your financial system. With +ledger this is not required. In the same journal you can have entries +in any or all commodities you actually hold. You can use the +reporting capabilities to convert all commodities to a single +commodity for reporting purposes without ever changing the underlying +entry. + +For example, the following entries reflect transactions made for a +business trip to Europe from the US: + +@smallexample @c input:82150D9 +2011/09/23 Cash in Munich + Assets:Cash €50.00 + Assets:Checking $-66.00 + +2011/09/24 Dinner in Munich + Expenses:Business:Travel €35.00 + Assets:Cash +@end smallexample + +This says that $66.00 came out of checking and turned into 50 +Euros. The implied exchange rate was $1.32. Then 35.00 Euros were +spent on Dinner in Munich. + +Running a ledger balance report shows: + +@smallexample @c command:82150D9 +$ ledger -f example.dat bal +@end smallexample + +@smallexample @c output:82150D9 + $-66.00 + €15.00 Assets + €15.00 Cash + $-66.00 Checking + €35.00 Expenses:Business:Travel +-------------------- + $-66.00 + €50.00 +@end smallexample + +The top two lines show my current assets as $-66.00 in checking (in +this very short example I didn't establish opening an opening balance +for the checking account) and €15.00. After spending on dinner I have +€15.00 in my wallet. The bottom line balances to zero, but is shown +in two lines since we haven't told ledger to convert commodities. + +@menu +* Naming Commodities:: +* Buying and Selling Stock:: +* Fixing Lot Prices:: +* Complete control over commodity pricing:: +@end menu + +@node Naming Commodities, Buying and Selling Stock, Currency and Commodities, Currency and Commodities +@subsection Naming Commodities + +Commodity names can have any character, including white-space. +However, if you include white-space or numeric characters, the +commodity name must be enclosed in double quotes @samp{"}: + +@smallexample @c input:validate +1999/06/09 ! Achat + Actif:SG PEE STK 49.957 "Arcancia Équilibre 454" + Actif:SG PEE STK $-234.90 + +2000/12/08 ! Achat + Actif:SG PEE STK 215.796 "Arcancia Équilibre 455" + Actif:SG PEE STK $-10742.54 +@end smallexample + +@node Buying and Selling Stock, Fixing Lot Prices, Naming Commodities, Currency and Commodities +@subsection Buying and Selling Stock +@cindex buying stock + +Buying stock is a typical example that many will use that involves +multiple commodities in the same transaction. The type of the share +(AAPL for Apple Inc.) and the share purchase price in the currency +unit you made the purchase in ($ for AAPL). Yes, the typical +convention is as follows: + +@smallexample @c input:validate +2004/05/01 Stock purchase + Assets:Broker 50 AAPL @@ $30.00 + Expenses:Broker:Commissions $19.95 + Assets:Broker $-1,519.95 +@end smallexample + +This assumes you have a brokerage account that is capable of managing +both liquid and commodity assets. Now, on the day of the sale: + +@smallexample @c input:validate +2005/08/01 Stock sale + Assets:Broker -50 AAPL @{$30.00@} @@ $50.00 + Expenses:Broker:Commissions $19.95 + Income:Capital Gains $-1,000.00 + Assets:Broker $2,480.05 +@end smallexample + +@noindent +You can, of course, elide the amount of the last posting. It is there +for clarity's sake. + +The @samp{@{$30.00@}} is a lot price. You can also use a lot date, +@samp{[2004/05/01]}, or both, in case you have several lots of the +same price/date and your taxation model is based on +longest-held-first. + +@node Fixing Lot Prices, Complete control over commodity pricing, Buying and Selling Stock, Currency and Commodities +@subsection Fixing Lot Prices +@cindex fixing lot prices +@cindex consumable commodity pricing + +Commodities that you keep in order to sell at a later time have +a variable value that fluctuates with the market prices. Commodities +that you consume should not fluctuate in value, but stay at the lot +price they were purchased at. As an extension of ``lot pricing'', you +can fix the per-unit price of a commodity. + +For example, say you buy 10 gallons of gas at $1.20. In future +``value'' reports, you don't want these gallons reported in terms of +today's price, but rather the price when you bought it. At the same +time, you also want other kinds of commodities---like stocks--- +reported in terms of today's price. + +This is supported as follows: + +@smallexample @c input:validate +2009/01/01 Shell + Expenses:Gasoline 11 GAL @{=$2.299@} + Assets:Checking +@end smallexample + +This transaction actually introduces a new commodity, @samp{GAL +@{=$2.29@}}, whose market value disregards any future changes in the +price of gasoline. + +If you do not want price fixing, you can specify this same transaction +in one of two ways, both equivalent (note the lack of the equal sign +compared to the transaction above): + +@smallexample @c input:validate +2009/01/01 Shell + Expenses:Gasoline 11 GAL @{$2.299@} + Assets:Checking + +2009/01/01 Shell + Expenses:Gasoline 11 GAL @@ $2.299 + Assets:Checking +@end smallexample + +There is no difference in meaning between these two forms. Why do +both exist, you ask? To support things like this: + +@smallexample @c input:validate +2009/01/01 Shell + Expenses:Gasoline 11 GAL @{=$2.299@} @@ $2.30 + Assets:Checking +@end smallexample + +This transaction says that you bought 11 gallons priced at $2.299 per +gallon at a @emph{cost to you} of $2.30 per gallon. Ledger +auto-generates a balance posting in this case to Equity:Capital Losses +to reflect the 1.1 cent difference, which is then balanced by +Assets:Checking because its amount is null. + +@node Complete control over commodity pricing, , Fixing Lot Prices, Currency and Commodities +@subsection Complete control over commodity pricing +@findex --market +@findex --exchange @var{COMMODITY} + +Ledger allows you to have very detailed control over how your +commodities are valued. You can fine tune the results given using the +@option{--market} or @option{--exchange @var{COMMODITY}} options. There +are now several points of interception; you can specify the valuation +method: + +@enumerate +@item on a commodity itself, +@item on a posting, via metadata (effect is largely the same as #1), +@item on an xact, which then applies to all postings in that xact, +@item on any posting via an automated transaction, +@item on a per-account basis, +@item on a per-commodity basis, +@item by changing the journal default of @code{market}. +@end enumerate + +Fixated pricing (such as @samp{@{=$20@}}) still plays a role in this +scheme. As far as valuation goes, it's shorthand for writing +@samp{((s,d,t -> market($20,d,t)))}. + +A valuation function receives three arguments: + +@table @code + +@item source +A string identifying the commodity whose price is being asked for +(example: @samp{EUR}). + +@item date +The reference date the price should be relative. + +@item target +A string identifying the ``target'' commodity, or the commodity the +returned price should be in. This argument is null if @option{--market} +was used instead of @option{--exchange @var{COMMODITY}}. + +@end table + +The valuation function should return an amount. If you've written +your function in Python, you can return something like +@samp{Amount("$100")}. If the function returns an explicit value, +that value is always used, regardless of the commodity, the date, or +the desired target commodity. For example, + +@smallexample @c input:validate +define myfunc_seven(s, d, t) = 7 EUR +@end smallexample + +In order to specify a fixed price, but still valuate that price into +the target commodity, use something like this: + +@smallexample @c input:validate +define myfunc_five(s, d, t) = market(5 EUR, d, t) +@end smallexample + +The @code{value} directive sets the valuation used for all commodities +used in the rest of the data stream. This is the fallback, if nothing +more specific is found. + +@smallexample @c input:validate +value myfunc_seven +@end smallexample + +You can set a specific valuation function on a per-commodity basis. +Instead of defining a function, you can also pass a lambda. + +@smallexample @c input:validate +commodity $ + value s, d, t -> 6 EUR +@end smallexample + +Each account can also provide a default valuation function for any +commodities transferred to that account. + +@smallexample @c input:validate +account Expenses:Food5 + value myfunc_five +@end smallexample + +The metadata field @samp{Value}, if found, overrides the valuation +function on a transaction-wide or per-posting basis. + +@smallexample @c input:validate += @@XACT and Food + ; Value:: 8 EUR + (Equity) $1 + += @@POST and Dining + (Expenses:Food9) $1 + ; Value:: 9 EUR +@end smallexample + +Lastly, you can specify the valuation function/value for any specific +amount using the @samp{(( ))} commodity annotation. + +@smallexample @c input:814A366 +2012-03-02 KFC + Expenses:Food2 $1 ((2 EUR)) + Assets:Cash2 + +2012-03-03 KFC + Expenses:Food3 $1 + ; Value:: 3 EUR + Assets:Cash3 + +2012-03-04 KFC + ; Value:: 4 EUR + Expenses:Food4 $1 + Assets:Cash4 + +2012-03-05 KFC + Expenses:Food5 $1 + Assets:Cash5 + +2012-03-06 KFC + Expenses:Food6 $1 + Assets:Cash6 + +2012-03-07 KFC + Expenses:Food7 1 CAD + Assets:Cash7 + +2012-03-08 XACT + Expenses:Food8 $1 + Assets:Cash8 + +2012-03-09 POST + Expenses:Dining9 $1 + Assets:Cash9 +@end smallexample + +@smallexample @c command:814A366 +$ ledger reg -V food +@end smallexample + +@smallexample @c output:814A366 +12-Mar-02 KFC Expenses:Food2 2 EUR 2 EUR +12-Mar-03 KFC Expenses:Food3 3 EUR 5 EUR +12-Mar-04 KFC Expenses:Food4 4 EUR 9 EUR +12-Mar-05 KFC Expenses:Food5 $1 $1 + 9 EUR +12-Mar-06 KFC Expenses:Food6 $1 $2 + 9 EUR +12-Mar-07 KFC Expenses:Food7 1 CAD $2 + 1 CAD + 9 EUR +12-Mar-08 XACT Expenses:Food8 $1 $3 + 1 CAD + 9 EUR +@end smallexample + +@node Keeping it Consistent, Journal Format, Currency and Commodities, Keeping a Journal +@section Keeping it Consistent +@findex --strict +@findex accounts + +Sometimes Ledger's flexibility can lead to difficulties. Using a +freeform text editor to enter transactions makes it easy to keep the +data, but also easy to enter accounts or payees inconsistently or with +spelling errors. + +In order to combat inconsistency you can define allowable accounts and +payees. For simplicity, create a separate text file and define accounts +and payees like + +@smallexample @c input:validate +account Expenses +account Expenses:Utilities +@end smallexample + +Using the @option{--strict} option will cause Ledger to complain if any +accounts are not previously defined: + +@smallexample +$ ledger bal --strict +Warning: "FinanceData/Master.dat", line 6: Unknown account 'Liabilities:Tithe Owed' +Warning: "FinanceData/Master.dat", line 8: Unknown account 'Liabilities:Tithe Owed' +Warning: "FinanceData/Master.dat", line 15: Unknown account 'Allocation:Equities:Domestic' +@end smallexample + +If you have a large Ledger register already created use the +@command{accounts} command to get started: + +@smallexample @c command:validate +$ ledger accounts >> Accounts.dat +@end smallexample + +@noindent +You will have to edit this file to add the @code{account} directive in +front of every line. + +@node Journal Format, Converting from other formats, Keeping it Consistent, Keeping a Journal +@section Journal Format + +The ledger file format is quite simple, but also very flexible. It +supports many options, though typically the user can ignore most of +them. They are summarized below. + +@menu +* Transactions and Comments:: +* Command Directives:: +@end menu + +@node Transactions and Comments, Command Directives, Journal Format, Journal Format +@subsection Transactions and Comments + +The initial character of each line determines what the line means, and +how it should be interpreted. Allowable initial characters are: + +@table @code + +@item NUMBER +A line beginning with a number denotes a transaction. It may be +followed by any number of lines, each beginning with white-space, to +denote the transaction's account postings. The format of the first +line is: + +@smallexample +DATE[=EDATE] [*|!] [(CODE)] DESC +@end smallexample + +If @samp{*} appears after the date (with optional effective date), it +indicates the transaction is ``cleared'', which can mean whatever the +user wants it to mean. If @samp{!} appears after the date, it +indicates the transaction is ``pending''; i.e., tentatively cleared +from the user's point of view, but not yet actually cleared. If +a @code{CODE} appears in parentheses, it may be used to indicate +a check number, or the type of the posting. Following these is the +payee, or a description of the posting. + +The format of each following posting is: + +@smallexample + ACCOUNT AMOUNT [; NOTE] +@end smallexample + +The @code{ACCOUNT} may be surrounded by parentheses if it is a virtual +posting, or square brackets if it is a virtual posting that +must balance. The @code{AMOUNT} can be followed by a per-unit +posting cost, by specifying @code{@@ AMOUNT}, or a complete +posting cost with @code{@@@@ AMOUNT}. Lastly, the @code{NOTE} may +specify an actual and/or effective date for the posting by using +the syntax @code{[ACTUAL_DATE]} or @code{[=EFFECTIVE_DATE]} or +@code{[ACTUAL_DATE=EFFECTIVE_DATE]} (@pxref{Virtual postings}). + +@item P +@findex --download +@findex P +@cindex historical prices +Specifies a historical price for a commodity. These are usually found +in a pricing history file (see the @option{--download (-Q)} option). +The syntax is: + +@smallexample +P DATE SYMBOL PRICE +@end smallexample + +@item = +@findex = +@cindex automated transaction +@cindex transaction, automated +An automated transaction. A value expression must appear after the +equal sign. + +After this initial line there should be a set of one or more postings, +just as if it were a normal transaction. If the amounts of the postings +have no commodity, they will be applied as multipliers to whichever real +posting is matched by the value expression (@pxref{Automated +Transactions}). + +@item ~ +@findex ~ +@cindex periodic transaction +@cindex transaction, periodic +A periodic transaction. A period expression must appear after the tilde. + +After this initial line there should be a set of one or more +postings, just as if it were a normal transaction. + +@item ; # % | * +@findex comment +@cindex comments +A line beginning with a semicolon, pound, percent, bar or asterisk +indicates a comment, and is ignored. Comments will not be returned in +a ``print'' response. + +@item indented ; +@cindex tags +If the semicolon is indented and occurs inside a transaction, it is +parsed as a persistent note for its preceding category. These notes or +tags can be used to augment the reporting and filtering capabilities of +Ledger. + +@end table + +@node Command Directives, , Transactions and Comments, Journal Format +@subsection Command Directives +@findex --strict +@findex --pedantic + +@table @code + +@item beginning of line +Command directives must occur at the beginning of a line. Use of +@samp{!} and @samp{@@} is deprecated. + +@item account +@findex account +@cindex pre-declare account +Pre-declare valid account names. This only has an effect if +@option{--strict} or @option{--pedantic} is used (see below). The +@code{account} directive supports several optional sub-directives, if +they immediately follow the account directive and if they begin with +whitespace: + +@smallexample @c input:validate +account Expenses:Food + note This account is all about the chicken! + alias food + payee ^(KFC|Popeyes)$ + check commodity == "$" + assert commodity == "$" + eval print("Hello!") + default +@end smallexample + +The @code{note} sub-directive associates a textual note with the +account. This can be accessed later using the @code{note} value +expression function in any account context. + +The @code{alias} sub-directive, which can occur multiple times, allows +the alias to be used in place of the full account name anywhere that +account names are allowed. + +The @code{payee} sub-directive, which can occur multiple times, +provides regexes that identify the account if that payee is +encountered and an account within its transaction ends in the name +"Unknown". Example: + +@smallexample @c input:validate +2012-02-27 KFC + Expenses:Unknown $10.00 ; Read now as "Expenses:Food" + Assets:Cash +@end smallexample + +The @code{check} and @code{assert} directives warn or raise an error +(respectively) if the given value expression evaluates to false within +the context of any posting. + +The @code{eval} directive evaluates the value expression in the +context of the account at the time of definition. At the moment this +has little value. + +The @code{default} directive specifies that this account should be +used as the ``balancing account'' for any future transactions that +contain only a single posting. + +@item apply account +@findex apply account +@c instance_t::master_account_directive +Sets the root for all accounts following this directive. Ledger +supports a hierarchical tree of accounts. It may be convenient to +keep two ``root accounts''. For example you may be tracking your +personal finances and your business finances. In order to keep them +separate you could preface all personal accounts with @samp{personal:} +and all business accounts with @samp{business:}. You can easily split +out large groups of transactions without manually editing them using +the account directive. For example: + +@smallexample @c input:validate +apply account Personal +2011/11/15 Supermarket + Expenses:Groceries $ 50.00 + Assets:Checking +@end smallexample + +Would result in all postings going into +@samp{Personal:Expenses:Groceries} and @samp{Personal:Assets:Checking} +until an @samp{end apply account} directive was found. + +@item apply fixed +@findex fixed +@cindex fixated prices +@c instance_t::fixed_directive in textual.cc + +A fixed block is used to set fixated prices (@pxref{Fixated prices and +costs}) for a series of transactions. It's purely a typing saver, for +use when entering many transactions with fixated prices. + +Thus, the following: + +@smallexample @c input:validate +apply fixed CAD $0.90 +2012-04-10 Lunch in Canada + Assets:Wallet -15.50 CAD + Expenses:Food 15.50 CAD + +2012-04-11 Second day Dinner in Canada + Assets:Wallet -25.75 CAD + Expenses:Food 25.75 CAD +end apply fixed +@end smallexample + +is equivalent to this: + +@smallexample @c input:validate +2012-04-10 Lunch in Canada + Assets:Wallet -15.50 CAD @{=$0.90@} + Expenses:Food 15.50 CAD @{=$0.90@} + +2012-04-11 Second day Dinner in Canada + Assets:Wallet -25.75 CAD @{=$0.90@} + Expenses:Food 25.75 CAD @{=$0.90@} +@end smallexample + +@item alias +@findex alias +@cindex account, alias +@c instance_t::alias_directive +Define an alias for an account name. If you have a deeply nested tree +of accounts, it may be convenient to define an alias, for example: + +@smallexample @c input:94A99E8 +alias Dining=Expenses:Entertainment:Dining +alias Checking=Assets:Credit Union:Joint Checking Account + +2011/11/28 YummyPalace + Dining $10.00 + Checking +@end smallexample + +The aliases are only in effect for transactions read in after the alias +is defined and are affected by @code{account} directives that precede +them. + +@smallexample @c command:94A99E8 +$ ledger bal --no-total ^Exp +@end smallexample + +@smallexample @c output:94A99E8 + $10.00 Expenses:Entertainment:Dining +@end smallexample + +With the option @option{--recursive-aliases}, aliases can refer to other +aliases, the following example produces exactly the same transactions +and account names as the preceding one: + +@smallexample @c input:83E1FB3 +alias Entertainment=Expenses:Entertainment +alias Dining=Entertainment:Dining +alias Checking=Assets:Credit Union:Joint Checking Account + +2011/11/30 ChopChop + Dining $10.00 + Checking +@end smallexample + +@smallexample @c command:83E1FB3 +$ ledger balance --no-total --recursive-aliases ^Exp +@end smallexample + +@smallexample @c output:83E1FB3 + $10.00 Expenses:Entertainment:Dining +@end smallexample + +The option @option{--no-aliases} completely disables alias expansion. +All accounts are read verbatim as they are in the ledger file. + +@item assert +@findex assert +@cindex assertions +@c instance_t::assert_directive +An assertion can throw an error if a condition is not met during +Ledger's run. + +@smallexample +assert <VALUE EXPRESSION BOOLEAN RESULT> +@end smallexample + +@item bucket +@anchor{bucket} +@findex bucket +@cindex bucket +@c instance_t::default_account_directive +Defines the default account to use for balancing transactions. +Normally, each transaction has at least two postings, which must +balance to zero. Ledger allows you to leave one posting with no +amount and automatically balance the transaction in the +posting. The @code{bucket} allows you to fill in all postings and +automatically generate an additional posting to the bucket account +balancing the transaction. If any transaction is unbalanced, it +will automatically be balanced against the @code{bucket} account. +The following example sets @samp{Assets:Checking} as the bucket: + +@smallexample @c input:validate +bucket Assets:Checking +2011/01/25 Tom's Used Cars + Expenses:Auto $ 5,500.00 + +2011/01/27 Book Store + Expenses:Books $20.00 + +2011/12/01 Sale + Assets:Checking:Business $ 30.00 +@end smallexample + +@item capture +@c instance_t::account_mapping_directive +@findex capture +@findex print +@findex register + +Directs Ledger to replace any account matching a regex with the given +account. For example: + +@smallexample @c input:validate +capture Expenses:Deductible:Medical Medical +@end smallexample + +Would cause any posting with @samp{Medical} in its name to be replaced +with @samp{Expenses:Deductible:Medical}. + +Ledger will display the mapped payees in @command{print} and +@command{register} reports. + +@item check +@findex check +@cindex assertions +@c instance_t::check_directive in textual.cc +A check issues a warning if a condition is not met during Ledger's +run. + +@smallexample +check <VALUE EXPRESSION BOOLEAN RESULT> +@end smallexample + +@item comment +@findex comment +@cindex comments +@c instance_t::comment_directive in textual.cc +Start a block comment, closed by @code{end comment}. + +@item commodity +@findex commodity +@cindex pre-declare commodity +Pre-declare commodity names. This only has an effect if +@option{--strict} or @option{--pedantic} is used (see below). + +@smallexample @c input:validate +commodity $ +commodity CAD +@end smallexample + +The @code{commodity} directive supports several optional +sub-directives, if they immediately follow the commodity directive +and---if they are on successive lines---begin with whitespace: + +@smallexample @c input:validate +commodity $ + note American Dollars + format $1,000.00 + nomarket + alias USD + default +@end smallexample + +The @code{note} sub-directive associates a textual note with the +commodity. At present this has no value other than documentation. + +The @code{format} sub-directive gives you a way to tell Ledger how to +format this commodity. In the future, using this directive will disable +Ledger's observation of other ways that commodity is used, and will +provide the ``canonical'' representation. + +The @code{nomarket} sub-directive states that the commodity's price +should never be auto-downloaded. + +The @code{alias} sub-directive states that any commodity matching this +symbol is to use the commodity declared in this block. + +The @code{default} sub-directive marks this as the ``default'' commodity. + +@item define +@findex define +@c instance_t::define_directive in textual.cc +Allows you to define value expressions for future use. For example: + +@smallexample @c input:validate +define var_name=$100 + +2011/12/01 Test + Expenses (var_name*4) + Assets +@end smallexample + +The posting will have a cost of $400. + +@item end +@findex end +@c instance_t::end_directive in textual.cc +Closes block commands like @code{apply} or @code{comment}. + +@item expr +@findex expr +@c instance_t::expr_directive in textual.cc + +@item include +@findex include +@c instance_t::include_directive in textual.cc +Include the stated file as if it were part of the current file. The file +name can contain a wildcard (@samp{*}) to refer to multiple files (e.g. +@samp{bank/*.ledger}). + +@item payee +@findex payee +@c instance_t::payee_alias_mapping_directive in textual.cc +@c instance_t::payee_uuid_mapping_directive in textual.cc +@findex print +@findex register + +The @code{payee} directive supports two optional sub-directives, if they +immediately follow the payee directive and---if it is on a successive +line---begins with whitespace: + +@smallexample @c input:validate +payee KFC + alias KENTUCKY FRIED CHICKEN + uuid 2a2e21d434356f886c84371eebac6e44f1337fda +@end smallexample + +The @code{alias} sub-directive provides a regex which, if it matches +a parsed payee, the declared payee name is substituted: + +@smallexample @c input:validate +2012-02-27 KENTUCKY FRIED CHICKEN ; will be read as being 'KFC' +@end smallexample + +The @code{uuid} sub-directive specifies that a transaction with exactly +the uuid given should have the declared payee name substituted: + +@smallexample @c input:validate +2014-05-13 UNHELPFUL PAYEE ; will be read as being 'KFC' + ; UUID: 2a2e21d434356f886c84371eebac6e44f1337fda +@end smallexample + +Ledger will display the mapped payees in @command{print} and +@command{register} reports. + +@item apply tag +@findex apply tag +@c instance_t::tag_directive in textual.cc +Allows you to designate a block of transactions and assign the same +tag to all. Tags can have values and may be nested. + +@smallexample @c input:validate +apply tag hastag +apply tag nestedtag: true + +2011/01/25 Tom's Used Cars + Expenses:Auto $ 5,500.00 + ; :nobudget: + Assets:Checking + +2011/01/27 Book Store + Expenses:Books $20.00 + Liabilities:MasterCard + +end apply tag + +2011/12/01 Sale + Assets:Checking:Business $ 30.00 + Income:Sales + +end apply tag +@end smallexample + +@noindent +is the equivalent of: + +@smallexample @c input:validate +2011/01/25 Tom's Used Cars + ; :hastag: + ; nestedtag: true + Expenses:Auto $ 5,500.00 + ; :nobudget: + Assets:Checking + +2011/01/27 Book Store + ; :hastag: + ; nestedtag: true + Expenses:Books $20.00 + Liabilities:MasterCard + +2011/12/01 Sale + ; :hastag: + Assets:Checking:Business $ 30.00 + Income:Sales +@end smallexample + +@c TODO: the following paragraph seems to be false, the automated tests +@c fail, if anything appears after end apply tag. + +@c Note that anything following @code{end apply tag} is ignored. Placing +@c the name of the tag that is being closed is a simple way to keep +@c track. + +@item tag +@findex tag +@cindex pre-declare tag +Pre-declares tag names. This only has an effect if @option{--strict} or +@option{--pedantic} is used (see below). + +@smallexample @c input:validate +tag Receipt +tag CSV +@end smallexample + +The @code{tag} directive supports two optional sub-directives, if they +immediately follow the tag directive and---if on a successive +line---begin with whitespace: + +@smallexample @c input:validate +tag Receipt + check value =~ /pattern/ + assert value != "foobar" +@end smallexample + +The @code{check} and @code{assert} sub-directives warn or error +(respectively) if the given value expression evaluates to false within +the context of any use of the related tag. In such a context, +``value'' is bound to the value of the tag (which may be something else +but a string if typed metadata is used!). Such checks or assertions are +not called if no value is given. + +@item test +@findex test +@cindex comments +@c instance_t::comment_directive in textual.cc +This is a synonym for @code{comment} and must be closed by an +@code{end} tag. + +@item year +@findex year +@anchor{year} +@c instance_t::year_directive in textual.cc +Denotes the year used for all subsequent transactions that give a date +without a year. The year should appear immediately after the +directive, for example: @code{year 2004}. This is useful at the +beginning of a file, to specify the year for that file. If all +transactions specify a year, however, this command has no effect. + +@end table + +The following single letter commands may be at the beginning of a line +alone, for backwards compatibility with older Ledger versions. + +@table @code + +@item A +@findex A +@findex bucket +@xref{bucket}. + +@item Y +@findex Y +@findex year +@xref{year}. + +@item N SYMBOL +@findex N +Indicates that pricing information is to be ignored for a given +symbol, nor will quotes ever be downloaded for that symbol. Useful +with a home currency, such as the dollar @samp{$}. It is recommended +that these pricing options be set in the price database file, which +defaults to @file{~/.pricedb}. The syntax for this command is: + +@smallexample @c input:validate +N SYMBOL +@end smallexample + +@item D AMOUNT +@findex xact +@findex D +Specifies the default commodity to use, by specifying an amount in the +expected format. The @command{xact} command will use this commodity as +the default when none other can be determined. This command may be used +multiple times, to set the default flags for different commodities; +whichever is seen last is used as the default commodity. For example, +to set US dollars as the default commodity, while also setting the +thousands flag and decimal flag for that commodity, use: + +@smallexample @c input:validate +D $1,000.00 +@end smallexample + +@item C AMOUNT1 = AMOUNT2 +@findex C +Specifies a commodity conversion, where the first amount is given to +be equivalent to the second amount. The first amount should use the +decimal precision desired during reporting: + +@smallexample @c input:validate +C 1.00 Kb = 1024 bytes +@end smallexample + +@item I, i, O, o, b, h +@findex I +@findex i +@findex O +@findex o +@findex b +@findex h +These four relate to timeclock support, which permits Ledger to read +timelog files. See timeclock's documentation for more info on the +syntax of its timelog files. + +@end table + +@node Converting from other formats, Archiving Previous Years, Journal Format, Keeping a Journal +@section Converting from other formats +@cindex csv importing + +There are numerous tools to help convert various formats to a Ledger +file. Most banks will generate a comma separated values file that can +easily be parsed into Ledger format using one of those tools. Some of +the most popular tools are: + +@itemize +@item @code{ledger convert download.csv} +@item @code{hledger -f checking.csv print} +@item @uref{https://github.com/quentinsf/icsv2ledger, @code{icsv2ledger}} +@item @uref{https://github.com/tazzben/csvToLedger, @code{csvToLedger}} +@item @uref{https://launchpad.net/csv2ledger, @code{CSV2Ledger}} +@end itemize + +@noindent +Directly pulling information from banks is outside the scope of +Ledger's function. + +@node Archiving Previous Years, , Converting from other formats, Keeping a Journal +@section Archiving Previous Years +@findex equity +@findex print + +After a while, your journal can get to be pretty large. While this +will not slow down Ledger---it's designed to process journals very +quickly---things can start to feel ``messy''; and it's a universal +complaint that when finances feel messy, people avoid them. + +Thus, archiving the data from previous years into their own files can +offer a sense of completion, and freedom from the past. But how to +best accomplish this with the ledger program? There are two commands +that make it very simple: @command{print}, and @command{equity}. + +Let's take an example file, with data ranging from year 2000 until +2004. We want to archive years 2000 and 2001 to their own file, +leaving 2002--2004 in the current file. So, use @command{print} to +output all the earlier transactions to a file called +@file{ledger-old.dat}: + +@smallexample +$ ledger -f ledger.dat -b 2000 -e 2002 print > ledger-old.dat +@end smallexample + +Note that @option{-e} limits output to transactions @emph{before} the +date specified. + +To delete older data from the current ledger file, use @command{print} +again, this time specifying year 2002 as the starting date: + +@smallexample +$ ledger -f ledger.dat -b 2002 print > x +$ mv x ledger.dat +@end smallexample + +However, now the current file contains @emph{only} postings from 2002 +onward, which will not yield accurate present-day balances, because the +net income from previous years is no longer being tallied. To +compensate for this, we must append an equity report for the old ledger +at the beginning of the new one: + +@smallexample +$ ledger -f ledger-old.dat equity > equity.dat +$ cat equity.dat ledger.dat > x +$ mv x ledger.dat +$ rm equity.dat +@end smallexample + +Now the balances reported from @file{ledger.dat} are identical to what +they were before the data was split. + +How often should you split your ledger? You never need to, if you +don't want to. Even eighty years of data will not slow down ledger +much, and that's just using present day hardware! Or, you can keep +the previous and current year in one file, and each year before that +in its own file. It's really up to you, and how you want to organize +your finances. For those who also keep an accurate paper trail, it +might be useful to archive the older years to their own files, then +burn those files to a CD to keep with the paper records---along with +any electronic statements received during the year. In the arena of +organization, just keep in mind this maxim: Do whatever keeps you +doing it. + +@node Transactions, Building Reports, Keeping a Journal, Top +@chapter Transactions + +@menu +* Basic format:: +* Eliding amounts:: +* Auxiliary dates:: +* Codes:: +* Transaction state:: +* Transaction notes:: +* Metadata:: +* Virtual postings:: +* Expression amounts:: +* Balance verification:: +* Posting cost:: +* Explicit posting costs:: +* Posting cost expressions:: +* Total posting costs:: +* Virtual posting costs:: +* Commodity prices:: +* Prices versus costs:: +* Fixated prices and costs:: +* Lot dates:: +* Lot notes:: +* Lot value expressions:: +* Automated Transactions:: +@end menu + +@node Basic format, Eliding amounts, Transactions, Transactions +@section Basic format + +The most basic form of transaction is: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash $-20.00 +@end smallexample + +This transaction has a date, a payee or description, a target account +(the first posting), and a source account (the second posting). Each +posting specifies what action is taken related to that account. + +A transaction can have any number of postings: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash $-10.00 + Liabilities:Credit $-10.00 +@end smallexample + +@node Eliding amounts, Auxiliary dates, Basic format, Transactions +@section Eliding amounts + +The first thing you can do to make things easier is elide amounts. +That is, if exactly one posting has no amount specified, Ledger will +infer the inverse of the other postings' amounts: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash $-10.00 + Liabilities:Credit ; same as specifying $-10 +@end smallexample + +@noindent +If the other postings use multiple commodities, Ledger will copy the +empty posting N times and fill in the negated values of the various +commodities: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Expenses:Tips $2.00 + Assets:Cash EUR -10.00 + Assets:Cash GBP -10.00 + Liabilities:Credit +@end smallexample + +@noindent +This transaction is identical to writing: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Expenses:Tips $2.00 + Assets:Cash EUR -10.00 + Assets:Cash GBP -10.00 + Liabilities:Credit $-22.00 + Liabilities:Credit EUR 10.00 + Liabilities:Credit GBP 10.00 +@end smallexample + +@node Auxiliary dates, Codes, Eliding amounts, Transactions +@section Auxiliary dates +@findex --aux-date + +You can associate a second date with a transaction by following the +primary date with an equals sign: + +@smallexample @c input:validate +2012-03-10=2012-03-08 KFC + Expenses:Food $20.00 + Assets:Cash $-20.00 +@end smallexample + +What this auxiliary date means is entirely up to you. The only use +Ledger has for it is that if you specify @option{--aux-date}, then all +reports and calculations (including pricing) will use the auxiliary +date as if it were the primary date. + +@node Codes, Transaction state, Auxiliary dates, Transactions +@section Codes + +A transaction can have a textual ``code''. This has no meaning and is +only displayed by the print command. Checking accounts often use +codes like DEP, XFER, etc., as well as check numbers. This is to give +you a place to put those codes: + +@smallexample @c input:validate +2012-03-10 (#100) KFC + Expenses:Food $20.00 + Assets:Checking +@end smallexample + +@node Transaction state, Transaction notes, Codes, Transactions +@section Transaction state +@findex --cleared +@findex --uncleared +@findex --pending + +A transaction can have a ``state'': cleared, pending, or uncleared. The +default is uncleared. To mark a transaction cleared, put an asterisk +@samp{*} after the date, before the code or payee: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +@noindent +To mark it pending, use a @samp{!}: + +@smallexample @c input:validate +2012-03-10 ! KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +What these mean is entirely up to you. The @option{--cleared} option +limits reports to only cleared items, while @option{--uncleared} +shows both uncleared and pending items, and @option{--pending} shows +only pending items. + +I use cleared to mean that I've reconciled the transaction with my +bank statement, and pending to mean that I'm in the middle of +a reconciliation. + +When you clear a transaction, that's really just shorthand for +clearing all of its postings. That is: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +@noindent +Is the same as writing: + +@smallexample @c input:validate +2012-03-10 KFC + * Expenses:Food $20.00 + * Assets:Cash +@end smallexample + +@noindent +You can mark individual postings as cleared or pending, in case one +``side'' of the transaction has cleared, but the other hasn't yet: + +@smallexample @c input:validate +2012-03-10 KFC + Liabilities:Credit $100.00 + * Assets:Checking +@end smallexample + +@node Transaction notes, Metadata, Transaction state, Transactions +@section Transaction notes + +After the payee, and after at least one tab or two spaces (or a space +and a tab), which Ledger calls a ``hard separator'', you may +introduce a note about the transaction using the @samp{;} character: + +@smallexample @c input:validate +2012-03-10 * KFC ; yum, chicken... + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +@noindent +Notes can also appear on the next line, so long as that line begins +with whitespace: + +@smallexample @c input:validate +2012-03-10 * KFC ; yum, chicken... + ; and more notes... + Expenses:Food $20.00 + Assets:Cash + +2012-03-10 * KFC + ; just these notes... + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +A transaction's note is shared by all its postings. This becomes +significant when querying for metadata (see below). To specify that +a note belongs only to one posting, place it after a hard separator +after the amount, or on its own line preceded by whitespace: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 ; posting #1 note + Assets:Cash + ; posting #2 note, extra indentation is optional +@end smallexample + +@node Metadata, Virtual postings, Transaction notes, Transactions +@section Metadata +@c TODO add cindex +@c TODO https://groups.google.com/d/msg/ledger-cli/2csLPcHJ3ak/a17jOClzLTUJ +@c > Is there a way to produce a register report that lists all the transaction +@c > that contain a certain tag, and sort them based on the value of the tag? +@c ledger reg --sort "tag('foo')" %foo +@c ledger reg --group-by "tag('Employer)" Remboursement:Employer and tag Employer +@c > Is it possible to get subtotals for each tag value? +@c ledger --group-by "tag('foo')" bal +@c TODO https://groups.google.com/d/msg/ledger-cli/K9NBhNlVnYc/TDYDAWhOA5EJ + +One of Ledger's more powerful features is the ability to associate +typed metadata with postings and transactions (by which I mean all of +a transaction's postings). This metadata can be queried, displayed, +and used in calculations. + +The are two forms of metadata: plain tags, and tag/value pairs. + +@menu +* Metadata tags:: +* Metadata values:: +* Typed metadata:: +@end menu + +@node Metadata tags, Metadata values, Metadata, Metadata +@subsection Metadata tags + +To tag an item, put any word not containing whitespace between two +colons inside a comment: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; :TAG: +@end smallexample + +You can gang up multiple tags by sharing colons: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; :TAG1:TAG2:TAG3: +@end smallexample + +@menu +* Payee metadata tag:: +@end menu + +@node Payee metadata tag, , Metadata tags, Metadata tags +@subsubsection Payee metadata tag +@cindex Payee metadata tag +@findex register +@findex payees +@findex --by-payee + +``Payee'' is a special metadata field. If set on a posting, it will be +used as the payee name for that posting. This affects the +@command{register} report, the @command{payees} report, and the +@option{--by-payee} option. + +This is useful when for example you deposit 4 checks at a time to the +bank. On the bank statement, there is just one amount @samp{$400}, but +you can specify from whom each check came, as shown by example +below: + +@smallexample @c input:9B43E57 +2010-06-17 Sample + Assets:Bank $400.00 + Income:Check1 $-100.00 ; Payee: Person One + Income:Check2 $-100.00 ; Payee: Person Two + Income:Check3 $-100.00 ; Payee: Person Three + Income:Check4 $-100.00 ; Payee: Person Four +@end smallexample + +When reporting with + +@smallexample @c command:9B43E57 +$ ledger reg +@end smallexample + +it appears as: + +@smallexample @c output:9B43E57 +10-Jun-17 Sample Assets:Bank $400.00 $400.00 + Person One Income:Check1 $-100.00 $300.00 + Person Two Income:Check2 $-100.00 $200.00 + Person Three Income:Check3 $-100.00 $100.00 + Person Four Income:Check4 $-100.00 0 +@end smallexample + +This shows that they are all in the same transaction (which is why the +date is not repeated), but they have different payees now. + +If using the @option{--strict} or @option{--pedantic} options, you must +declare this tag to avoid warnings and errors. + +The payee name used with the tag is not enforced by the +@option{--check-payees} option, due to a bug: +@url{https://github.com/ledger/ledger/issues/556}. + +@node Metadata values, Typed metadata, Metadata tags, Metadata +@subsection Metadata values + +To associate a value with a tag, use the syntax ``Key: Value'', where +the value can be any string of characters. Whitespace is needed after +the colon, and cannot appear in the Key: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; MyTag: This is just a bogus value for MyTag +@end smallexample + +@node Typed metadata, , Metadata values, Metadata +@subsection Typed metadata + +If a metadata tag ends in ::, its value will be parsed as a value +expression and stored internally as a value rather than as a string. +For example, although I can specify a date textually like so: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; AuxDate: 2012/02/30 +@end smallexample + +@noindent +This date is just a string, and won't be parsed as a date unless its +value is used in a date-context (at which time the string is parsed +into a date automatically every time it is needed as a date). If on +the other hand I write this: + +@smallexample +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; AuxDate:: [2012/02/30] +@end smallexample + +@noindent +Then it is parsed as a date only once, and during parsing of the +journal file, which would let me know right away that it is an invalid +date. + +@node Virtual postings, Expression amounts, Metadata, Transactions +@section Virtual postings +@findex --real + +Ordinarily, the amounts of all postings in a transaction must balance +to zero. This is non-negotiable. It's what double-entry accounting +is all about! But there are some tricks up Ledger's sleeve... + +You can use virtual accounts to transfer amounts to an account on the +sly, bypassing the balancing requirement. The trick is that these +postings are not considered ``real'', and can be removed from all +reports using @option{--real}. + +To specify a virtual account, surround the account name with +parentheses: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + (Budget:Food) $-20.00 +@end smallexample + +If you want, you can state that virtual postings @emph{should} balance +against one or more other virtual postings by using brackets (which +look ``harder'') rather than parentheses: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + [Budget:Food] $-20.00 + [Equity:Budgets] $20.00 +@end smallexample + +@node Expression amounts, Balance verification, Virtual postings, Transactions +@section Expression amounts + +An amount is a numerical figure with a commodity, but it can also be +any value expression. To indicate this, surround the amount +expression with parentheses: + +@smallexample @c input:validate +2012-03-10 * KFC + Expenses:Food ($10.00 + $20.00) ; Ledger adds it up for you + Assets:Cash +@end smallexample + +@node Balance verification, Posting cost, Expression amounts, Transactions +@section Balance verification +@findex --permissive + +@menu +* Balance assertions:: +* Balance assignments:: +* Resetting a balance:: +* Balancing transactions:: +@end menu + +If at the end of a posting's amount (and after the cost too, if there +is one) there is an equals sign, then Ledger will verify that the +total value for that account as of that posting matches the amount +specified. See @option{--permissive} option to relax the balance assertions checks. + +There are two forms of this features: balance assertions, and balance +assignments. + +@node Balance assertions, Balance assignments, Balance verification, Balance verification +@subsection Balance assertions + +A balance assertion has this general form: + +@smallexample +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash $-20.00 = $500.00 +@end smallexample + +This simply asserts that after subtracting $20.00 from Assets:Cash, +that the resulting total matches $500.00. If not, it is an error. + +The assertion has an effect only on the specified commodity. If an account has +multiple commodities, then only the one asserted is verified: + +@smallexample +2012-03-10 KFC New York + Expenses:Food $20.00 + Assets:Cash $-20.00 = $500.00 + +2012-03-11 KFC Montreal + Expenses:Food 15.00 CAD + Assets:Cash -15.00 CAD = $500.00 +@end smallexample + +In this case, the amount in USD of cash (which has not changed) is validated. +Nothing is asserted about the current amount of Canadian dollars in @samp{Asset:Cash}. + +@subsubsection Special assertion value 0 + +The only value that can be asserted without a commodity is @samp{0}. +This results in a cross-commodities assertion, which makes it possible to +assert that an account is totally empty. + +@smallexample +2012-03-09 Fill Wallet + Revenue $20.00 + Revenue 15.00 CAD + Assets:Cash + +2012-03-10 KFC New York + Expenses:Food $20.00 + Assets:Cash $-20.00 + +2012-03-11 KFC Montreal + Expenses:Food 15.00 CAD + Assets:Cash -15.00 CAD = 0 +@end smallexample + +The last transaction will assert that we are out of cash of any sort. + +@node Balance assignments, Resetting a balance, Balance assertions, Balance verification +@subsection Balance assignments + +A balance assignment has this form: + +@smallexample +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash = $500.00 +@end smallexample + +This sets the amount of the second posting to whatever it would need to +be for the total in @samp{Assets:Cash} to be $500.00 after the posting. +If the resulting amount is not $-20.00 in this case, it is an error. + +@node Resetting a balance, Balancing transactions, Balance assignments, Balance verification +@subsection Resetting a balance + +Say your book-keeping has gotten a bit out of date, and your Ledger +balance no longer matches your bank balance. You can create an +adjustment transaction using balance assignments: + +@smallexample @c input:validate +2012-03-10 Adjustment + Assets:Cash = $500.00 + Equity:Adjustments +@end smallexample + +Since the second posting is also null, its value will become the +inverse of whatever amount is generated for the first posting. + +This is the only time in ledger when more than one posting's amount +may be empty---and then only because it's not truly empty, it is +indirectly provided by the balance assignment's value. + +@node Balancing transactions, , Resetting a balance, Balance verification +@subsection Balancing transactions +@findex --empty + +As a consequence of all the above, consider the following transaction: + +@smallexample +2012-03-10 My Broker + [Assets:Brokerage] = 10 AAPL +@end smallexample + +What this says is: set the amount of the posting to whatever value is +needed so that @samp{Assets:Brokerage} contains 10 AAPL. Then, because +this posting must balance, ensure that its value is zero. This can only +be true if Assets:Brokerage does indeed contain 10 AAPL at that point in +the input file. + +A balanced virtual transaction is used simply to indicate to Ledger that +this is not a ``real'' transaction. It won't appear in any reports +anyway (unless you use a register report with @option{--empty}). + +@node Posting cost, Explicit posting costs, Balance verification, Transactions +@section Posting cost + +When you transfer a commodity from one account to another, sometimes +it gets transformed during the transaction. This happens when you +spend money on gas, for example, which transforms dollars into gallons +of gasoline, or dollars into stocks in a company. + +In those cases, Ledger will remember the ``cost'' of that transaction +for you, and can use it during reporting in various ways. Here's an +example of a stock purchase: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL + Assets:Brokerage:Cash $-500.00 +@end smallexample + +This is different from transferring 10 AAPL shares from one account to +another, in this case you are @emph{exchanging} one commodity for +another. The resulting posting's cost is $50.00 per share. + +@node Explicit posting costs, Posting cost expressions, Posting cost, Transactions +@section Explicit posting costs + +You can make any posting's cost explicit using the @samp{@@} symbol +after the amount or amount expression: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash $-500.00 +@end smallexample + +When you do this, since Ledger can now figure out the balancing amount +from the first posting's cost, you can elide the other amount: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash +@end smallexample + +@menu +* Primary and secondary commodities:: +@end menu + +@node Primary and secondary commodities, , Explicit posting costs, Explicit posting costs +@subsection Primary and secondary commodities +@findex --market +@findex --exchange @var{COMMODITY} + +It is a general convention within Ledger that the ``top'' postings in +a transaction contain the target accounts, while the final posting +contains the source account. Whenever a commodity is exchanged like +this, the commodity moved to the target account is considered +``secondary'', while the commodity used for purchasing and tracked in +the cost is ``primary''. + +Said another way, whenever Ledger sees a posting cost of the form +"AMOUNT @@ AMOUNT", the commodity used in the second amount is marked +``primary''. + +The only meaning a primary commodity has is that the @option{--market +(-V)} flag will never convert a primary commodity into any other +commodity. @option{--exchange @var{COMMODITY} (-X)} still will, +however. + +@node Posting cost expressions, Total posting costs, Explicit posting costs, Transactions +@section Posting cost expressions + +Just as you can have amount expressions, you can have posting +expressions: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@ ($500.00 / 10) + Assets:Brokerage:Cash +@end smallexample + +You can even have both: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage (5 AAPL * 2) @@ ($500.00 / 10) + Assets:Brokerage:Cash +@end smallexample + +@node Total posting costs, Virtual posting costs, Posting cost expressions, Transactions +@section Total posting costs + +The cost figure following the @samp{@@} character specifies the +@emph{per-unit} price for the commodity being transferred. If you'd +like to specify the total cost instead, use @samp{@@@@}: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@@@ $500.00 + Assets:Brokerage:Cash +@end smallexample + +Ledger reads this as if you had written: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@ ($500.00 / 10) + Assets:Brokerage:Cash +@end smallexample + +@node Virtual posting costs, Commodity prices, Total posting costs, Transactions +@section Virtual posting costs + +Normally whenever a commodity exchange like this happens, the price of +the exchange (such as $50 per share of AAPL, above) is recorded in +Ledger's internal price history database. To prevent this from +happening in the case of an exceptional transaction, surround the +@samp{@@} or @samp{@@@@} with parentheses: + +@smallexample @c input:validate +2012-03-10 My Brother + Assets:Brokerage 1000 AAPL (@@) $1 + Income:Gifts Received +@end smallexample + +@node Commodity prices, Prices versus costs, Virtual posting costs, Transactions +@section Commodity prices +@findex --lot-prices + +When a transaction occurs that exchanges one commodity for another, +Ledger records that commodity price not only within its internal price +database, but also attached to the commodity itself. Usually this fact +remains invisible to the user, unless you turn on @option{--lot-prices} +to show these hidden price figures. + +For example, consider the stock sale given above: + +@smallexample @c input:validate +2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash +@end smallexample + +The commodity transferred into @samp{Assets:Brokerage} is not actually 10 +AAPL, but rather 10 AAPL @{$50.00@}. The figure in braces after the +amount is called the ``lot price''. It's Ledger's way of remembering +that this commodity was transferred through an exchange, and that +$50.00 was the price of that exchange. + +This becomes significant if you later sell that commodity again. For +example, you might write this: + +@smallexample @c input:validate +2012-04-10 My Broker + Assets:Brokerage:Cash + Assets:Brokerage -10 AAPL @@ $75.00 +@end smallexample + +And that would be perfectly fine, but how do you track the capital +gains on the sale? It could be done with a virtual posting: + +@smallexample @c input:validate +2012-04-10 My Broker + Assets:Brokerage:Cash + Assets:Brokerage -10 AAPL @@ $75.00 + (Income:Capital Gains) $-250.00 +@end smallexample + +But this gets messy since capital gains income is very real, and not +quite appropriate for a virtual posting. + +Instead, if you reference that same hidden price annotation, Ledger +will figure out that the price of the shares you're selling, and the +cost you're selling them at, don't balance: + +@smallexample +2012-04-10 My Broker + Assets:Brokerage:Cash $750.00 + Assets:Brokerage -10 AAPL @{$50.00@} @@ $75.00 +@end smallexample + +This transaction will fail because the $250.00 price difference +between the price you bought those shares at, and the cost you're +selling them for, does not match. The lot price also identifies which +shares you purchased on that prior date. + +@menu +* Total commodity prices:: +@end menu + +@node Total commodity prices, , Commodity prices, Commodity prices +@subsection Total commodity prices + +As a shorthand, you can specify the total price instead of the +per-share price in doubled braces. This goes well with total costs, +but is not required to be used with them: + +@smallexample @c input:validate +2012-04-10 My Broker + Assets:Brokerage:Cash $750.00 + Assets:Brokerage -10 AAPL @{@{$500.00@}@} @@@@ $750.00 + Income:Capital Gains $-250.00 +@end smallexample + +It should be noted that this is a convenience only for cases where you +buy and sell whole lots. The @{@{$500.00@}@} is @emph{not} an attribute +of the commodity, whereas @{$50.00@} is. In fact, when you write +@{@{$500.00@}@}, Ledger just divides that value by 10 and sees +@{$50.00@}. So if you use the print command to look at this +transaction, you'll see the single braces form in the output. The +double braces price form is a shorthand only. + +Plus, it comes with dangers. This works fine: + +@smallexample @c input:validate +2012-04-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash $-500.00 + +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{$50.00@} @@@@ $375.00 + Income:Capital Gains $-125.00 + +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{$50.00@} @@@@ $375.00 + Income:Capital Gains $-125.00 +@end smallexample + +@noindent +But this does not do what you might expect: + +@smallexample +2012-04-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash $-500.00 + +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{@{$500.00@}@} @@@@ $375.00 + Income:Capital Gains $-125.00 + +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{@{$500.00@}@} @@@@ $375.00 + Income:Capital Gains $-125.00 +@end smallexample + +And in cases where the amounts do not divide into whole figures and +must be rounded, the capital gains figure could be off by a cent. Use +with caution. + +@node Prices versus costs, Fixated prices and costs, Commodity prices, Transactions +@section Prices versus costs + +Because lot pricing provides enough information to infer the cost, the +following two transactions are equivalent: + +@smallexample +2012-04-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash $-500.00 + +2012-04-10 My Broker + Assets:Brokerage 10 AAPL @{$50.00@} + Assets:Brokerage:Cash $-500.00 +@end smallexample + +However, note that what you see in some reports may differ, for +example in the print report. Functionally, however, there is no +difference, and neither the register nor the balance report are +sensitive to this difference. + +@node Fixated prices and costs, Lot dates, Prices versus costs, Transactions +@section Fixated prices and costs + +If you bought a stock last year, and ask for its value today, Ledger +will consult its price database to see what the most recent price for +that stock is. You can short-circuit this lookup by ``fixing'' the +price at the time of a transaction. This is done using +@samp{@{=AMOUNT@}}: + +@smallexample +2012-04-10 My Broker + Assets:Brokerage 10 AAPL @{=$50.00@} + Assets:Brokerage:Cash $-500.00 +@end smallexample + +These 10 AAPL will now always be reported as being worth $50, no +matter what else happens to the stock in the meantime. + +Fixated prices are a special case of using lot valuation expressions +(see below) to fix the value of a commodity lot. + +Since price annotations and costs are largely interchangeable and +a matter of preference, there is an equivalent syntax for specified +fixated prices by way of the cost: + +@smallexample +2012-04-10 My Broker + Assets:Brokerage 10 AAPL @@ =$50.00 + Assets:Brokerage:Cash $-500.00 +@end smallexample + +This is the same as the previous transaction, with the same caveats +found in @ref{Prices versus costs}. + +@node Lot dates, Lot notes, Fixated prices and costs, Transactions +@section Lot dates +@findex --lot-dates + +In addition to lot prices, you can specify lot dates and reveal them +with @option{--lot-dates}. Other than that, however, they have no +special meaning to Ledger. They are specified after the amount in +square brackets (the same way that dates are parsed in value +expressions): + +@smallexample +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{$50.00@} [2012-04-10] @@@@ $375.00 + Income:Capital Gains $-125.00 +@end smallexample + +@node Lot notes, Lot value expressions, Lot dates, Transactions +@section Lot notes +@findex --lot-notes +@findex --lots + +You can also associate arbitrary notes for your own record keeping in +parentheses, and reveal them with @option{--lot-notes}. One caveat is +that the note cannot begin with an @samp{@@} character, as that would +indicate a virtual cost: + +@smallexample +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{$50.00@} [2012-04-10] (Oh my!) @@@@ $375.00 + Income:Capital Gains $-125.00 +@end smallexample + +You can specify any combination of lot prices, dates or notes, in any +order. They are all optional. + +To show all lot information in a report, use @option{--lots}. + +@node Lot value expressions, Automated Transactions, Lot notes, Transactions +@section Lot value expressions + +Normally when you ask Ledger to display the values of commodities held, +it uses a value expression called ``market'' to determine the most +recent value from its price database---even downloading prices from the +Internet, if @option{--download (-Q)} was specified and a suitable +@file{getquote} script is found on your system. + +However, you can override this valuation logic by providing +a commodity valuation expression in doubled parentheses. This +expression must result in one of two values: either an amount to +always be used as the per-share price for that commodity; or +a function taking three arguments, which is called to determine that +price. + +If you use the functional form, you can either specify a function +name, or a lambda expression. Here's a function that yields the price +as $10 in whatever commodity is being requested: + +@smallexample @c input:validate +define ten_dollars(s, date, t) = market($10, date, t) +@end smallexample + +I can now use that in a lot value expression as follows: + +@smallexample @c input:validate +2012-04-10 My Broker + Assets:Brokerage:Cash $375.00 + Assets:Brokerage -5 AAPL @{$50.00@} ((ten_dollars)) @@@@ $375.00 + Income:Capital Gains $-125.00 +@end smallexample + +Alternatively, I could do the same thing without pre-defining +a function by using a lambda expression taking three arguments: + +@smallexample +2012-04-10 My Broker + A:B:Cash $375.00 + A:B -5 AAPL @{$50.00@} ((s, d, t -> market($10, date, t))) @@@@ $375.00 + Income:Capital Gains $-125.00 +@end smallexample + +The arguments passed to these functions have the following meaning: + +@itemize + +@item source +The source commodity string, or an amount object. If it is a string, +the return value must be an amount representing the price of the +commodity identified by that string (example: @samp{$}). If it is an +amount, return the value of that amount as a new amount (usually +calculated as commodity price times source amount). + +@item date +The date to use for determining the value. If null, it means no date +was specified, which can mean whatever you want it to mean. + +@item target +If not null, a string representing the desired target commodity that the +commodity price, or repriced amount, should be valued in. Note that +this string can be a comma-separated list, and that some or all of the +commodities in that list may be suffixed with an exclamation mark, to +indicate what is being desired. + +@end itemize + +In most cases, it is simplest to either use explicit amounts in your +valuation expressions, or just pass the arguments down to @samp{market} +after modifying them to suit your needs. + +@node Automated Transactions, , Lot value expressions, Transactions +@section Automated Transactions + +An automated transaction is a special kind of transaction which adds +its postings to other transactions any time one of that other +transactions' postings matches its predicate. The predicate uses the +same query syntax as the Ledger command-line. + +Consider this posting: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +If I write this automated transaction before it in the file: + +@smallexample @c input:validate += expr true + Foo $50.00 + Bar $-50.00 +@end smallexample + +Then the first transaction will be modified during parsing as if I'd +written this: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Foo $50.00 + Bar $-50.00 + Assets:Cash $-20.00 + Foo $50.00 + Bar $-50.00 +@end smallexample + +Despite this fancy logic, automated transactions themselves follow +most of the same rules as regular transactions: their postings must +balance (unless you use a virtual posting), you can have metadata, +etc. + +One thing you cannot do, however, is elide amounts in an automated +transaction. + +@menu +* Amount multipliers:: +* Accessing the matching posting's amount:: +* Referring to the matching posting's account:: +* Applying metadata to every matched posting:: +* Applying metadata to the generated posting:: +* State flags:: +* Effective Dates:: +* Periodic Transactions:: +* Concrete Example of Automated Transactions:: +@end menu + +@node Amount multipliers, Accessing the matching posting's amount, Automated Transactions, Automated Transactions +@subsection Amount multipliers + +As a special case, if an automated transaction's posting's amount +(phew) has no commodity, it is taken as a multiplier upon the matching +posting's cost. For example: + +@smallexample @c input:validate += expr true + Foo 50.00 + Bar -50.00 + +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Then the latter transaction turns into this during parsing: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + Foo $1000.00 + Bar $-1000.00 + Assets:Cash $-20.00 + Foo $1000.00 + Bar $-1000.00 +@end smallexample + +@node Accessing the matching posting's amount, Referring to the matching posting's account, Amount multipliers, Automated Transactions +@subsection Accessing the matching posting's amount + +If you use an amount expression for an automated transaction's +posting, that expression has access to all the details of the matched +posting. For example, you can refer to that posting's amount using +the ``amount'' value expression variable: + +@smallexample @c input:validate += expr true + (Foo) (amount * 2) ; same as just "2" in this case + +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +This becomes: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + (Foo) $40.00 + Assets:Cash $-20.00 + (Foo) $-40.00 +@end smallexample + +@node Referring to the matching posting's account, Applying metadata to every matched posting, Accessing the matching posting's amount, Automated Transactions +@subsection Referring to the matching posting's account + +Sometimes you want to refer to the account that was matched +in some way within the automated transaction itself. This is +done by using the string @samp{$account}, anywhere within the +account part of the automated posting: + +@smallexample @c input:validate += food + (Budget:$account) 10 + +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Becomes: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + (Budget:Expenses:Food) $200.00 + Assets:Cash $-20.00 +@end smallexample + +@node Applying metadata to every matched posting, Applying metadata to the generated posting, Referring to the matching posting's account, Automated Transactions +@subsection Applying metadata to every matched posting + +If the automated transaction has a transaction note, that note is +copied (along with any metadata) to every posting that matches the +predicate: + +@smallexample @c input:validate += food + ; Foo: Bar + (Budget:$account) 10 + +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Becomes: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + ; Foo: Bar + (Budget:Expenses:Food) $200.00 + Assets:Cash $-20.00 +@end smallexample + +@node Applying metadata to the generated posting, State flags, Applying metadata to every matched posting, Automated Transactions +@subsection Applying metadata to the generated posting + +If the automated transaction's posting has a note, that note is +carried to the generated posting within the matched transaction: + +@smallexample @c input:validate += food + (Budget:$account) 10 + ; Foo: Bar + +2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Becomes: + +@smallexample @c input:validate +2012-03-10 KFC + Expenses:Food $20.00 + (Budget:Expenses:Food) $200.00 + ; Foo: Bar + Assets:Cash $-20.00 +@end smallexample + +This is slightly different from the rules for regular transaction +notes, in that an automated transaction's note does not apply to every +posting within the automated transaction itself, but rather to every +posting it matches. + +@node State flags, Effective Dates, Applying metadata to the generated posting, Automated Transactions +@subsection State flags + +Although you cannot mark an automated transaction as a whole as +cleared or pending, you can mark its postings with a @samp{*} or +@samp{!} before the account name, and that state flag gets carried to +the generated posting. + +@node Effective Dates, Periodic Transactions, State flags, Automated Transactions +@subsection Effective Dates +@cindex effective dates +@findex --effective + +In the real world, transactions do not take place instantaneously. +Purchases can take several days to post to a bank account. And you may +pay ahead for something for which you want to distribute costs. With +Ledger you can control every aspect of the timing of a transaction. + +Say you're in business. If you bill a customer, you can enter +something like + +@cindex effective date of invoice + +@smallexample @c input:validate +2008/01/01=2008/01/14 Client invoice ; estimated date you'll be paid + Assets:Accounts Receivable $100.00 + Income: Client name +@end smallexample + +Then, when you receive the payment, you change it to + +@smallexample @c input:validate +2008/01/01=2008/01/15 Client invoice ; actual date money received + Assets:Accounts Receivable $100.00 + Income: Client name +@end smallexample + +@noindent +and add something like + +@smallexample @c input:validate +2008/01/15 Client payment + Assets:Checking $100.00 + Assets:Accounts Receivable +@end smallexample + +Now + +@smallexample @c command:validate +$ ledger --begin 2008/01/01 --end 2008/01/14 bal Income +@end smallexample + +@noindent +gives you your accrued income in the first two weeks of the year, and + +@smallexample @c command:validate +$ ledger --effective --begin 2008/01/01 --end 2008/01/14 bal Income +@end smallexample + +@noindent +gives you your cash basis income in the same two weeks. + +Another use is distributing costs out in time. As an example, suppose +you just prepaid into a local vegetable co-op that sustains you +through the winter. It costs $225 to join the program, so you write +a check. You don't want your October grocery budget to be blown +because you bought food ahead, however. What you really want is for +the money to be evenly distributed over the next six months so that +your monthly budgets gradually take a hit for the vegetables you'll +pick up from the co-op, even though you've already paid for them. + +@smallexample @c input:6453542 +2008/10/16 * (2090) Bountiful Blessings Farm + Expenses:Food:Groceries $ 37.50 ; [=2008/10/01] + Expenses:Food:Groceries $ 37.50 ; [=2008/11/01] + Expenses:Food:Groceries $ 37.50 ; [=2008/12/01] + Expenses:Food:Groceries $ 37.50 ; [=2009/01/01] + Expenses:Food:Groceries $ 37.50 ; [=2009/02/01] + Expenses:Food:Groceries $ 37.50 ; [=2009/03/01] + Assets:Checking +@end smallexample + +This entry accomplishes this. Every month you'll see an +automatic $37.50 deficit like you should, while your checking account +really knows that it debited $225 this month. + +And using the @option{--effective} option, the initial date will be +overridden by the effective dates. + +@smallexample @c command:6453542 +$ ledger --effective register Groceries +@end smallexample + +@smallexample @c output:6453542 +08-Oct-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 37.50 +08-Nov-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 75.00 +08-Dec-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 112.50 +09-Jan-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 150.00 +09-Feb-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 187.50 +09-Mar-01 Bountiful Blessings.. Expense:Food:Groceries $ 37.50 $ 225.00 +@end smallexample + +@node Periodic Transactions, Concrete Example of Automated Transactions, Effective Dates, Automated Transactions +@subsection Periodic Transactions +@findex --budget + +A periodic transaction starts with a tilde @samp{~} followed by a period +expression (see @ref{Period Expressions}). Periodic transactions are used for budgeting and +forecasting only, they have no effect without the @option{--budget} +option specified. For examples and details, @pxref{Budgeting and +Forecasting}. + +@node Concrete Example of Automated Transactions, , Periodic Transactions, Automated Transactions +@subsection Concrete Example of Automated Transactions + +As a Bahá'í, I need to compute Huqúqu'lláh whenever I acquire assets. +It is similar to tithing for Jews and Christians, or to Zakát for +Muslims. The exact details of computing Huqúqu'lláh are somewhat +complex, but if you have further interest, please consult the Web. + +Ledger makes this otherwise difficult law very easy. Just set up an +automated posting at the top of your ledger file: + +@smallexample @c input:C371854 +; This automated transaction will compute Huqúqu'lláh based on this +; journal's postings. Any accounts that match will affect the +; Liabilities:Huququ'llah account by 19% of the value of that posting. + += /^(?:Income:|Expenses:(?:Business|Rent$|Furnishings|Taxes|Insurance))/ + (Liabilities:Huququ'llah) 0.19 +@end smallexample + +This automated posting works by looking at each posting in the +ledger file. If any match the given value expression, 19% of the +posting's value is applied to the @samp{Liabilities:Huququ'llah} +account. So, if $1000 is earned from @samp{Income:Salary}, $190 is +added to @samp{Liabilities:Huqúqu'lláh}; if $1000 is spent on Rent, +$190 is subtracted. + +@smallexample @c input:C371854 +2003/01/01 (99) Salary + Income:Salary -$1000 + Assets:Checking + +2003/01/01 (100) Rent + Expenses:Rent $500 + Assets:Checking +@end smallexample + +The ultimate balance of Huqúqu'lláh reflects how +much is owed in order to fulfill one's obligation to Huqúqu'lláh. +When ready to pay, just write a check to cover the amount shown in +@samp{Liabilities:Huququ'llah}. That transaction would look like: + +@smallexample @c input:validate +2003/01/01 (101) Baha'i Huqúqu'lláh Trust + Liabilities:Huququ'llah $1,000.00 + Assets:Checking +@end smallexample + +That's it. To see how much Huqúq is currently owed based on your +ledger transactions, use: + +@smallexample @c command:C371854 +$ ledger balance Liabilities:Huquq +@end smallexample + +@smallexample @c output:C371854 + $-95 Liabilities:Huququ'llah +@end smallexample + +This works fine, but omits one aspect of the law: that Huqúq is only +due once the liability exceeds the value of 19 mithqáls of gold (which +is roughly 2.22 ounces). So what we want is for the liability to +appear in the balance report only when it exceeds the present day +value of 2.22 ounces of gold. This can be accomplished using the +command: + +@c TODO: fix this, it doesn't work any longer +@smallexample +$ ledger -Q -t "/Liab.*Huquq/?(a/P@{2.22 AU@}<=@{-1.0@}&a):a" bal liab +@end smallexample + +With this command, the current price for gold is downloaded, and the +Huqúqu'lláh is reported only if its value exceeds that of 2.22 ounces +of gold. If you wish the liability to be reflected in the parent +subtotal either way, use this instead: + +@c TODO: fix this, it doesn't work any longer +@smallexample +$ ledger -Q -T "/Liab.*Huquq/?(O/P@{2.22 AU@}<=@{-1.0@}&O):O" bal liab +@end smallexample + +In some cases, you may wish to refer to the account of whichever posting +matched your automated transaction's value expression. To do this, use +the special account name @samp{$account}: + +@smallexample @c input:validate += /^Some:Long:Account:Name/ + [$account] -0.10 + [Savings] 0.10 +@end smallexample + +This example causes 10% of the matching account's total to be deferred +to the @samp{Savings} account---as a balanced virtual posting, which +may be excluded from reports by using @option{--real}. + +@node Building Reports, Reporting Commands, Transactions, Top +@chapter Building Reports + +@menu +* Introduction:: +* Balance Reports:: +* Typical queries:: +* Advanced Reports:: +@end menu + +@node Introduction, Balance Reports, Building Reports, Building Reports +@section Introduction + +The power of Ledger comes from the incredible flexibility in its +reporting commands, combined with formatting commands. Some options +control what is included in the calculations, and formatting controls +how it is displayed. The combinations are infinite. This chapter will +show you the basics of combining various options and commands. In the +next chapters you will find details about the specific commands and +options. + +@node Balance Reports, Typical queries, Introduction, Building Reports +@section Balance Reports + +@menu +* Controlling the Accounts and Payees:: +* Controlling Formatting:: +@end menu + +@node Controlling the Accounts and Payees, Controlling Formatting, Balance Reports, Balance Reports +@subsection Controlling the Accounts and Payees + +The balance report is the most commonly used report. The simplest +invocation is: + +@smallexample @c command:1D00D56 +$ ledger balance -f drewr3.dat +@end smallexample + +@noindent +which will print the balances of every account in your journal. + +@smallexample @c output:1D00D56 + $ -3,804.00 Assets + $ 1,396.00 Checking + $ 30.00 Business + $ -5,200.00 Savings + $ -1,000.00 Equity:Opening Balances + $ 6,654.00 Expenses + $ 5,500.00 Auto + $ 20.00 Books + $ 300.00 Escrow + $ 334.00 Food:Groceries + $ 500.00 Interest:Mortgage + $ -2,030.00 Income + $ -2,000.00 Salary + $ -30.00 Sales + $ -63.60 Liabilities + $ -20.00 MasterCard + $ 200.00 Mortgage:Principal + $ -243.60 Tithe +-------------------- + $ -243.60 +@end smallexample + +Most times, this is more than you want. Limiting the results to +specific accounts is as easy as entering the names of the accounts +after the command: + +@smallexample @c command:06B2AD4 +$ ledger balance -f drewr3.dat Auto MasterCard +@end smallexample + +@smallexample @c output:06B2AD4 + $ 5,500.00 Expenses:Auto + $ -20.00 Liabilities:MasterCard +-------------------- + $ 5,480.00 +@end smallexample + +@noindent +Note the implicit logical or between @samp{Auto} and +@samp{Mastercard}. + +If you want the entire contents of a branch of your account tree, use +the highest common name in the branch: + +@smallexample @c command:B0468E1 +$ ledger balance -f drewr3.dat Income +@end smallexample + +@smallexample @c output:B0468E1 + $ -2,030.00 Income + $ -2,000.00 Salary + $ -30.00 Sales +-------------------- + $ -2,030.00 +@end smallexample + +You can use general regular expressions in nearly any place Ledger +needs a string: + +@smallexample @c command:EAE389F +$ ledger balance -f drewr3.dat ^Bo +@end smallexample +@smallexample @c output:EAE389F +@end smallexample + +This first example looks for any account starting with @samp{Bo}, of +which there are none. + +@smallexample @c command:E2AF6AD +$ ledger balance -f drewr3.dat Bo +@end smallexample + +@smallexample @c output:E2AF6AD + $ 20.00 Expenses:Books +@end smallexample + +This second example looks for any account containing @samp{Bo}, which is +@samp{Expenses:Books}. + +@cindex limit by payees +@findex --limit @var{EXPR} + +If you want to know exactly how much you have spent in a particular +account on a particular payee, the following are equivalent: + +@smallexample @c command:validate +$ ledger balance Auto:Fuel and Chevron +@end smallexample + +@smallexample @c command:validate +$ ledger balance --limit 'account=~/Fuel/' and 'payee=~/Chev/' +@end smallexample + +@noindent +will show you the amount expended on gasoline at Chevron. The second +example is the first example of the very powerful expression language +available to shape reports. The first example may be easier to +remember, but learning to use the second will open up far more +possibilities. + +If you want to exclude specific accounts from the report, you can +exclude multiple accounts with parentheses: + +@smallexample @c command:validate +$ ledger bal Expenses and not (Expenses:Drinks or Expenses:Candy or Expenses:Gifts) +@end smallexample + +@node Controlling Formatting, , Controlling the Accounts and Payees, Balance Reports +@subsection Controlling Formatting + +These examples all use the default formatting for the balance +report. Customizing the formatting can easily allowing to see only what +you want, or interface Ledger with other programs. + +@node Typical queries, Advanced Reports, Balance Reports, Building Reports +@section Typical queries + +A query such as the following shows all expenses since last +October, sorted by total: + +@smallexample @c command:validate +$ ledger -b "last oct" -S T bal ^expenses +@end smallexample + +From left to right the options mean: Show transactions since last +October; sort by the absolute value of the total; and report the balance +for all accounts that begin with @samp{expenses}. + +@menu +* Reporting monthly expenses:: +@end menu + +@node Reporting monthly expenses, , Typical queries, Typical queries +@subsection Reporting monthly expenses +@findex --monthly +@findex --display @var{EXPR} +@findex --period-sort @var{VEXPR} +@findex --related +@findex --subtotal + +The following query makes it easy to see monthly expenses, with each +month's expenses sorted by the amount: + +@smallexample @c command:validate +$ ledger -M --period-sort "(amount)" reg ^expenses +@end smallexample + +Now, you might wonder where the money came from to pay for these things. +To see that report, add @option{--related (-r)}, which shows the +``related account'' postings: + +@smallexample @c command:validate +$ ledger -M --period-sort "(amount)" -r reg ^expenses +@end smallexample + +But maybe this prints too much information. You might just want to +see how much you're spending with your MasterCard. That kind of query +requires the use of a display predicate, since the postings +calculated must match @samp{^expenses}, while the postings +displayed must match @samp{mastercard}. The command would be: + +@smallexample @c command:validate +$ ledger -M -r --display 'account=~/mastercard/' reg ^expenses +@end smallexample + +This query says: Report monthly subtotals; report the ``related +account'' postings; display only related postings whose +account matches @samp{mastercard}, and base the calculation on +postings matching @samp{^expenses}. + +This works just as well for reporting the overall total, too: + +@smallexample @c command:validate +$ ledger -s -r --display "account=~/mastercard/" reg ^expenses +@end smallexample + +The @option{--subtotal (-s)} option subtotals all postings, just as +@option{--monthly (-M)} subtotaled by the month. The running total in +both cases is off, however, since a display expression is being used. + +@node Advanced Reports, , Typical queries, Building Reports +@section Advanced Reports + +@menu +* Asset Allocation:: +* Visualizing with Gnuplot:: +@end menu + +@node Asset Allocation, Visualizing with Gnuplot, Advanced Reports, Advanced Reports +@subsection Asset Allocation + +A very popular method of managing portfolios is to control the +percent allocation of assets by certain categories. The mix of +categories and the weights applied to them vary by investing +philosophy, but most follow a similar pattern. Tracking asset +allocation in ledger is not difficult but does require some additional +effort to describe how the various assets you own contribute to the +asset classes you want to track. + +In our simple example we assume you want to apportion your assets into +the general categories of domestic and international equities (stocks) +and a combined category of bonds and cash. For illustrative purposes, +we will use several publicly available mutual funds from Vanguard. +The three funds we will track are the Vanguard 500 IDX FD Signal +(VIFSX), the Vanguard Target Retirement 2030 (VTHRX), and the Vanguard +Short Term Federal Fund (VSGBX). Each of these funds allocates assets +to different categories of the investment universe and in different +proportions. When you buy a share of VTHRX, that share is partially +invested in equities, and partially invested in bonds and cash. Below +is the asset allocation for each of the instruments listed above: + +@multitable @columnfractions .2 .2 .3 .3 +@item @tab Domestic @tab Global @tab +@item Symbol @tab Equity @tab Equity @tab bonds/cash +@item VIFSX @tab 100% @tab @tab +@item VTHRX @tab 24.0% @tab 56.3% @tab 19.7% +@item VSGBX @tab @tab @tab 100% +@end multitable + +These numbers are available from the prospectus of any publicly +available mutual fund. Of course a single stock issue is 100% equity +and a single bond issue is 100% bonds. + +We track purchases of specific investments using the symbol of that +investment as its commodity. How do we tell Ledger that a share of +VTHRX is 24% Domestic equity? Enter automatic transactions and +virtual accounts. + +At the top of our ledger we enter automatic transactions that describe +these proportions to Ledger. In the same entries we set up virtual +accounts that let us separate these abstract calculations from our +actual balances. + +For the three instruments listed above, those automatic transactions +would look like: + +@smallexample @c input:582C8C2 += expr ( commodity == 'VIFSX' ) + (Allocation:Equities:Domestic) 1.000 + += expr ( commodity == 'VTHRX' ) + (Allocation:Equities:Global) 0.240 + (Allocation:Equities:Domestic) 0.563 + (Allocation:Bonds/Cash) 0.197 + += expr ( commodity == 'VBMFX') + (Allocation:Bonds/Cash) 1.000 + +2015-01-01 Buy VIFSX + Assets:Broker 100 VIFSX + Assets:Cash $-10000 + +2015-01-01 Buy VTHRX + Assets:Broker 10 VTHRX + Assets:Cash $-10000 + +2015-01-01 Buy VBMFX + Assets:Broker 1 VBMFX + Assets:Cash $-10000 +@end smallexample + +How do these work? First the @samp{=} sign at the beginning of the +line tells ledger this is an automatic transaction to be applied when +the condition following the @samp{=} is true. After the @samp{=} sign +is a value expression (@pxref{Value Expressions}) that returns true +any time a posting contains the commodity of interest. + +The following line gives the proportions (not percentages) of each unit +of commodity that belongs to each asset class. Whenever Ledger sees a +buy or sell of a particular commodity it will credit or debit these +virtual accounts with that proportion of the number of shares moved. + +Now that Ledger understands how to distribute the commodities amongst +the various asset classes how do we get a report that tells us our +current allocation? Using the balance command and some tricky +formatting! + +@smallexample @c command:582C8C2 +ledger bal Allocation --current --format "\ + %-17((depth_spacer)+(partial_account))\ + %10(percent(market(display_total), market(parent.total)))\ + %16(market(display_total))\n%/" +@end smallexample + +Which yields: + +@smallexample @c output:582C8C2 + Allocation 100.00% $30000 + Bonds/Cash 39.90% $11970 + Equities 60.10% $18030 + Domestic 86.69% $15630 + Global 13.31% $2400 +@end smallexample + +Let's look at the Ledger invocation a bit closer. The command above is +split into lines for clarity. The first line is very vanilla Ledger +asking for the current balances of the account in the ``Allocation'' +tree, using a special formatter. + +@cindex depth_spacer +@cindex display_total +@cindex parent.total +The magic is in the formatter. The second line simply tells Ledger to +print the partial account name indented by its depth in the tree. The +third line is where we calculate and display the percentages. The +@code{display_total} command gives the values of the total calculated +for the account in this line. The @code{parent.total} command gives +the total for the next level up in the tree. @code{percent} formats +their ratio as a percentage. The fourth line tells ledger to display +the current market value of the line. The last two characters +@samp{%/} tell Ledger what to do for the last line, in this case, +nothing. + +@node Visualizing with Gnuplot, , Asset Allocation, Advanced Reports +@subsection Visualizing with Gnuplot +@cindex plotting +@cindex Gnuplot +@findex --amount-data +@findex --total-data +@findex --limit @var{EXPR} +@findex --display @var{EXPR} + +If you have the ``Gnuplot'' program installed, you can graph any of the +above register reports. The script to do this is included in the ledger +distribution, and is named @file{contrib/report}. Install @file{report} +anywhere along your @env{PATH}, and then use @file{report} instead of +@file{ledger} when doing a register report. The only thing to keep in +mind is that you must specify @option{--amount-data (-j)} or +@option{--total-data (-J)} to indicate whether ``Gnuplot'' should plot +the amount, or the running total. For example, this command plots total +monthly expenses made on your MasterCard. + +@smallexample +$ report -j -M -r --display "account =~ /mastercard/" reg ^expenses +@end smallexample + +The @file{report} script is a very simple Bourne shell script, that +passes a set of scripted commands to ``Gnuplot''. Feel free to modify +the script to your liking, since you may prefer histograms to line +plots, for example. + +Here are some useful plots: + +@smallexample +report -j -M reg ^expenses # monthly expenses +report -J reg checking # checking account balance +report -J reg ^income ^expenses # cash flow report + +# net worth report, ignoring non-$ postings + +report -J -l "Ua>=@{\$0.01@}" reg ^assets ^liab + +# net worth report starting last February. the use of a display +# predicate (-d) is needed, otherwise the balance will start at +# zero, and thus the y-axis will not reflect the true balance + +report -J -l "Ua>=@{\$0.01@}" -d "d>=[last feb]" reg ^assets ^liab +@end smallexample + +The last report uses both a calculation predicate @option{--limit +@var{EXPR} (-l)} and a display predicate @option{--display @var{EXPR} +(-d)}. The calculation predicate limits the report to postings whose +amount is greater than or equal to $1 (which can only happen if the +posting amount is in dollars). The display predicate limits the +transactions @emph{displayed} to just those since last February, even +though those transactions from before will be computed as part of the +balance. + +@node Reporting Commands, Command-Line Syntax, Building Reports, Top +@chapter Reporting Commands + +@menu +* Primary Financial Reports:: Reports in other formats:: Reports about +* Reports in other Formats:: +* Reports about your Journals:: +@end menu + +@node Primary Financial Reports, Reports in other Formats, Reporting Commands, Reporting Commands +@section Primary Financial Reports + +@menu +* The @command{balance} command:: +* The @command{equity} command:: +* The @command{register} command:: +* The @command{print} command:: +@end menu + +@node The @command{balance} command, The @command{equity} command, Primary Financial Reports, Primary Financial Reports +@subsection The @command{balance} command +@findex balance + +The @command{balance} command reports the current balance of all +accounts. It accepts a list of optional regexes, which confine the +balance report to the matching accounts. If an account contains +multiple types of commodities, each commodity's total is reported +separately. + +@node The @command{equity} command, The @command{register} command, The @command{balance} command, Primary Financial Reports +@subsection The @command{equity} command +@findex equity + +The @command{equity} command prints out account balances as if they +were transactions. This makes it easy to establish the starting +balances for an account, such as when @ref{Archiving Previous Years}. + +@node The @command{register} command, The @command{print} command, The @command{equity} command, Primary Financial Reports +@subsection The @command{register} command +@findex register +@findex --amount-data +@findex --total-data + +The @command{register} command displays all the postings occurring in +a single account, line by line. The account regex must be specified as +the only argument to this command. If any regexes occur after the +required account name, the register will contain only those postings +that match, which makes it very useful for hunting down a particular +posting. + +The output from @command{register} is very close to what a typical +checkbook, or single-account ledger, would look like. It also shows a +running balance. The final running balance of any register should +always be the same as the current balance of that account. + +If you have ``Gnuplot'' installed, you may plot the amount or running +total of any register by using the script @file{report}, which is +included in the Ledger distribution. The only requirement is that you +add either @option{--amount-data (-j)} or @option{--total-data (-J)} to +your @command{register} command, in order to plot either the amount or +total column, respectively. + +@node The @command{print} command, , The @command{register} command, Primary Financial Reports +@subsection The @command{print} command +@findex print + +The @command{print} command prints out ledger transactions in a textual +format that can be parsed by Ledger. They will be properly formatted, +and output in the most economic form possible. The @command{print} +command also takes a list of optional regexes, which will cause only +those postings which match in some way to be printed. + +The @command{print} command can be a handy way to clean up a ledger +file whose formatting has gotten out of hand. + +@node Reports in other Formats, Reports about your Journals, Primary Financial Reports, Reporting Commands +@section Reports in other Formats + +@menu +* Comma Separated Values files:: +* The @command{lisp} command:: +* Emacs @command{org} Mode:: +* Org mode with Babel:: +* The @command{pricemap} command:: +* The @command{xml} command:: +* @command{prices} and @command{pricedb} commands:: +@end menu + +@node Comma Separated Values files, The @command{lisp} command, Reports in other Formats, Reports in other Formats +@subsection Comma Separated Values files + +@menu +* The @command{csv} command:: +* The @command{convert} command:: +@end menu + +@node The @command{csv} command, The @command{convert} command, Comma Separated Values files, Comma Separated Values files +@subsubsection The @command{csv} command +@findex csv + +The @command{csv} command prints the desired ledger +transactions in a csv format suitable for importing into other programs. +You can specify the transactions to print using all the normal +limiting and searching functions. + +@node The @command{convert} command, , The @command{csv} command, Comma Separated Values files +@subsubsection The @command{convert} command +@cindex csv importing +@cindex comma separated variable file reading +@findex convert +@findex --input-date-format @var{DATE_FORMAT} + +The @command{convert} command parses a comma separated value (csv) file +and prints Ledger transactions. Many banks offer csv file downloads. +Unfortunately, the file formats, aside from the commas, are all +different. The ledger @command{convert} command tries to help as much +as it can. + +Your bank's csv files will have fields in different orders from other +banks, so there must be a way to tell Ledger what to expect. Insert +a line at the beginning of the csv file that describes the fields to +Ledger. + +For example, this is a portion of a csv file downloaded from a credit +union in the United States: + +@smallexample +Account Name: VALUFIRST CHECKING +Account Number: 71 +Date Range: 11/13/2011 - 12/13/2011 + +Transaction Number,Date,Description,Memo,Amount Debit,Amount Credit,Balance,Check Number,Fees +767718,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-8.80,,00001640.04,, +767406,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-1.03,,00001648.84,, +683342,12/13/2011,"Visa Checking","NetFlix Date 12/12/11 000326585896 5968",-21.85,,00001649.87,, +639668,12/13/2011,"Withdrawal","ID: 1741472662 CO: XXAA.COM PAYMNT",-236.65,,00001671.72,, +1113648,12/12/2011,"Withdrawal","Tuscan IT #00037657",-29.73,,00001908.37,, +@end smallexample + +Unfortunately, as it stands Ledger cannot read it, but you can. Ledger +expects the first line to contain a description of the fields on each +line of the file. The fields ledger can recognize contain these +case-insensitive strings @code{date}, @code{posted}, @code{code}, +@code{payee} or @code{desc} or @code{description}, @code{amount}, +@code{cost}, @code{total}, and @code{note}. + +Delete the account description lines at the top, and replace the first +line in the data above with: + +@smallexample +,date,payee,note,amount,,,code, +@end smallexample + +Then execute ledger like this: + +@smallexample +$ ledger convert download.csv --input-date-format "%m/%d/%Y" +@end smallexample + +Where the @option{--input-date-format @var{DATE_FORMAT}} option tells +ledger how to interpret the dates. + +Importing csv files is a lot of work, but is very amenable to +scripting. + +If there are columns in the bank data you would like to keep in your +ledger data, besides the primary fields described above, you can name +them in the field descriptor list and Ledger will include them in the +transaction as meta data if it doesn't recognize the field name. For +example, if you want to capture the bank transaction number and it +occurs in the first column of the data use: + +@smallexample +transid,date,payee,note,amount,,,code, +@end smallexample + +Ledger will include @samp{; transid: 767718} in the first transaction +from the file above. + +@findex --invert +@findex --auto-match +@findex --account @var{STR} +@findex --rich-data + +The @command{convert} command accepts four options. They are +@option{--invert} which inverts the amount field, @option{--auto-match} +which automatically matches an account from the Ledger journal for every +CSV line, @option{--account @var{STR}} which you can use to specify the +account to balance against, and @option{--rich-data} which stores +additional tag/value pairs. + +Using the two first lines of the above csv file, + +@smallexample @c file:01B0350 +,date,payee,note,amount,,,code, +767718,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-8.80,,00001640.04,, +767406,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-1.03,,00001648.84,, +@end smallexample + +and launching the below command, + +@smallexample @c command:01B0350,with_file:download.csv +$ ledger convert download.csv --input-date-format "%m/%d/%Y" \ + --invert --account Assets:MyBank --rich-data \ + --file sample.dat --now=2012/01/13 +@end smallexample + +you will get the result: + +@smallexample @c output:01B0350 +2011/12/13 * Withdrawal ;ACE HARDWARE 16335 S HOUGHTON RD + ; CSV: 767718,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-8.80,,00001640.04,, + ; Imported: 2012/01/13 + ; UUID: dfdc3c3d5c54c6967dd39d5b4e4fd1ea76e87233 + Expenses:Unknown 8.8 + Assets:MyBank + +2011/12/13 * Withdrawal ;ACE HARDWARE 16335 S HOUGHTON RD + ; CSV: 767406,12/13/2011,"Withdrawal","ACE HARDWARE 16335 S HOUGHTON RD",-1.03,,00001648.84,, + ; Imported: 2012/01/13 + ; UUID: 63086448b1f29f7fd6efb11ea40660185a213f9d + Expenses:Unknown 1.03 + Assets:MyBank +@end smallexample + +The three added metadata are: @samp{CSV} as the original line from csv +file, @samp{Imported} as the date when the csv file was imported into +Ledger, and @samp{UUID} as a checksum of original csv line. + +If an entry with the same @samp{UUID} tag is already included in the +normal ledger file (specified via @option{--file @var{FILE} (-f)} or via +the environment variable @env{LEDGER_FILE}) this entry will not be +printed again. + +In the output above, the account is @samp{Expenses:Unknown} for CSV +lines. You can use the @option{--auto-match} option to automatically +match an account from your Ledger journal. + +You can also use @command{convert} with @code{payee} and @code{account} +directives. First, you can use the @code{payee} and @code{alias} +directive to rewrite the @code{payee} field based on some rules. Then +you can use the account and its @code{payee} directive to specify the +account. I use it like this, for example: + +@smallexample @c input:validate +payee Aldi + alias ^ALDI SUED SAGT DANKE +account Aufwand:Einkauf:Lebensmittel + payee ^(Aldi|Alnatura|Kaufland|REWE)$ +@end smallexample + +Note that it may be necessary for the output of @samp{ledger convert} +to be passed through @code{ledger print} a second time if you want to +match on the new payee field. During the @code{ledger convert} run, +only the original payee name as specified in the csv data seems to be +used. + +@node The @command{lisp} command, Emacs @command{org} Mode, Comma Separated Values files, Reports in other Formats +@subsection The @command{lisp} command +@findex lisp +@findex emacs + +The @command{lisp} command prints results in a form that can be read +directly by Emacs Lisp. The format of the @code{sexp} is: + +@smallexample +((BEG-POS CLEARED DATE CODE PAYEE + (ACCOUNT AMOUNT)...) ; list of postings + ...) ; list of transactions +@end smallexample + +@noindent +@command{emacs} can also be used as a synonym for @command{lisp}. + +@node Emacs @command{org} Mode, Org mode with Babel, The @command{lisp} command, Reports in other Formats +@subsection Emacs @command{org} Mode +@findex org + +Org mode has a sub-system known as Babel which allows for literate +programming. This allows you to mix text and code within the same +document and automatically execute code which may generate results +which will then appear in the text. + +One of the languages supported by Babel is Ledger, so that you can have +ledger commands embedded in a text file and have the output of ledger +commands also appear in the text file. The output can be updated +whenever any new ledger entries are added. + +For instance, the following Org mode text document snippet illustrates +a very naive but still useful application of the Babel system: + +@smallexample +* A simple test of ledger in an org file +The following are some entries and I have requested that ledger be run +to generate a balance on the accounts. I could have asked for +a register or, in fact, anything at all the ledger can do through +command-line options. + +#+begin_src ledger :cmdline bal :results value +2010/01/01 * Starting balance + assets:bank:savings £1300.00 + income:starting balances +2010/07/22 * Got paid + assets:bank:chequing £1000.00 + income:salary +2010/07/23 Rent + expenses:rent £500.00 + assets:bank:chequing +#+end_src + +#+results: +: £1800.00 assets:bank +: £500.00 chequing +: £1300.00 savings +: £500.00 expenses:rent +: £-2300.00 income +: £-1000.00 salary +: £-1300.00 starting balances +@end smallexample + +Typing @kbd{C-c C-c} anywhere in the ``ledger source code block'' will +invoke ledger on the contents of that block and generate a ``results'' +block. The results block can appear anywhere in the file but, by +default, will appear immediately below the source code block. + +You can combine multiple source code blocks before executing ledger and +do all kinds of other wonderful things with Babel (and Org mode). + +@node Org mode with Babel, The @command{pricemap} command, Emacs @command{org} Mode, Reports in other Formats +@subsection Org mode with Babel + +Using Babel, it is possible to record financial transactions +conveniently in an org file and subsequently generate the financial +reports required. + +As of Org mode 7.01, Ledger support is provided. Check the Babel +documentation on Worg for instructions on how to achieve this but +I currently do this directly as follows: + +@smallexample +(org-babel-do-load-languages + 'org-babel-load-languages + '((ledger . t) ;this is the important one for this tutorial + )) +@end smallexample + +Once Ledger support in Babel has been enabled, we can proceed to +include Ledger entries within an org file. There are three ways (at +least) in which these can be included: + +@enumerate + +@item +place all Ledger entries within one single source block and execute this +block with different arguments to generate the appropriate reports, + +@item +place Ledger entries in more than one source block and use the +@code{noweb} literary programming approach, supported by Babel, to +combine these into one block elsewhere in the file for processing by +Ledger, + +@item +place Ledger entries in different source blocks and use @code{tangle} to +generate a Ledger file which you can subsequently process using Ledger +directly. +@end enumerate + +The first two are described in more detail in this short tutorial. + +@menu +* Embedded Ledger example with single source block:: +* Multiple Ledger source blocks with @code{noweb}:: +* Income Entries:: +* Expenses:: +* Financial Summaries:: +* An overall balance summary:: +* Generating a monthly register:: +* Summary:: +@end menu + +@node Embedded Ledger example with single source block, Multiple Ledger source blocks with @code{noweb}, Org mode with Babel, Org mode with Babel +@subsubsection Embedded Ledger example with single source block + +The easiest, albeit possibly least useful, way in which to use Ledger +within an org file is to use a single source block to record all Ledger +entries. The following is an example source block: + +@smallexample +#+name: allinone +#+begin_src ledger +2010/01/01 * Starting balance + assets:bank:savings £1300.00 + income:starting balances +2010/07/22 * Got paid + assets:bank:chequing £1000.00 + income:salary +2010/07/23 Rent + expenses:rent £500.00 + assets:bank:chequing +2010/07/24 Food + expenses:food £150.00 + assets:bank:chequing +2010/07/31 * Interest on bank savings + assets:bank:savings £3.53 + income:interest +2010/07/31 * Transfer savings + assets:bank:savings £250.00 + assets:bank:chequing +2010/08/01 got paid again + assets:bank:chequing £1000.00 + income:salary +#+end_src +@end smallexample + +In this example, we have combined both expenses and income into one set +of Ledger entries. We can now generate register and balance reports (as +well as many other types of reports) using Babel to invoke Ledger with +specific arguments. The arguments are passed to Ledger using the +@code{:cmdline} header argument. In the code block above, there is no +such argument so the system takes the default. For Ledger code blocks, +the default @code{:cmdline} argument is @code{bal} and the result of +evaluating this code block (@kbd{C-c C-c}) would be: + +@smallexample +#+results: allinone() +: £2653.53 assets:bank +: £1100.00 chequing +: £1553.53 savings +: £650.00 expenses +: £150.00 food +: £500.00 rent +: £-3303.53 income +: £-3.53 interest +: £-2000.00 salary +: £-1300.00 starting balances +@end smallexample + +If, instead, you wished to generate a register of all the transactions, +you would change the @code{#+begin_src} line for the code block to +include the required command-line option: + +@smallexample +#+begin_src ledger :cmdline reg +@end smallexample + +Evaluating the code block again would generate a different report. + +Having to change the actual directive on the code block and re-evaluate +makes it difficult to have more than one view of your transactions and +financial state. Eventually, Babel will support passing arguments to +@code{#+call} evaluations of code blocks but this support is missing +currently. Instead, we can use the concepts of literary programming, as +implemented by the @code{noweb} features of Babel, to help us. + +@node Multiple Ledger source blocks with @code{noweb}, Income Entries, Embedded Ledger example with single source block, Org mode with Babel +@subsubsection Multiple Ledger source blocks with @code{noweb} + +The @code{noweb} feature of Babel allows us to expand references to +other code blocks within a code block. For Ledger, this can be used to +group transactions according to type, say, and then bring various sets +of transactions together to generate reports. + +Using the same transactions used above, we could consider splitting +these into expenses and income, as follows: + +@node Income Entries, Expenses, Multiple Ledger source blocks with @code{noweb}, Org mode with Babel +@subsubsection Income Entries + +The first set of entries relates to income, either monthly pay or +interest, all typically going into one of my bank accounts. Here, I have +placed several entries, but we could have had each entry in a separate +@code{src} block. Note that all code blocks you wish to refer to later +must have the @code{:noweb yes} header argument specified. + +@smallexample +#+name: income +#+begin_src ledger :noweb yes +2010/01/01 * Starting balance + assets:bank:savings £1300.00 + income:starting balances +2010/07/22 * Got paid + assets:bank:chequing £1000.00 + income:salary +2010/07/31 * Interest on bank savings + assets:bank:savings £3.53 + income:interest +2010/07/31 * Transfer savings + assets:bank:savings £250.00 + assets:bank:chequing +2010/08/01 got paid again + assets:bank:chequing £1000.00 + income:salary +#+end_src +@end smallexample + +@node Expenses, Financial Summaries, Income Entries, Org mode with Babel +@subsubsection Expenses + +The following entries relate to personal expenses, such as rent and +food. Again, these have all been placed in a single @code{src} block but +could have been done individually. + +@smallexample +#+name: expenses +#+begin_src ledger :noweb yes +2010/07/23 Rent + expenses:rent £500.00 + assets:bank:chequing +2010/07/24 Food + expenses:food £150.00 + assets:bank:chequing +#+end_src +@end smallexample + +@node Financial Summaries, An overall balance summary, Expenses, Org mode with Babel +@subsubsection Financial Summaries + +Given the ledger entries defined above in the income and expenses code +blocks, we can now refer to these using the noweb expansion directives, +@code{<<name>>}. We can now define different code blocks to generate +specific reports for those transactions. Below are two examples, one to +generate a balance report and one to generate a register report of all +transactions. + +@node An overall balance summary, Generating a monthly register, Financial Summaries, Org mode with Babel +@subsubsection An overall balance summary +@findex --subtotal + +The overall balance of your account and expenditure with a breakdown +according to category is specified by passing the @code{:cmdline bal} +argument to Ledger. This code block can now be evaluated (@kbd{C-c C-c}) +and the results generated by incorporating the transactions referred to +by the @code{<<income>>} and @code{<<expenses>>} lines. + +@smallexample +#+name: balance +#+begin_src ledger :cmdline bal :noweb yes +<<income>> +<<expenses>> +#+end_src + +#+results: balance +: £2653.53 assets:bank +: £1100.00 chequing +: £1553.53 savings +: £650.00 expenses +: £150.00 food +: £500.00 rent +: £-3303.53 income +: £-3.53 interest +: £-2000.00 salary +: £-1300.00 starting balances +@end smallexample + +If you want a less detailed breakdown of where your money is, you can +specify the @option{--collapse (-n)} flag (i.e. @samp{:cmdline -n bal}) +to tell Ledger to exclude sub-accounts in the report. + +@smallexample +#+begin_src ledger :cmdline -n bal :noweb yes +<<income>> +<<expenses>> +#+end_src + +#+results: +: £2653.53 assets +: £650.00 expenses +: £-3303.53 income +@end smallexample + +@node Generating a monthly register, Summary, An overall balance summary, Org mode with Babel +@subsubsection Generating a monthly register +@findex register +@findex --monthly + +You can also generate a monthly register (the @command{reg} command) by +executing the following @code{src} block. This presents a summary of +transactions for each monthly period (the @option{--monthly (-M)} +argument) with a running total in the final column (which should be 0 at +the end if all the entries are correct). + +@smallexample +#+name: monthlyregister +#+begin_src ledger :cmdline -M reg :noweb yes +<<income>> +<<expenses>> +#+end_src + +#+results: monthlyregister +:2010/01/01 - 2010/01/31 assets:bank:savings £1300.00 £1300.00 +: in:starting balances £-1300.00 0 +:2010/07/01 - 2010/07/31 assets:bank:chequing £100.00 £100.00 +: assets:bank:savings £253.53 £353.53 +: expenses:food £150.00 £503.53 +: expenses:rent £500.00 £1003.53 +: income:interest £-3.53 £1000.00 +: income:salary £-1000.00 0 +:2010/08/01 - 2010/08/01 assets:bank:chequing £1000.00 £1000.00 +: income:salary £-1000.00 0 +@end smallexample + +We could also generate a monthly report on our assets showing how these +are increasing (or decreasing!). In this case, the final column will be +the running total of the assets in our ledger. + +@smallexample +#+name: monthlyassetsregister +#+begin_src ledger :cmdline -M reg assets :noweb yes +<<income>> +<<expenses>> +#+end_src + +#+results: monthlyassetsregister +: 2010/01/01 - 2010/01/31 assets:bank:savings £1300.00 £1300.00 +: 2010/07/01 - 2010/07/31 assets:bank:chequing £100.00 £1400.00 +: assets:bank:savings £253.53 £1653.53 +: 2010/08/01 - 2010/08/01 assets:bank:chequing £1000.00 £2653.53 +@end smallexample + +@node Summary, , Generating a monthly register, Org mode with Babel +@subsubsection Summary + +This short tutorial shows how Ledger entries can be embedded in an org +file and manipulated using Babel. However, only simple Ledger features +have been illustrated; please refer to the Ledger documentation for +examples of more complex operations on a ledger. + +@node The @command{pricemap} command, The @command{xml} command, Org mode with Babel, Reports in other Formats +@subsection The @command{pricemap} command +@findex pricemap + +If you have the @file{graphviz} graph visualization package installed, +ledger can generate a graph of the relationship between your various +commodities. The output file is in the ``dot'' format. + +This is probably not very interesting, unless you have many different +commodities valued in terms of each other. For example, multiple +currencies and multiple investments valued in those currencies. + +@node The @command{xml} command, @command{prices} and @command{pricedb} commands, The @command{pricemap} command, Reports in other Formats +@subsection The @command{xml} command +@findex xml + +By default, Ledger uses a human-readable data format, and displays its +reports in a manner meant to be read on screen. For the purpose of +writing tools which use Ledger, however, it is possible to read and +display data using XML. This section documents that format. + +The general format used for Ledger data is: + +@smallexample +<?xml version="1.0"?> +<ledger> + <xact>...</xact> + <xact>...</xact> + <xact>...</xact>... +</ledger> +@end smallexample + +The data stream is enclosed in a @code{ledger} tag, which contains a +series of one or more transactions. Each @code{xact} describes one +transaction and contains a series of one or more postings: + +@smallexample +<xact> + <en:date>2004/03/01</en:date> + <en:cleared/> + <en:code>100</en:code> + <en:payee>John Wiegley</en:payee> + <en:postings> + <posting>...</posting> + <posting>...</posting> + <posting>...</posting>... + </en:postings> +</xact> +@end smallexample + +The date format for @code{en:date} is always @code{YYYY/MM/DD}. The +@code{en:cleared} tag is optional, and indicates whether the posting has +been cleared or not. There is also an @code{en:pending} tag, for +marking pending postings. The @code{en:code} and @code{en:payee} tags +both contain whatever text the user wishes. + +After the initial transaction data, there must follow a set of postings +marked with @code{en:postings}. Typically these postings will all +balance each other, but if not they will be automatically balanced into +an account named @samp{Unknown}. + +Within the @code{en:postings} tag is a series of one or more +@code{posting}'s, which have the following form: + +@smallexample +<posting> + <tr:account>Expenses:Computer:Hardware</tr:account> + <tr:amount> + <value type="amount"> + <amount> + <commodity flags="PT">$</commodity> + <quantity>90.00</quantity> + </amount> + </value> + </tr:amount> +</posting> +@end smallexample + +This is a basic posting. It may also begin with +@code{tr:virtual} and/or @code{tr:generated} tags, to indicate virtual +and auto-generated postings. Then follows the @code{tr:account} +tag, which contains the full name of the account the posting is +related to. Colons separate parent from child in an account name. + +Lastly follows the amount of the posting, indicated by +@code{tr:amount}. Within this tag is a @code{value} tag, of which +there are four different kinds, each with its own format: + +@enumerate +@item Boolean, +@item integer, +@item amount, +@item balance. +@end enumerate + +The format of a Boolean value is @code{true} or @code{false} +surrounded by a @code{boolean} tag, for example: + +@smallexample +<boolean>true</boolean> +@end smallexample + +The format of an integer value is the numerical value surrounded by an +@code{integer} tag, for example: + +@smallexample +<integer>12036</integer> +@end smallexample + +The format of an amount contains two members, the commodity and the +quantity. The commodity can have a set of flags that indicate how to +display it. The meaning of the flags (all of which are optional) are: + +@table @code + +@item P +The commodity is prefixed to the value. + +@item S +The commodity is separated from the value by a space. + +@item T +Thousands markers are used to display the amount. + +@item E +The format of the amount is European, with period used as a thousands +marker, and comma used as the decimal point. + +@end table + +The actual quantity for an amount is an integer of arbitrary size. +Ledger uses the GNU multiple precision arithmetic library to handle +such values. The XML format assumes the reader to be equally capable. +Here is an example amount: + +@smallexample +<value type="amount"> + <amount> + <commodity flags="PT">$</commodity> + <quantity>90.00</quantity> + </amount> +</value> +@end smallexample + +Lastly, a balance value contains a series of amounts, each with a +different commodity. Unlike the name, such a value does need to +balance. It is called a balance because it sums several amounts. For +example: + +@smallexample +<value type="balance"> + <balance> + <amount> + <commodity flags="PT">$</commodity> + <quantity>90.00</quantity> + </amount> + <amount> + <commodity flags="TE">DM</commodity> + <quantity>200.00</quantity> + </amount> + </balance> +</value> +@end smallexample + +That is the extent of the XML data format used by Ledger. It will +output such data if the @command{xml} command is used, and can read +the same data. + +@node @command{prices} and @command{pricedb} commands, , The @command{xml} command, Reports in other Formats +@subsection @command{prices} and @command{pricedb} commands +@findex prices +@findex pricedb +@findex --average + +The @command{prices} command displays the price history for matching +commodities. The @option{--average (-A)} option is useful with this +report, to display the running average price, or @option{--deviation +(-D)} to show each price's deviation from that average. + +There is also a @command{pricedb} command which outputs the same +information as @command{prices}, but does so in a format that can be +parsed by Ledger. This is useful for generating and tidying up +pricedb database files. + +@node Reports about your Journals, , Reports in other Formats, Reporting Commands +@section Reports about your Journals +@findex --count + +@menu +* @command{accounts}:: +* @command{payees}:: +* @command{commodities}:: +* @command{tags}:: +* @command{xact}:: +* @command{stats}:: +* @command{select}:: +@end menu + +@node @command{accounts}, @command{payees}, Reports about your Journals, Reports about your Journals +@subsection @command{accounts} +@findex accounts + +The @command{accounts} command reports all of the accounts in the +journal. Following the command with a regular expression will limit the +output to accounts matching the regex. The output is sorted by name. +Using the @option{--count} option will tell you how many entries use +each account. + +@node @command{payees}, @command{commodities}, @command{accounts}, Reports about your Journals +@subsection @command{payees} +@findex payees + +The @command{payees} command reports all of the unique payees in the +journal. Using the @option{--count} option will tell you how many +entries use each payee. To filter the payees displayed you must use the +prefix @@: + +@smallexample @c command:validate +$ ledger payees @@Nic +@end smallexample +@smallexample +Nicolas +Nicolas BOILABUS +Oudtshoorn Municipality +Vaca Veronica +@end smallexample + +@node @command{commodities}, @command{tags}, @command{payees}, Reports about your Journals +@subsection @command{commodities} +@findex commodities + +Report all commodities present in the journals under consideration. The +output is sorted by name. Using the @option{--count} option will tell +you how many entries use each commodity. + +@node @command{tags}, @command{xact}, @command{commodities}, Reports about your Journals +@subsection @command{tags} +@findex tags +@findex --values + +The @command{tags} command reports all of the tags in the journal. The +output is sorted by name. Using the @option{--count} option will tell +you how many entries use each tag. Using the @option{--values} option +will report the values used by each tag. + +@node @command{xact}, @command{stats}, @command{tags}, Reports about your Journals +@subsection @command{xact} +@findex draft +@findex entry +@findex xact + +The @command{xact} command simplifies the creation of new transactions. +It works on the principle that 80% of all postings are variants of +earlier postings. Here's how it works: + +Say you currently have this posting in your ledger file: + +@smallexample @c input:03ACB97 +2004/03/15 * Viva Italiano + Expenses:Food $12.45 + Expenses:Tips $2.55 + Liabilities:MasterCard $-15.00 +@end smallexample + +Now it's @samp{2004/4/9}, and you've just eaten at @samp{Viva Italiano} +again. The exact amounts are different, but the overall form is the +same. With the @command{xact} command you can type: + +@smallexample @c command:03ACB97 +$ ledger xact 2004/4/9 viva food 11 tips 2.50 +@end smallexample + +This produces the following output: + +@smallexample @c output:03ACB97 +2004/04/09 Viva Italiano + Expenses:Food $11.00 + Expenses:Tips $2.50 + Liabilities:MasterCard +@end smallexample + +It works by finding a past posting matching the regular expression +@samp{viva}, and assuming that any accounts or amounts specified will be +similar to that earlier posting. If Ledger does not succeed in +generating a new transaction, an error is printed and the exit code is +set to @samp{1}. + +Here are a few more examples of the @command{xact} command, assuming +the above journal transaction: + +@smallexample +$ ledger xact 4/9 viva 11.50 +$ ledger xact 4/9 viva 11.50 checking # (from `checking') +$ ledger xact 4/9 viva food 11.50 tips 8 +$ ledger xact 4/9 viva food 11.50 tips 8 cash +$ ledger xact 4/9 viva food $11.50 tips $8 cash +$ ledger xact 4/9 viva dining "DM 11.50" +@end smallexample + +@command{draft} and @command{entry} are both synonyms of +@command{xact}. @command{entry} is provided for backwards compatibility +with Ledger 2.X. + +@node @command{stats}, @command{select}, @command{xact}, Reports about your Journals +@subsection @command{stats} +@findex stats +@findex stat + +@value{FIXME:UNDOCUMENTED} + +@node @command{select}, , @command{stats}, Reports about your Journals +@subsection @command{select} +@findex select + +@value{FIXME:UNDOCUMENTED} + +@node Command-Line Syntax, Budgeting and Forecasting, Reporting Commands, Top +@chapter Command-Line Syntax + +@menu +* Basic Usage:: +* Command-Line Quick Reference:: +* Detailed Option Description:: +* Period Expressions:: +@end menu + +@node Basic Usage, Command-Line Quick Reference, Command-Line Syntax, Command-Line Syntax +@section Basic Usage + +This chapter describes Ledger's features and options. You may wish to +survey this to get an overview before diving into the @ref{Ledger +Tutorial} and more detailed examples that follow. + +Ledger has a very simple command-line interface, named---enticingly +enough---@file{ledger}. It supports a few reporting commands, and +a large number of options for refining the output from those commands. +The basic syntax of any ledger command is: + +@smallexample +$ ledger [OPTIONS...] COMMAND [ARGS...] +@end smallexample + +After the command word there may appear any number of arguments. For +most commands, these arguments are regular expressions that cause the +output to relate only to postings matching those regular expressions. +For the @command{xact} command, the arguments have a special meaning, +described below. + +The regular expressions arguments always match the account name that +a posting refers to. To match on the payee of the transaction +instead, precede the regular expression with @samp{payee} or +@samp{@@}. For example, the following balance command reports account +totals for rent, food and movies, but only those whose payee matches +Freddie: + +@smallexample @c command:validate +$ ledger bal rent food movies payee freddie +@end smallexample + +@noindent +or + +@smallexample @c command:validate +$ ledger bal rent food movies @@freddie +@end smallexample + +There are many, many command options available with the @file{ledger} +program, and it takes a while to master them. However, none of them are +required to use the basic reporting commands. + +@node Command-Line Quick Reference, Detailed Option Description, Basic Usage, Command-Line Syntax +@section Command-Line Quick Reference + +@menu +* Basic Reporting Commands:: +* Basic Options:: +* Report Filtering:: +* Error Checking and Calculation Options:: +* Output Customization:: +* Grouping Options:: +* Commodity Reporting:: +@end menu + +@node Basic Reporting Commands, Basic Options, Command-Line Quick Reference, Command-Line Quick Reference +@subsection Basic Reporting Commands + +@ftable @command + +@item balance +@itemx bal +Show account balances. + +@item register +@itemx reg +Show all transactions with running total. + +@item csv +@cindex csv exporting +Show transactions in csv format, for exporting to other programs. + +@item print +Print transactions in a format readable by ledger. + +@item xml +Produce XML output of the register command. + +@item lisp +@itemx emacs +Produce s-expression output, suitable for Emacs. + +@item equity +Print account balances as transactions. + +@item prices +Print price history for matching commodities. + +@item pricedb +Print price history for matching commodities in a format readable by +ledger. + +@item xact +Generate transactions based on previous postings. + +@end ftable + +@node Basic Options, Report Filtering, Basic Reporting Commands, Command-Line Quick Reference +@subsection Basic Options + +@ftable @option + +@item --help +@itemx -h +Display the man page for @file{ledger}. + +@item --version +Print version information and exit. + +@item --file @var{FILE} +@itemx -f @var{FILE} +Read @file{FILE} as a ledger file. + +@item --output @var{FILE} +@itemx -o @var{FILE} +Redirect output to @file{FILE}. + +@item --init-file @var{FILE} +@itemx -i @var{FILE} +Specify an options file. + +@item --import @var{FILE} +Import @var{FILE} as Python module. + +@item --account @var{STR} +@itemx -a @var{STR} +Specify default account @var{STR} for QIF file postings. + +@end ftable + +@node Report Filtering, Error Checking and Calculation Options, Basic Options, Command-Line Quick Reference +@subsection Report Filtering + +@ftable @option + +@item --current +@itemx -c +Display only transactions on or before the current date. + +@item --begin @var{DATE} +@itemx -b @var{DATE} +Limit the processing to transactions on or after @var{DATE}. + +@item --end @var{DATE} +@itemx -e @var{DATE} +Limit the processing to transactions before @var{DATE}. + +@item --period @var{PERIOD_EXPRESSION} +@itemx -p @var{PERIOD_EXPRESSION} +Limit the processing to transactions in @var{PERIOD_EXPRESSION}. + +@item --period-sort @var{VEXPR} +Sort postings within each period according to @var{VEXPR}. + +@item --cleared +@itemx -C +Display only cleared postings. + +@item --dc +Display register or balance in debit/credit format. + +@item --uncleared +@itemx -U +Display only uncleared postings. + +@item --real +@itemx -R +Display only real postings. + +@item --actual +@itemx -L +Display only actual postings, not automated ones. + +@item --related +@itemx -r +Display related postings. + +@item --budget +Display how close your postings meet your budget. + +@item --add-budget +Show unbudgeted postings. + +@item --unbudgeted +Show only unbudgeted postings. + +@item --forecast-while @var{VEXPR} +@itemx --forecast @var{VEXPR} +Project balances into the future. + +@item --limit @var{EXPR} +@itemx -l @var{EXPR} +Limit which postings are used in calculations by @var{EXPR}. + +@item --amount @var{EXPR} +@itemx -t @var{EXPR} +Change value expression reported in @command{register} report. + +@item --total @var{VEXPR} +@itemx -T @var{VEXPR} +Change the value expression used for ``totals'' column in +@command{register} and @command{balance} reports. + +@end ftable + +@node Error Checking and Calculation Options, Output Customization, Report Filtering, Command-Line Quick Reference +@subsection Error Checking and Calculation Options + +@ftable @option + +@item --strict +Accounts, tags or commodities not previously declared will cause +warnings. + +@item --pedantic +Accounts, tags or commodities not previously declared will cause errors. + +@item --check-payees +Enable strict and pedantic checking for payees as well as accounts, +commodities and tags. This only works in conjunction with +@option{--strict} or @option{--pedantic}. + +@item --immediate +Instruct ledger to evaluate calculations immediately rather than lazily. + +@end ftable + +@node Output Customization, Grouping Options, Error Checking and Calculation Options, Command-Line Quick Reference +@subsection Output Customization + +@ftable @option + +@item --collapse +@itemx -n +Collapse transactions with multiple postings. + +@item --subtotal +@itemx -s +Report register as a single subtotal. + +@item --by-payee +@itemx -P +Report subtotals by payee. + +@item --empty +@itemx -E +Include empty accounts in the report. + +@item --weekly +@itemx -W +Report posting totals by week. + +@item --quarterly +Report posting totals by quarter. + +@item --yearly +@itemx -Y +Report posting totals by year. + +@item --dow +Report posting totals by day of week. + +@item --sort @var{VEXPR} +@itemx -S @var{VEXPR} +Sort a report using @var{VEXPR}. + +@item --wide +@itemx -w +Assume 132 columns instead of 80. + +@item --head @var{INT} +Report the first @var{INT} postings. + +@item --tail @var{INT} +Report the last @var{INT} postings. + +@item --pager @var{FILE} +Direct output to @var{FILE} pager program. + +@item --no-pager +Direct output to stdout, avoiding pager program. + +@item --average +@itemx -A +Report the average posting value. + +@item --deviation +@itemx -D +Report each posting's deviation from the average. + +@item --percent +@itemx -% +Show subtotals in the balance report as percentages. +@c @item --totals +@c Include running total in the @command{xml} report + +@item --pivot @var{TAG} +Produce a pivot table of the @var{TAG} type specified. + +@item --amount-data +@itemx -j +Show only the date and value columns to format the output for plots. + +@item --plot-amount-format @var{FORMAT_STRING} +Specify the format for the plot output. + +@item --total-data +@itemx -J +Show only the date and total columns to format the output for plots. + +@item --plot-total-format @var{FORMAT_STRING} +Specify the format for the plot output. + +@item --display @var{EXPR} +@itemx -d @var{EXPR} +Display only postings that meet the criteria in the @var{EXPR}. + +@item --date-format @var{DATE_FORMAT} +@itemx -y @var{DATE_FORMAT} +Change the basic date format used in reports. + +@item --format @var{FORMAT_STRING} +@itemx --balance-format @var{FORMAT_STRING} +@itemx --register-format @var{FORMAT_STRING} +@itemx --prices-format @var{FORMAT_STRING} +@itemx -F @var{FORMAT_STRING} +Set the reporting format for various reports. + +@item --anon +Print the ledger register with anonymized accounts and payees, useful +for filing bug reports. + +@end ftable + +@node Grouping Options, Commodity Reporting, Output Customization, Command-Line Quick Reference +@subsection Grouping Options + +@ftable @option + +@item --by-payee +@itemx -P +Group postings by common payee names. + +@item --daily +@itemx -D +Group postings by day. + +@item --weekly +@itemx -W +Group postings by week. + +@item --monthly +@itemx -M +Group postings by month. + +@item --quarterly +Group postings by quarter. + +@item --yearly +@itemx -Y +Group postings by year. + +@item --dow +Group by day of weeks. + +@item --subtotal +@itemx -s +Group postings together, similar to the balance report. + +@end ftable + +@node Commodity Reporting, , Grouping Options, Command-Line Quick Reference +@subsection Commodity Reporting + +@ftable @option + +@item --price-db @var{FILE} +Use @file{FILE} for retrieving stored commodity prices. + +@item --price-exp @var{INT} +@itemx --leeway @var{INT} +@itemx -Z @var{INT} +Set expected freshness of prices in @var{INT} minutes. + +@item --download +@itemx -Q +Download quotes using the script named @file{getquote}. + +@c FIXME: The option doesn't exist currently. +@c @item --getquote @var{FILE} +@c Sets the path to a user-defined script to download commodity prices. + +@item --quantity +@itemx -O +Report commodity totals without conversion. + +@item --basis +@itemx -B +Report cost basis. + +@item --market +@itemx -V +Report last known market value. + +@item --gain +@itemx -G +Report net gain or loss for commodities that have a price history. + +@end ftable + +@node Detailed Option Description, Period Expressions, Command-Line Quick Reference, Command-Line Syntax +@section Detailed Option Description + +@menu +* Global Options:: +* Session Options:: +* Report Options:: +* Basic options:: +* Report filtering:: +* Output customization:: +* Commodity reporting:: +* Environment variables:: +@end menu + +@node Global Options, Session Options, Detailed Option Description, Detailed Option Description +@subsection Global Options + +Options for Ledger reports affect three separate scopes of operation: +Global, Session, and Report. In practice there is very little +difference between these scopes. Ledger 3.0 contains provisions for +GUIs, which would make use of the different scopes by keeping an +instance of Ledger running in the background and running multiple +sessions with multiple reports per session. + +@ftable @option + +@item --args-only +Ignore all environment and init-file settings and +use only command-line arguments to control Ledger. Useful for debugging +or testing small journal files not associated with your main financial +database. + +@item --debug @var{CODE} +@value{FIXME:UNDOCUMENTED} +If ledger has been built with debug options this will provide extra data during +the run. + +@item --help +@itemx -h +Display the man page for @file{ledger}. + +@item --init-file @var{FILE} +Specify the location of the init file. The default is home directory +@file{~/.ledgerrc}, or current directory @file{./.ledgerrc} if not found +in home directory. + +@item --options +Display the options in effect for this Ledger invocation, along with +their values and the source of those values, for example: + +@smallexample @c command:A9349E4,with_input:03ACB97 +$ ledger --options bal --cleared +@end smallexample + +@smallexample @c output:A9349E4 +=============================================================================== +[Global scope options] + --args-only --args-only + +[Session scope options] + --file = A9349E4.dat --file + +[Report scope options] + --cleared --cleared + --columns = 80 --columns + --limit = cleared --cleared +=============================================================================== + $15.00 Expenses + $12.45 Food + $2.55 Tips + $-15.00 Liabilities:MasterCard +-------------------- + 0 +@end smallexample + +@noindent +For the source column, a value starting with a @samp{-} or @samp{--} +indicated the source was a command-line argument. If the entry starts +with a @samp{$}, the source was an environment variable. If the source +is @code{?normalize} the value was set internally by ledger, in +a function called @code{normalize_options}. + +@item --script @var{FILE} +Execute a ledger script. + +@item --trace @var{INT} +Enable tracing. The @var{INT} specifies the level of trace desired. + +@item --verbose +@itemx -v +Print detailed information on the execution of Ledger. + +@item --verify +Enable additional assertions during run-time. This causes a significant +slowdown. When combined with @option{--debug @var{CODE}} ledger will +produce memory trace information. + +@item --verify-memory +Verify that every constructed object is properly destructed. This is for +debugging purposes only. + +@item --version +Print version information and exit. + +@end ftable + +@node Session Options, Report Options, Global Options, Detailed Option Description +@subsection Session Options + +Options for Ledger reports affect three separate scopes of operation: +Global, Session, and Report. In practice there is very little +difference between these scopes. Ledger 3.0 contains provisions for +GUIs, which would make use of the different scopes by keeping an +instance of Ledger running in the background and running multiple +sessions with multiple reports per session. + +@ftable @option + +@item --check-payees +Enable strict and pedantic checking for payees as well as accounts, +commodities and tags. This only works in conjunction with +@option{--strict} or @option{--pedantic}. + +@item --day-break +Break up @command{register} report of @ref{timelog} entries that span multiple +days by day. +@c see test/baseline/opt-day-break.dat +@c @smallexample @c input: +@c i 2015/ +@c @end smallexample +@c @smallexample @c command: +@c $ ledger reg --day-break +@c @end smallexample +@c @smallexample @c output: +@c @end smallexample + +@item --decimal-comma +Direct Ledger to parse journals using the European standard comma as +a decimal separator, not the usual period. + +@item --download +@itemx -Q +Direct Ledger to download prices. +@c using the script defined via the option +@c @option{--getquote @var{FILE}}. + +@item --explicit +Direct Ledger to require pre-declarations for entities (such as accounts, +commodities and tags) rather than taking entities from cleared +transactions as defined. This option is useful in combination with +@option{--strict} or @option{--pedantic}. + +@item --file @var{FILE} +@itemx -f @var{FILE} +Specify the input @file{FILE} for this invocation. + +@c FIXME: The option doesn't exist currently. +@c @item --getquote @var{FILE} +@c @cindex getquote +@c @cindex download prices +@c Tell ledger where to find the user defined script to download prices +@c information. + +@item --input-date-format @var{DATE_FORMAT} +Specify the input date format for journal entries. For example, + +@smallexample +$ ledger convert Export.csv --input-date-format "%m/%d/%Y" +@end smallexample + +Would convert the @file{Export.csv} file to ledger format, assuming +the dates in the CSV file are like 12/23/2009 (@pxref{Date and Time +Format Codes}). + +@item --master-account @var{STR} +Prepend all account names with the argument. + +@smallexample @c command:A76BB56 +$ ledger -f drewr3.dat bal --no-total --master-account HUMBUG +@end smallexample + +@smallexample @c output:A76BB56 + 0 HUMBUG + $ -3,804.00 Assets + $ 1,396.00 Checking + $ 30.00 Business + $ -5,200.00 Savings + $ -1,000.00 Equity:Opening Balances + $ 6,654.00 Expenses + $ 5,500.00 Auto + $ 20.00 Books + $ 300.00 Escrow + $ 334.00 Food:Groceries + $ 500.00 Interest:Mortgage + $ -2,030.00 Income + $ -2,000.00 Salary + $ -30.00 Sales + $ 180.00 Liabilities + $ -20.00 MasterCard + $ 200.00 Mortgage:Principal +@end smallexample + +@item --no-aliases +Ledger does not expand any aliases if this option is specified. + +@item --pedantic +Accounts, tags or commodities not previously declared will cause errors. + +@item --permissive +Quiet balance assertions. + +@item --price-db @var{FILE} +Specify the location of the price entry data file. + +@item --price-exp @var{INT} +@itemx --leeway @var{INT} +@itemx -Z @var{INT} +Set the expected freshness of price quotes, in @var{INT} minutes. That +is, if the last known quote for any commodity is older than this value, +and if @option{--download} is being used, then the Internet will be +consulted again for a newer price. Otherwise, the old price is still +considered to be fresh enough. + +@item --strict +Ledger normally silently accepts any account or commodity in a posting, +even if you have misspelled a commonly used one. The option +@option{--strict} changes that behavior. While running with +@option{--strict}, Ledger interprets all cleared transactions as +correct, and if it encounters a new account or commodity (same as +a misspelled commodity or account) it will issue a warning giving you +the file and line number of the problem. + +@item --recursive-aliases +Normally, ledger only expands aliases once. With this option, ledger +tries to expand the result of alias expansion recursively, until no more +expansions apply. + +@item --time-colon +The @option{--time-colon} option will display the value for a seconds +based commodity as real hours and minutes. + +For example 8100 seconds by default will be displayed as 2.25 whereas +with the @option{--time-colon} option they will be displayed as 2:15. + +@item --value-expr @var{VEXPR} +Set a global value expression annotation. +@c needs example + +@end ftable + +@node Report Options, Basic options, Session Options, Detailed Option Description +@subsection Report Options + +Options for Ledger reports affect three separate scopes of operation: +Global, Session, and Report. In practice there is very little +difference between these scopes. Ledger 3.0 contains provisions for +GUIs, which would make use of the different scopes by keeping an +instance of Ledger running in the background and running multiple +sessions with multiple reports per session. + +@ftable @option + +@item --abbrev-len @var{INT} +Set the minimum length an account can be abbreviated to if it doesn't +fit inside the @code{account-width}. If @var{INT} is zero, then the +account name will be truncated on the right. If @var{INT} is greater +than @code{account-width} then the account will be truncated on the +left, with no shortening of the account names in order to fit into the +desired width. + +@item --account @var{STR} +Prepend @var{STR} to all accounts reported. That is, the option +@samp{--account Personal} would tack @samp{Personal:} to the beginning +of every account reported in a balance report or register report. + +@item --account-width @var{INT} +Set the width of the account column in the @command{register} report +to @var{INT} characters. + +@item --actual +@itemx -L +Report only real transactions, ignoring all automated or virtual +transactions. + +@item --add-budget +Show only unbudgeted postings. + +@item --amount @var{EXPR} +@itemx -t @var{EXPR} +Apply the given value expression to the posting amount (@pxref{Value +Expressions}). Using @option{--amount @var{EXPR}} you can apply an +arbitrary transformation to the postings. + +@item --amount-data +@itemx -j +On a register report print only the date and amount of postings. +Useful for graphing and spreadsheet applications. + +@item --amount-width @var{INT} +Set the width in characters of the amount column in the +@command{register} report. + +@item --anon +Anonymize registry output, mostly for sending in bug reports. + +@item --auto-match +When generating a ledger transaction from a CSV file using the +@command{convert} command, automatically match an account from the +Ledger journal. + +@item --aux-date +@itemx --effective +Show auxiliary dates for all calculations (@pxref{Effective Dates}). + +@item --average +@itemx -A +Print average values over the number of transactions instead of +running totals. + +@item --balance-format @var{FORMAT_STRING} +Specify the format to use for the @command{balance} report (@pxref{Format +Strings}). The default is: + +@smallexample +"%(justify(scrub(display_total), 20, -1, true, color))" +" %(!options.flat ? depth_spacer : \"\")" +"%-(ansify_if(partial_account(options.flat), blue if color))\n%/" +"%$1\n%/" +"--------------------\n" +@end smallexample + +@item --base +Reduce convertible commodities down the bottom of the conversion, e.g. +display time in seconds. This also applies to custom commodity +conversions (@pxref{Commodity equivalences}). + +@item --basis +@itemx -B +@itemx --cost +Report the cost basis on all posting. + +@item --begin @var{DATE} +Specify the start @var{DATE} of all calculations. Transactions before +that date will be ignored. + +@item --bold-if @var{VEXPR} +Print the entire line in bold if the given value expression is true +(@pxref{Value Expressions}). + +@smallexample @c command:validate +$ ledger reg Expenses --begin Dec --bold-if "amount>100" +@end smallexample + +@noindent +list all transactions since the beginning of December and print in +bold any posting greater than $100. + +@item --budget +Only display budgeted items. In a register report this +displays transactions in the budget, in a balance report this displays +accounts in the budget (@pxref{Budgeting and Forecasting}). + +@item --budget-format @var{FORMAT_STRING} +Specify the format to use for the @command{budget} report (@pxref{Format +Strings}). The default is: + +@smallexample +"%(justify(scrub(get_at(display_total, 0)), 12, -1, true, color))" +" %(justify(-scrub(get_at(display_total, 1)), 12, " +" 12 + 1 + 12, true, color))" +" %(justify(scrub(get_at(display_total, 1) + " +" get_at(display_total, 0)), 12, " +" 12 + 1 + 12 + 1 + 12, true, color))" +" %(ansify_if(" +" justify((get_at(display_total, 1) ? " +" (100% * quantity(scrub(get_at(display_total, 0)))) / " +" -quantity(scrub(get_at(display_total, 1))) : 0), " +" 5, -1, true, false)," +" magenta if (color and get_at(display_total, 1) and " +" (abs(quantity(scrub(get_at(display_total, 0))) / " +" quantity(scrub(get_at(display_total, 1)))) >= 1))))" +" %(!options.flat ? depth_spacer : \"\")" +"%-(ansify_if(partial_account(options.flat), blue if color))\n" +"%/%$1 %$2 %$3 %$4\n%/" +"%(prepend_width ? \" \" * int(prepend_width) : \"\")" +"------------ ------------ ------------ -----\n" +@end smallexample + +@item --by-payee +@itemx -P +Group the register report by payee. + +@item --cleared +@itemx -C +Consider only transactions that have been cleared for display and +calculation. + +@item --cleared-format @var{FORMAT_STRING} +@c FIXME thdox: to keep? +Specify the format to use for the @command{cleared} report (@pxref{Format +Strings}). The default is: + +@smallexample +"%(justify(scrub(get_at(total_expr, 0)), 16, 16 + prepend_width, " +" true, color)) %(justify(scrub(get_at(total_expr, 1)), 18, " +" 36 + prepend_width, true, color))" +" %(latest_cleared ? format_date(latest_cleared) : \" \")" +" %(!options.flat ? depth_spacer : \"\")" +"%-(ansify_if(partial_account(options.flat), blue if color))\n%/" +"%$1 %$2 %$3\n%/" +"%(prepend_width ? \" \" * prepend_width : \"\")" +"---------------- ---------------- ---------\n" +@end smallexample + +@item --collapse +@itemx -n +By default ledger prints all accounts in an account tree. With +@option{--collapse} it prints only the top level account specified. + +@item --collapse-if-zero +Collapse the account display only if it has a zero balance. + +@item --color +@itemx --ansi +Use color if the terminal supports it. + +@item --columns @var{INT} +Specify the width of the @command{register} report in characters. + +@item --count +Direct ledger to report the number of items when appended to the +@command{commodities}, @command{accounts} or @command{payees} command. + +@item --csv-format @var{FORMAT_STRING} +Specify the format to use for the @command{csv} report (@pxref{Format +Strings}). The default is: + +@smallexample +"%(quoted(date))," +"%(quoted(code))," +"%(quoted(payee))," +"%(quoted(display_account))," +"%(quoted(commodity(scrub(display_amount))))," +"%(quoted(quantity(scrub(display_amount))))," +"%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\")))," +"%(quoted(join(note | xact.note)))\n" +@end smallexample + +@item --current +Shorthand for @samp{--limit "date <= today"}. + +@item --daily +@itemx -D +Shorthand for @samp{--period "daily"}. + +@item --date @var{EXPR} +Transform the date of the transaction using @var{EXPR}. + +@item --date-format @var{DATE_FORMAT} +@itemx -y @var{DATE_FORMAT} +Specify the format ledger should use to read and print dates +(@pxref{Date and Time Format Codes}). + +@item --date-width @var{INT} +Specify the width, in characters, of the date column in the +@command{register} report. + +@item --datetime-format @var{DATETIME_FORMAT} +Specify the format ledger should use to print datetimes. + +@item --dc +Display register or balance in debit/credit format If you use +@option{--dc} with either the @command{register} (reg) or +@command{balance} (bal) commands, you will now get extra columns. +The register goes from this: + +@smallexample +12-Mar-10 Employer Assets:Cash $100 $100 + Income:Employer $-100 0 +12-Mar-10 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +12-Mar-10 KFC - Rebate Assets:Cash $5 $5 + Expenses:Food $-5 0 +12-Mar-10 KFC - Food & Reb.. Expenses:Food $20 $20 + Expenses:Food $-5 $15 + Assets:Cash $-15 0 +@end smallexample + +@noindent +To this: + +@smallexample +12-Mar-10 Employer Assets:Cash $100 0 $100 + In:Employer 0 $100 0 +12-Mar-10 KFC Expens:Food $20 0 $20 + Assets:Cash 0 $20 0 +12-Mar-10 KFC - Rebate Assets:Cash $5 0 $5 + Expens:Food 0 $5 0 +12-Mar-10 KFC - Food &.. Expens:Food $20 0 $20 + Expens:Food 0 $5 $15 + Assets:Cash 0 $15 0 +@end smallexample + +@noindent +Where the first column is debits, the second is credits, and the third +is the running total. Only the running total may contain negative +values. + +For the balance report without @option{--dc}: + +@smallexample + $70 Assets:Cash + $30 Expenses:Food + $-100 Income:Employer +-------------------- + 0 +@end smallexample + +@noindent +And with @option{--dc} it becomes this: + +@smallexample + $105 $35 $70 Assets:Cash + $40 $10 $30 Expenses:Food + 0 $100 $-100 Income:Employer +-------------------------------------------- + $145 $145 0 +@end smallexample + +@item --depth @var{INT} +Limit the depth of the account tree. In a balance report, for example, +a @samp{--depth 2} statement will print balances only for accounts with +two levels, i.e. @samp{Expenses:Entertainment} but not +@samp{Expenses:Entertainment:Dining}. This is a display predicate, which +means it only affects display, not the total calculations. + +@item --deviation +Report each posting’s deviation from the average. It is only meaningful +in the register and prices reports. + +@item --display @var{EXPR} +Display only lines that satisfy the expression @var{EXPR}. + +@item --display-amount @var{EXPR} +Apply a transformation to the @emph{displayed} amount. This happens +after calculations occur. + +@item --display-total @var{EXPR} +Apply a transformation to the @emph{displayed} total. This happens after +calculations occur. + +@item --dow +@itemx --days-of-week +Group transactions by the day of the week. + +@smallexample @c command:validate +$ ledger reg Expenses --dow --collapse +@end smallexample + +@noindent +Will print all Expenses totaled for each day of the week. + +@item --empty +@itemx -E +Include empty accounts in the report and in average calculations. + +@item --end @var{DATE} +Specify the end @var{DATE} for a transaction to be considered in the +report. All transactions on or after this date are ignored. + +@item --equity +Related to the @command{equity} command (@pxref{The @command{equity} +command}). Gives current account balances in the form of a register +report. + +@item --exact +Report beginning and ending of periods by the date of the first and last +posting occurring in that period. + +@item --exchange @var{COMMODITY} +@itemx -X @var{COMMODITY} +Display values in terms of the given @var{COMMODITY}. The latest +available price is used. The syntax +@option{-X @var{COMMODITY1}:@var{COMMODITY2}} displays values in @var{COMMODITY1} +in terms of @var{COMMODITY2} using the latest available price, but +will not automatically covert any other commodities to +@var{COMMODITY2}. Multiple @option{-X} arguments may be used on a +single command-line (as in +@option{-X COMMODITY1:COMMODITY2 -X COMMODITY3:COMMODITY2}), +which is particularly useful for situations where many prices are +available for reporting in terms of @var{COMMODITY2}, but only a few +should be displayed that way. + +@item --flat +Force the full names of accounts to be used in the balance report. The +balance report will not use an indented tree. + +@item --force-color +Output TTY color codes even if the TTY doesn't support them. Useful +for TTYs that don't advertise their capabilities correctly. + +@item --force-pager +Force Ledger to paginate its output. + +@item --forecast-while @var{VEXPR} +@itemx --forecast @var{VEXPR} +Continue forecasting while @var{VEXPR} is true. + +@item --forecast-years @var{INT} +Forecast at most @var{INT} years into the future. + +@item --format @var{FORMAT_STRING} +@itemx -F @var{FORMAT_STRING} +Use the given format string to print output. + +@item --gain +@itemx -G +@itemx --change +Report on gains using the latest available prices. + +@item --generated +Include auto-generated postings (such as those from automated +transactions) in the report, in cases where you normally wouldn't want +them. + +@item --group-by @var{EXPR} +Group transactions together in the @command{register} report. +@var{EXPR} can be anything, although most common would be @code{payee} +or @code{commodity}. The @code{tags()} function is also useful here. + +@item --group-title-format @var{FORMAT_STRING} +Set the format for the headers that separates the report sections of +a grouped report. Only has an effect with a @option{--group-by +@var{EXPR}} register report. + +@smallexample @c command:validate +$ ledger reg Expenses --group-by "payee" --group-title-format "------------------------ %-20(value) ---------------------\n" +@end smallexample +@smallexample +------------------------ 7-Eleven --------------------- +2011/08/13 7-Eleven Expenses:Auto:Misc $ 5.80 $ 5.80 + +------------------------ AAA Dues --------------------- +2011/06/02 AAA Dues Expenses:Auto:Misc $ 215.00 $ 215.00 + +------------------------ ABC Towing and Wrecking --------------------- +2011/03/17 ABC Towing and Wrec.. Expenses:Auto:Hobbies $ 48.20 $ 48.20 +... +@end smallexample + +@item --head @var{INT} +@itemx --first @var{INT} +Print the first @var{INT} entries. Opposite of @option{--tail +@var{INT}}. + +@item --historical +@itemx -H +Value commodities at the time of their acquisition. + +@item --immediate +Evaluate calculations immediately rather than lazily. + +@item --inject +Use @code{Expected} amounts in calculations. In case you know +what amount a transaction should be, but the actual transaction has the +wrong value you can use metadata to specify the expected amount: + +@smallexample @c input:validate +2012-03-12 Paycheck + Income $-990; Expected:: $-1000.00 + Checking +@end smallexample + +Then using the command @code{ledger reg --inject=Expected Income} would +treat the transaction as if the ``Expected Value'' was actual. + +@item --invert +Change the sign of all reported values. + +@item --limit @var{EXPR} +@itemx -l @var{EXPR} +Only transactions that satisfy @var{EXPR} are considered in +calculations and for display. + +@item --lot-dates +Report the date on which each commodity in a balance report was +purchased. + +@item --lot-notes +@itemx --lot-tags +Report the tag attached to each commodity in a balance report. + +@item --lot-prices +Report the price at which each commodity in a balance report was +purchased. + +@item --lots +Report the date and price at which each commodity was purchased in +a balance report. + +@item --lots-actual +Preserve the uniqueness of commodities so they aren't merged during +reporting without printing the lot annotations. + +@item --market +@itemx -V +Use the latest market value for all commodities. + +@item --meta @var{TAG} +In the register report, prepend the transaction with the value of the +given @var{TAG}. + +@item --meta-width @var{INT} +Specify the width of the Meta column used for the @option{--meta +@var{TAG}} options. + +@item --monthly +@itemx -M +Synonym for @samp{--period "monthly"}. + +@item --no-aliases +Aliases are completely ignored. + +@item --no-color +Suppress any color TTY output. + +@item --no-pager +Direct output to stdout, avoiding pager program. + +@item --no-revalued +Stop Ledger from showing @code{<Revalued>} postings. This option is useful +in combination with the @option{--exchange} or @option{--market} option. + +@item --no-rounding +Don't output @samp{<Adjustment>} postings. Note that this will cause the +running total to often not add up! Its main use is for +@option{--amount-data (-j)} and @option{--total-data (-J)} reports. + +@item --no-titles +Suppress the output of group titles. + +@item --no-total +Suppress printing the final total line in a balance report. + +@item --now @var{DATE} +Define the current date in case you want to calculate in the past or +future using @option{--current}. + +@item --only @var{FIXME} +This is a postings predicate that applies after certain transforms have +been executed, such as periodic gathering. + +@item --output @var{FILE} +Redirect the output of ledger to the file defined in @file{FILE}. + +@item --pager @var{FILE} +Direct output to @var{FILE} pager program. + +@item --payee @var{VEXPR} +Sets a value expression for formatting the payee. In the +@command{register} report this prevents the second entry from having +a date and payee for each transaction. + +@item --payee-width @var{INT} +Set the number of columns dedicated to the payee in the register +report to @var{INT}. + +@item --pending +Use only postings that are marked pending. + +@item --percent +@itemx -% +Calculate the percentage value of each account in balance reports. +Only works for accounts that have a single commodity. + +@item --period @var{PERIOD_EXPRESSION} +Define a period expression that sets the time period during which +transactions are to be accounted. For a @command{register} report only +the transactions that satisfy the period expression with be displayed. +For a @command{balance} report only those transactions will be accounted +in the final balances. + +@item --pivot @var{TAG} +Produce a balance pivot report @emph{around} the given @var{TAG}. For +example, if you have multiple cars and track each fuel purchase in +@samp{Expenses:Auto:Fuel} and tag each fuel purchase with a tag +identifying which car the purchase was for @samp{; Car: Prius}, then the +command: + +@smallexample @c command:validate +$ ledger bal Fuel --pivot "Car" --period "this year" +@end smallexample +@smallexample + $ 3491.26 Car + $ 1084.22 M3:Expenses:Auto:Fuel + $ 149.65 MG V11:Expenses:Auto:Fuel + $ 621.89 Prius:Expenses:Auto:Fuel + $ 1635.50 Sienna:Expenses:Auto:Fuel + $ 42.69 Expenses:Auto:Fuel +-------------------- + $ 3533.95 +@end smallexample + +@xref{Metadata values}. + +@item --plot-amount-format @var{FORMAT_STRING} +Define the output format for an amount data plot. @xref{Visualizing +with Gnuplot}. + +@item --plot-total-format @var{FORMAT_STRING} +Define the output format for a total data plot. @xref{Visualizing with +Gnuplot}. + +@item --prepend-format @var{FORMAT_STRING} +Prepend @var{STR} to every line of the output. + +@item --prepend-width @var{INT} +Reserve @var{INT} spaces at the beginning of each line of the output. + +@item --price +@itemx -I +Use the price of the commodity purchase for performing calculations. + +@item --pricedb-format @var{FORMAT_STRING} +Set the format expected for the historical price file. + +@item --prices-format @var{FORMAT_STRING} +Set the format for the @command{prices} report. + +@item --primary-date +@itemx --actual-dates +Show primary dates for all calculations (@pxref{Effective Dates}). + +@item --quantity +@itemx -O +Report commodity totals (this is the default). + +@item --quarterly +Synonym for @samp{--period "quarterly"}. + +@item --raw +In the @command{print} report, show transactions using the exact same +syntax as specified by the user in their data file. Don't do any +massaging or interpreting. This can be useful for minor cleanups, like +just aligning amounts. + +@item --real +@itemx -R +Account using only real transactions ignoring virtual and automatic +transactions. + +@item --register-format @var{FORMAT_STRING} +Define the output format for the @command{register} report. + +@item --related +In a @command{register} report show the related account. This is the +other @emph{side} of the transaction. + +@item --related-all +Show all postings in a transaction, similar to @option{--related} but +show both @emph{sides} of each transaction. + +@item --revalued +Report discrepancy in values for manual reports by inserting @code{<Revalued>} +postings. This is implied when using the @option{--exchange} or +@option{--market} option. + +@item --revalued-only +Show only @code{<Revalued>} postings. + +@item --revalued-total @var{FIXME} +Display the sum of the revalued postings as the running total, which serves +to show unrealized capital in a gain/losses report. + +@item --rich-data +@itemx --detail +When generating a ledger transaction from a CSV file using the +@command{convert} command, add CSV, Imported, and UUID metadata. + +@item --seed @var{INT} +Set the random seed to @var{INT} for the @code{generate} command. +Used as part of development testing. + +@item --sort @var{VEXPR} +@itemx -S @var{VEXPR} +Sort the @command{register} report based on the value expression given +to sort. + +@item --sort-all @var{FIXME} +@value{FIXME:UNDOCUMENTED} + +@item --sort-xacts @var{VEXPR} +@itemx --period-sort @var{VEXPR} +Sort the postings within transactions using the given value expression. + +@item --start-of-week @var{INT} +Tell ledger to use a particular day of the week to start its ``weekly'' +summary. @samp{--start-of-week=1} specifies Monday as the start of the +week. + +@item --subtotal +@itemx -s +Cause all transactions in a @command{register} report to be collapsed +into a single, subtotaled transaction. + +@item --tail @var{INT} +@itemx --last @var{INT} +Report only the last @var{INT} entries. Only useful in +a @command{register} report. + +@item --time-report +Add two columns to the balance report to show the earliest checkin and +checkout times for timelog entries. + +@item --total @var{VEXPR} +@itemx -T @var{VEXPR} +Define a value expression used to calculate the total in reports. + +@item --total-data +@itemx -J +Show only dates and totals to format the output for plots. + +@item --total-width @var{INT} +Set the width of the total field in the register report. + +@item --truncate @var{CODE} +Indicates how truncation should happen when the contents of columns +exceed their width. Valid arguments are @samp{leading}, @samp{middle}, +and @samp{trailing}. The default is smarter than any of these three, +as it considers sub-names within the account name (that style is +called ``abbreviate''). + +@item --unbudgeted +Show only unbudgeted postings. + +@item --uncleared +@itemx -U +Use only uncleared transactions in calculations and reports. + +@item --unrealized +Show generated unrealized gain and loss accounts in the balance +report. + +@item --unrealized-gains @var{STR} +Allow the user to specify what account name should be used for +unrealized gains. Defaults to @samp{"Equity:Unrealized Gains"}. +Often set in one's @file{~/.ledgerrc} file to change the default. + +@item --unrealized-losses @var{STR} +Allow the user to specify what account name should be used for +unrealized losses. Defaults to @samp{"Equity:Unrealized Losses"}. +Often set in one's @file{~/.ledgerrc} file to change the default. + +@item --unround +Perform all calculations without rounding and display results to full +precision. + +@item --values +Shows the values used by each tag when used in combination with the +@command{tags} command. + +@item --weekly +@itemx -W +Synonym for @samp{--period "weekly"}. + +@item --wide +Let the register report use 132 columns instead of 80 (the default). +Identical to @samp{--columns "132"}. + +@item --yearly +@itemx -Y +Synonym for @samp{--period "yearly"}. + +@end ftable + +@node Basic options, Report filtering, Report Options, Detailed Option Description +@subsection Basic options + +These are the most basic command options. Most likely, the user will +want to set them using environment variables (see @ref{Environment +variables}), instead of using actual command-line options: + +@ftable @option + +@item --help +@itemx -h +Display the man page for @file{ledger}. + +@item --version +Print the current version of ledger and exits. This is useful for +sending bug reports, to let the author know which version of ledger you +are using. + +@item --file @var{FILE} +@itemx -f @var{FILE} +Read @file{FILE} as a ledger file. @var{FILE} can be @samp{-} which is +a synonym for @samp{/dev/stdin}. This command may be used multiple +times. Typically, the environment variable @env{LEDGER_FILE} is set, +rather than using this command-line option. + +@item --output @var{FILE} +@itemx -o @var{FILE} +Redirect output from any command to @file{FILE}. By default, all output +goes to standard output. + +@item --init-file @var{FILE} +@itemx -i @var{FILE} +Causes @file{FILE} to be read by ledger before any other ledger file. +This file may not contain any postings, but it may contain option +settings. To specify options in the init file, use the same syntax as +on the command-line, but put each option on its own line. Here is an +example init file: + +@smallexample @c input:validate +--price-db ~/finance/.pricedb +--wide +; ~/.ledgerrc ends here +@end smallexample + +Option settings on the command-line or in the environment always take +precedence over settings in the init file. + +@item --account @var{STR} +@itemx -a @var{STR} +Specify the default account which QIF file postings are assumed to +relate to. + +@end ftable + +@node Report filtering, Output customization, Basic options, Detailed Option Description +@subsection Report filtering + +These options change which postings affect the outcome of a +report, in ways other than just using regular expressions: + +@ftable @option + +@item --current +@itemx -c +Display only transactions occurring on or before the current date. + +@item --begin @var{DATE} +@itemx -b @var{DATE} +Constrain the report to transactions on or after @var{DATE}. Only +transactions after that date will be calculated, which means that the +running total in the balance report will always start at zero with the +first matching transaction. (Note: This is different from using +@option{--display @var{EXPR}} to constrain what is displayed). + +@item --end @var{DATE} +@itemx -e @var{DATE} +Constrain the report so that transactions on or after @var{DATE} are +not considered. + +@item --period @var{PERIOD_EXPRESSION} +@itemx -p @var{PERIOD_EXPRESSION} +Set the reporting period to @var{STR}. This will subtotal all matching +transactions within each period separately, making it easy to see +weekly, monthly, quarterly, etc., posting totals. A period string can +even specify the beginning and end of the report range, using simple +terms like @samp{last June} or @samp{next month}. For more details on +period expressions, see @ref{Period Expressions}. + +@item --period-sort @var{VEXPR} +Sort the postings within each reporting period using the value +expression @var{EXPR}. This is most often useful when reporting +monthly expenses, in order to view the highest expense categories at +the top of each month: + +@c TODO: the parameter to --period-sort was -At, which doesn't seem to work any longer +@smallexample @c command:validate +$ ledger -M --period-sort total reg ^Expenses +@end smallexample + +@item --cleared +@itemx -C +Display only postings whose transaction has been marked ``cleared'' +(by placing an asterisk to the right of the date). + +@item --uncleared +@itemx -U +Display only postings whose transaction has not been marked ``cleared'' +(i.e., if there is no asterisk to the right of the date). + +@item --real +@itemx -R +Display only real postings, not virtual. (A virtual posting is +indicated by surrounding the account name with parentheses or brackets; +see @ref{Virtual postings} for more information). + +@item --actual +@itemx -L +Display only actual postings, and not those created by automated +transactions. + +@item --related +@itemx -r +Display postings that are related to whichever postings would +otherwise have matched the filtering criteria. In the register +report, this shows where money went to, or the account it came from. +In the balance report, it shows all the accounts affected by +transactions having a related posting. For example, if a file had +this transaction: + +@smallexample @c input:94C5675 +2004/03/20 Safeway + Expenses:Food $65.00 + Expenses:Cash $20.00 + Assets:Checking $-85.00 +@end smallexample + +And the register command was: + +@smallexample @c command:94C5675 +$ ledger -f example.dat -r register food +@end smallexample + +The following would be printed, showing the postings related to the +posting that matched: + +@smallexample @c output:94C5675 +04-Mar-20 Safeway Expenses:Cash $20.00 $20.00 + Assets:Checking $-85.00 $-65.00 +@end smallexample + +@item --budget +Useful for displaying how close your postings meet your budget. +@option{--add-budget} also shows unbudgeted postings, while +@option{--unbudgeted} shows only those. @option{--forecast @var{VEXPR}} +is a related option that projects your budget into the future, showing +how it will affect future balances. @xref{Budgeting and Forecasting}. + +@item --limit @var{EXPR} +@itemx -l @var{EXPR} +Limit which postings take part in the calculations of a report. + +@item --amount @var{EXPR} +@itemx -t @var{EXPR} +Change the value expression used to calculate the ``value'' column in +the @command{register} report, the amount used to calculate account +totals in the @command{balance} report, and the values printed in the +@command{equity} report. @xref{Value Expressions}. + +@item --total @var{VEXPR} +@itemx -T @var{VEXPR} +Set the value expression used for the ``totals'' column in the +@command{register} and @command{balance} reports. + +@end ftable + +@c @node Search Terms, Output Customization, Report Filtering, Detailed Options Description +@c @subsection Search Terms + +@c Valid Ledger invocations look like: +@c @smallexample +@c ledger [OPTIONS] <COMMAND> <SEARCH-TERMS> +@c @end smallexample + +@c Where @code{COMMAND} is any command verb (@pxref{Reporting +@c Commands}), @code{OPTIONS} can occur anywhere, and +@c @code{SEARCH-TERM} is one or more of the following: + +@c @smallexample +@c word search for any account containing 'word' +@c TERM and TERM boolean AND between terms +@c TERM or TERM boolean OR between terms +@c not TERM invert the meaning of the term +@c payee word search for any payee containing 'word' +@c @@word shorthand for 'payee word' +@c desc word alternate for 'payee word' +@c note word search for any note containing 'word' +@c &word shorthand for 'note word' +@c tag word search for any metadata tag containing 'word' +@c tag word=value search for any metadata tag containing 'word' +@c whose value contains 'value' +@c %word shorthand for 'tag word' +@c %word=value shorthand for 'tag word=value' +@c meta word alternate for 'tag word' +@c meta word=value alternate for 'tag word=value' +@c expr 'EXPR' apply the given value expression as a predicate +@c '=EXPR' shorthand for 'expr EXPR' +@c \( TERMS \) group terms; useful if using and/or/not +@c @end smallexample + +@c So, to list all transaction that charged to ``food'' but not +@c ``dining'' for any payee other than ``chang'' the following three +@c commands would be equivalent: + +@c @smallexample +@c ledger reg food not dining @@chang +@c ledger reg food and not dining and not payee chang +@c ledger reg food not dining expr 'payee =~ /chang/' +@c @end smallexample + +@node Output customization, Commodity reporting, Report filtering, Detailed Option Description +@subsection Output customization + +These options affect only the output, but not which postings are +used to create it: + +@ftable @option + +@item --collapse +@itemx -n +Cause transactions in a @command{register} report with multiple +postings to be collapsed into a single, subtotaled transaction. + +@item --subtotal +@itemx -s +Cause all transactions in a @command{register} report to be collapsed +into a single, subtotaled transaction. + +@item --by-payee +@itemx -P +Report subtotals by payee. + +@item --empty +@itemx -E +Include even empty accounts in the @command{balance} report. + +@item --weekly +@itemx -W +Report posting totals by the week. The week begins on whichever day of +the week begins the month containing that posting. To set a specific +begin date, use a period string, such as @samp{weekly from DATE}. + +@item --monthly +@itemx -M +Report posting totals by month. + +@item --yearly +@itemx -Y +Report posting totals by year. For more complex periods, use +@option{--period}. +@c TODO end this sentence + +@item --period @var{PERIOD_EXPRESSION} +Option described above. + +@item --dow +Report posting totals for each day of the week. This is an easy way +to see if weekend spending is more than on weekdays. + +@item --sort @var{VEXPR} +@itemx -S @var{VEXPR} +Sort a report by comparing the values determined using the value +expression @var{VEXPR}. For example, using @samp{-S "-abs(total)"} in +the @command{balance} report will sort account balances from greatest to +least, using the absolute value of the total. For more on how to use +value expressions, see @ref{Value Expressions}. + +@item --pivot @var{TAG} +Produce a pivot table around the @var{TAG} provided. This requires +meta data using valued tags. + +@item --wide +@itemx -w +Cause the default @command{register} report to assume 132 columns +instead of 80. + +@item --head @var{INT} +Cause only the first @var{INT} transactions to be printed. This is +different from using the command-line utility @file{head}, which would +limit to the first @var{INT} postings. @option{--tail @var{INT}} outputs +only the last @var{INT} transactions. Both options may be used +simultaneously. If a negative amount is given, it will invert the +meaning of the flag (instead of the first five transactions being +printed, for example, it would print all but the first five). + +@item --pager @var{FILE} +Tell Ledger to pass its output to the given @var{FILE} pager program; +very useful when the output is especially long. This behavior can be +made the default by setting the @env{LEDGER_PAGER} environment variable. + +@item --no-pager +Tell Ledger to @emph{not} pass its output to a pager program; useful +when a pager is set by default. + +@item --average +@itemx -A +Report the average posting value. + +@item --deviation +@itemx -D +Report each posting's deviation from the average. It is only meaningful +in the @command{register} and @command{prices} reports. + +@item --percent +@itemx -% +Show account subtotals in the @command{balance} report as percentages of +the parent account. + +@c @option{--totals} include running total information in the +@c @command{xml} report. + +@item --amount-data +@itemx -j +Change the @command{register} report so that it prints nothing but the +date and the value column, and the latter without commodities. This is +only meaningful if the report uses a single commodity. This data can +then be fed to other programs, which could plot the date, analyze it, +etc. + +@item --total-data +@itemx -J +Change the @command{register} report so that it prints nothing but the +date and total columns, without commodities. + +@item --display @var{EXPR} +@itemx -d @var{EXPR} +Limit which postings or accounts are actually displayed in a report. +They might still be calculated, and be part of the running total of a +register report, for example, but they will not be displayed. This is +useful for seeing last month's checking postings, against a running +balance which includes all posting values: + +@smallexample @c command:validate +$ ledger -d "d>=[last month]" reg checking +@end smallexample + +The output from this command is very different from the following, +whose running total includes only postings from the last month +onward: + +@smallexample @c command:validate +$ ledger -p "last month" reg checking +@end smallexample + +Which is more useful depends on what you're looking to know: the total +amount for the reporting range (using @option{--period +@var{PERIOD_EXPRESSION} (-p)}), or simply a display restricted to the +reporting range (using @option{--display @var{EXPR} (-d)}). + +@item --date-format @var{DATE_FORMAT} +@itemx -y @var{DATE_FORMAT} +Change the basic date format used by reports. The default uses a date +like @samp{2004/08/01}, which represents the default date format of +@code{%Y/%m/%d}. To change the way dates are printed in general, the +easiest way is to put @option{--date-format @var{DATE_FORMAT}} in the +Ledger initialization file @file{~/.ledgerrc} (or the file referred to +by @env{LEDGER_INIT}). + +@item --format @var{FORMAT_STRING} +@itemx -F @var{FORMAT_STRING} +Set the reporting format for whatever report ledger is about to make. +@xref{Format Strings}. There are also specific format commands for +each report type: + +@item --balance-format @var{FORMAT_STRING} +Define the output format for the @command{balance} report. The default +(defined in @file{report.h} is: + +@smallexample +"%(ansify_if( + justify(scrub(display_total), 20, + 20 + int(prepend_width), true, color), + bold if should_bold)) + %(!options.flat ? depth_spacer : \"\") + %-(ansify_if( + ansify_if(partial_account(options.flat), blue if color), + bold if should_bold))\n%/ + %$1\n%/ + %(prepend_width ? \" \" * int(prepend_width) : \"\") + --------------------\n" +@end smallexample + +@item --cleared-format @var{FORMAT_STRING} +Define the format for the cleared report. The default is: + +@smallexample +"%(justify(scrub(get_at(display_total, 0)), 16, 16 + int(prepend_width), + true, color)) %(justify(scrub(get_at(display_total, 1)), 18, + 36 + int(prepend_width), true, color)) + %(latest_cleared ? format_date(latest_cleared) : \" \") + %(!options.flat ? depth_spacer : \"\") + %-(ansify_if(partial_account(options.flat), blue if color))\n%/ + %$1 %$2 %$3\n%/ + %(prepend_width ? \" \" * int(prepend_width) : \"\") + ---------------- ---------------- ---------\n" +@end smallexample + +@item --register-format @var{FORMAT_STRING} +Define the output format for the @command{register} report. The default +(defined in @file{report.h} is: + +@smallexample +"%(ansify_if( + ansify_if(justify(format_date(date), int(date_width)), + green if color and date > today), + bold if should_bold)) + %(ansify_if( + ansify_if(justify(truncated(payee, int(payee_width)), int(payee_width)), + bold if color and !cleared and actual), + bold if should_bold)) + %(ansify_if( + ansify_if(justify(truncated(display_account, int(account_width), + int(abbrev_len)), int(account_width)), + blue if color), + bold if should_bold)) + %(ansify_if( + justify(scrub(display_amount), int(amount_width), + 3 + int(meta_width) + int(date_width) + int(payee_width) + + int(account_width) + int(amount_width) + int(prepend_width), + true, color), + bold if should_bold)) + %(ansify_if( + justify(scrub(display_total), int(total_width), + 4 + int(meta_width) + int(date_width) + int(payee_width) + + int(account_width) + int(amount_width) + int(total_width) + + int(prepend_width), true, color), + bold if should_bold))\n%/ + %(justify(\" \", int(date_width))) + %(ansify_if( + justify(truncated(has_tag(\"Payee\") ? payee : \" \", + int(payee_width)), int(payee_width)), + bold if should_bold)) + %$3 %$4 %$5\n" +@end smallexample + +@item --csv-format @var{FORMAT_STRING} +Set the format for @command{csv} reports. The default is: + +@smallexample +"%(quoted(date)), + %(quoted(code)), + %(quoted(payee)), + %(quoted(display_account)), + %(quoted(commodity(scrub(display_amount)))), + %(quoted(quantity(scrub(display_amount)))), + %(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\"))), + %(quoted(join(note | xact.note)))\n" +@end smallexample + +@item --plot-amount-format @var{FORMAT_STRING} +Set the format for amount plots, using the @option{--amount-data (-j)} +option. The default is: + +@smallexample +"%(format_date(date, \"%Y-%m-%d\")) %(quantity(scrub(display_amount)))\n" +@end smallexample + +@item --plot-total-format @var{FORMAT_STRING} +Set the format for total plots, using the @option{--total-data (-J)} +option. The default is: + +@smallexample +"%(format_date(date, \"%Y-%m-%d\")) %(quantity(scrub(display_total)))\n" +@end smallexample + +@item --pricedb-format @var{FORMAT_STRING} +Set the format expected for the historical price file. The default is: + +@smallexample +"P %(datetime) %(display_account) %(scrub(display_amount))\n" +@end smallexample + +@item --prices-format @var{FORMAT_STRING} +Set the format for the @command{prices} report. The default is: + +@smallexample +"%(date) %-8(display_account) %(justify(scrub(display_amount), 12, + 2 + 9 + 8 + 12, true, color))\n" +@end smallexample + +@end ftable + +@node Commodity reporting, Environment variables, Output customization, Detailed Option Description +@subsection Commodity reporting + +These options affect how commodity values are displayed: + +@ftable @option + +@item --price-db @var{FILE} +Set the file that is used for recording downloaded commodity prices. +It is always read on startup, to determine historical prices. Other +settings can be placed in this file manually, to prevent downloading +quotes for a specific commodity, for example. This is done by adding a +line like the following: + +@smallexample @c input:validate +; Don't download quotes for the dollar, or timelog values +N $ +N h +@end smallexample + +@noindent +Note: Ledger NEVER writes output to files. You are responsible for +updating the price-db file. The best way is to have your price +download script maintain this file. + +The format of the file can be changed by telling ledger to use the +@option{--pricedb-format @var{FORMAT_STRING}} you define. + +@item --price-exp @var{INT} +@itemx --leeway @var{INT} +@itemx -Z @var{INT} +Set the expected freshness of price quotes, in @var{INT} minutes. That +is, if the last known quote for any commodity is older than this value, +and if @option{--download} is being used, then the Internet will be +consulted again for a newer price. Otherwise, the old price is still +considered to be fresh enough. + +@item --download +@itemx -Q +Cause quotes to be automagically downloaded, as needed, by running +a script named @file{getquote} and expecting that script to return +a value understood by ledger. A sample implementation of +a @file{getquote} script, implemented in Perl, is provided in the +distribution. Downloaded quote price are then appended to the price +database, usually specified using the environment variable +@env{LEDGER_PRICE_DB}. + +@end ftable + +There are several different ways that ledger can report the totals it +displays. The most flexible way to adjust them is by using value +expressions, and the @option{--amount @var{EXPR} (-t)} and +@option{--total @var{VEXPR} (-T)} options. However, there are also +several ``default'' reports, which will satisfy most users' basic +reporting needs: + +@ftable @option + +@item --quantity +@itemx -O +Report commodity totals (this is the default). + +@item --basis +@itemx -B +Report the cost basis for all postings. + +@item --market +@itemx -V +Use the last known value for commodities to calculate final values. + +@item --gain +@itemx -G +Report the net gain/loss for all commodities in the report that have +a price history. + +@end ftable + +Often you will be more interested in the value of your entire holdings, +in your preferred currency. It might be nice to know you hold 10,000 +shares of PENNY, but you are more interested in whether or not that is +worth $1000.00 or $10,000.00. However, the current day value of a +commodity can mean different things to different people, depending on +the accounts involved, the commodities, the nature of the transactions, +etc. + +@findex --now @var{DATE} +@findex --market +@findex --exchange @var{COMMODITY} + +When you specify @option{--market (-V)}, or @option{--exchange +@var{COMMODITY} (-X)}, you are requesting that some or all of the +commodities be valuated as of today (or whatever @option{--now +@var{DATE}} is set to). But what does such a valuation mean? This +meaning is governed by the presence of a @var{VALUE} meta-data property, +whose content is an expression used to compute that value. + +If no @var{VALUE} property is specified, each posting is assumed to have +a default, as if you'd specified a global, automated transaction as +follows: + +@smallexample @c input:validate += expr true + ; VALUE:: market(amount, date, exchange) +@end smallexample + +This definition emulates the present day behavior of @option{--market +(-V)} and @option{--exchange @var{COMMODITY} (-X)} (in the case of +@samp{-X}, the requested commodity is passed via the string +@samp{exchange} above). + +@cindex Euro conversion +One thing many people have wanted to do is to fixate the valuation of +old European currencies in terms of the Euro after a certain date: + +@smallexample @c input:validate += expr commodity == "DM" + ; VALUE:: date < [Jun 2008] ? market(amount, date, exchange) : 1.44 EUR +@end smallexample + +This says: If @option{--now @var{DATE}} is some old date, use market +prices as they were at that time; but if @option{--now @var{DATE}} is +past June 2008, use a fixed price for converting Deutsch Mark to Euro. + +Or how about never re-valuating commodities used in Expenses, since +they cannot have a different future value: + +@smallexample @c input:validate += /^Expenses:/ + ; VALUE:: market(amount, post.date, exchange) +@end smallexample + +This says the future valuation is the same as the valuation at the time +of posting. @code{post.date} equals the posting's date, while just 'date' is +the value of @option{--now @var{DATE}} (defaults to today). + +Or how about valuating miles based on a reimbursement rate during a +specific time period: + +@smallexample @c input:validate += expr commodity == "miles" and date >= [2007] and date < [2008] + ; VALUE:: market($1.05, date, exchange) +@end smallexample + +In this case, miles driven in 2007 will always be valuated at $1.05 +each. If you use @samp{-X EUR} to expressly request all amounts in +Euro, Ledger shall convert $1.05 to Euro by whatever means are +appropriate for dollars. + +Note that you can have a valuation expression specific to a particular +posting or transaction, by overriding these general defaults using +specific meta-data: + +@smallexample @c input:validate +2010-12-26 Example + Expenses:Food $20 + ; Just to be silly, always valuate *these* $20 as 30 DM, no matter what + ; the user asks for with -V or -X + ; VALUE:: 30 DM + Assets:Cash +@end smallexample + +This example demonstrates that your value expression should be as +symbolic as possible, using terms like 'amount' and 'date', rather than +specific amounts and dates. Also, you should pass the amount along to +the function 'market' so it can be further revalued if the user has +asked for a specific currency. + +Or, if it better suits your accounting, you can be less symbolic, +which allows you to report most everything in EUR if you use @samp{-X +EUR}, except for certain accounts or postings which should always be +valuated in another currency. For example: + +@c TODO is this example missing the actual line to get the effect? +@c it looks like it only contains a match, but no effect +@smallexample @c input:validate += /^Assets:Brokerage:CAD$/ + ; Always report the value of commodities in this account in + ; terms of present day dollars, despite what was asked for + ; on the command-line VALUE:: market(amount, date, @samp{$}) +@end smallexample + +@cindex FIFO/LIFO +@cindex LIFO/FIFO +@findex --lots +@findex --lot-prices +@findex --exchange @var{COMMODITY} +@findex --historical +@findex --basis +@findex --price + +Ledger presently has no way of handling such things as FIFO and LIFO. + +If you specify an unadorned commodity name, like AAPL, it will balance +against itself. If @option{--lots} are not being displayed, then it +will appear to balance against any lot of AAPL. + +@cindex adorned commodity +@findex --lot-prices +If you specify an adorned commodity, like AAPL @{$10.00@}, it will also +balance against itself, and against any AAPL if @option{--lots} is not +specified. But if you do specify @option{--lot-prices}, for example, +then it will balance against that specific price for AAPL. + +Normally when you use @option{--exchange @var{COMMODITY} (-X)} to +request that amounts be reported in a specific commodity, Ledger uses +these values: + +@itemize + +@item Register Report +For the @command{register} report, use the value of that commodity on +the date of the posting being reported, with a @samp{<Revalued>} posting +added at the end if today's value is different from the value of the +last posting. + +@item Balance Report +For the @command{balance} report, use the value of that commodity as of +today. + +@end itemize + +You can now specify @option{--historical (-H)} to ask that all +valuations for any amount be done relative to the date that amount was +encountered. + +You can also now use @option{--exchange @var{COMMODITY} (-X)} (and +@option{--historical (-H)}) in conjunction with @option{--basis (-B)} +and @option{--price (-I)}, to see valuation reports of just your basis +costs or lot prices. + +Finally, sometimes, you may seek to only report one (or some subset) of +the commodities in terms of another commodity. In this situation, you +can use the syntax @option{--exchange @var{COMMODITY1}:@var{COMMODITY2}} +to request that ledger always display @var{COMMODITY1} in terms of +@var{COMMODITY2}, but you want no other commodities to be automatically +displayed in terms of @var{COMMODITY2} without additional +@option{--exchange} options. For example, if you wanted to report EUR +and BTC in terms of USD, but report all other commodities without +conversion to USD, you could use: @option{--exchange EUR:USD --exchange +BTC:USD}. + +@node Environment variables, , Commodity reporting, Detailed Option Description +@subsection Environment variables + +Every option to ledger may be set using an environment variable if the +option has a long name. For example setting the environment variable +@samp{@env{LEDGER_DATE_FORMAT}="%d.%m.%Y"} will have the same effect as specifying +@samp{@option{--date-format} '%d.%m.%Y'} on the command-line. Options on the +command-line always take precedence over environment variable settings, however. + +Note that you may also permanently specify option values by placing +option settings in the file @file{~/.ledgerrc} one option per line, for example: + +@smallexample @c input:validate +--pager /bin/cat +@end smallexample + +@node Period Expressions, , Detailed Option Description, Command-Line Syntax +@section Period Expressions +@c TODO use @var below + +A period expression indicates a span of time, or a reporting interval, +or both. Ledger's end dates are always exclusive, imagine the date is +followed by 00:00:00 time. They are instants in time not entire days. +The full syntax is: + +@smallexample +[INTERVAL] [BEGIN] [END] +@end smallexample + +The optional @var{INTERVAL} part may be any one of: + +@smallexample +every day +every week +every month +every quarter +every year +every N days # N is any integer +every N weeks +every N months +every N quarters +every N years +daily +weekly +biweekly +monthly +bimonthly +quarterly +yearly +@end smallexample + +After the interval, a begin time, end time, both or neither may be +specified. As for the begin time, it can be either of: + +@smallexample +from <SPEC> +since <SPEC> +@end smallexample + +The end time can be either of: + +@smallexample +to <SPEC> +until <SPEC> +@end smallexample + +Where @var{SPEC} can be any of: + +@smallexample +2004 +2004/10 +2004/10/1 +10/1 +october +oct +this week # or day, month, quarter, year +next week +last week +@end smallexample + +The beginning and ending can be given at the same time, if it spans a +single period. In that case, just use @var{SPEC} by itself. In that +case, the period @samp{oct}, for example, will cover all the days in +October. The possible forms are: + +@smallexample +<SPEC> +in <SPEC> +@end smallexample + +Here are a few examples of period expressions: + +@smallexample +monthly +monthly in 2004 +weekly from oct +weekly from last month +from sep to oct +from 10/1 to 10/5 +monthly until 2005 +from apr +until nov +last oct +weekly last august +@end smallexample + +@node Budgeting and Forecasting, Time Keeping, Command-Line Syntax, Top +@chapter Budgeting and Forecasting + +@menu +* Budgeting:: +* Forecasting:: +@end menu + +@node Budgeting, Forecasting, Budgeting and Forecasting, Budgeting and Forecasting +@section Budgeting +@findex --budget +@findex --add-budget +@findex --unbudgeted +@findex --monthly + +Keeping a budget allows you to pay closer attention to your income and +expenses, by reporting how far your actual financial activity is from +your expectations. + +To start keeping a budget, put some periodic transactions +(@pxref{Periodic Transactions}) at the top of your ledger file. A +periodic transaction is almost identical to a regular transaction, except +that it begins with a tilde and has a period expression in place of a +payee. For example: + +@smallexample @c input:validate +~ Monthly + Expenses:Rent $500.00 + Expenses:Food $450.00 + Expenses:Auto:Gas $120.00 + Expenses:Insurance $150.00 + Expenses:Phone $125.00 + Expenses:Utilities $100.00 + Expenses:Movies $50.00 + Expenses $200.00 ; all other expenses + Assets + +~ Yearly + Expenses:Auto:Repair $500.00 + Assets +@end smallexample + +These two periodic transactions give the usual monthly expenses, as well +as one typical yearly expense. For help on finding out what your +average monthly expenses are for any category, use a command like: + +@smallexample @c command:validate +$ ledger -p "this year" --monthly --average balance ^expenses +@end smallexample + +The reported totals are the current year's average for each account. + +Once these periodic transactions are defined, creating a budget report is +as easy as adding @option{--budget} to the command-line. For example, +a typical monthly expense report would be: + +@smallexample @c command:validate +$ ledger --monthly register ^expenses +@end smallexample + +To see the same report balanced against your budget, use: + +@smallexample @c command:validate +$ ledger --budget --monthly register ^expenses +@end smallexample + +A budget report includes only those accounts that appear in the budget. +To see all expenses balanced against the budget, use +@option{--add-budget}. You can even see only the unbudgeted expenses +using @option{--unbudgeted}: + +@smallexample @c command:validate +$ ledger --unbudgeted --monthly register ^expenses +@end smallexample + +You can also use these flags with the @command{balance} command. + +@node Forecasting, , Budgeting, Budgeting and Forecasting +@section Forecasting +@findex --forecast @var{VEXPR} + +Sometimes it's useful to know what your finances will look like in the +future, such as determining when an account will reach zero. Ledger +makes this easy to do, using the same periodic transactions as are used +for budgeting. An example forecast report can be generated with: + +@smallexample @c command:validate +$ ledger --file drewr3.dat --forecast "T>@{\$-500.00@}" register ^assets ^liabilities +@end smallexample + +This report continues outputting postings until the running total +is greater than $-500.00. A final posting is always shown, to +inform you what the total afterwards would be. + +Forecasting can also be used with the @command{balance} report, +but by date only, and not against the running total: + +@smallexample @c command:validate +$ ledger --forecast "d<[2010]" bal ^assets ^liabilities +@end smallexample + +@node Time Keeping, Value Expressions, Budgeting and Forecasting, Top +@chapter Time Keeping +@findex --day-break + +@anchor{timelog} +Ledger directly supports ``timelog'' entries, which have this form: + +@smallexample @c input:validate +i 2013/03/28 22:13:00 ACCOUNT[ PAYEE] +o 2013/03/29 03:39:00 +@end smallexample + +This records a check-in to the given ACCOUNT, and a check-out. You can +be checked-in to multiple accounts at a time, if you wish, and they can +span multiple days (use @option{--day-break} to break them up in the +report). The number of seconds between check-in and check-out is +accumulated as time to that ACCOUNT. If the checkout uses a capital +@samp{O}, the transaction is marked ``cleared''. You can use an +optional PAYEE for whatever meaning you like. + +Now, there are a few ways to generate this information. You can use +the @file{timeclock.el} package, which is part of Emacs. Or you can +write a simple script in whichever language you prefer to emit similar +information. Or you can use Org mode's time-clocking abilities and +the @file{org2tc} script developed by John Wiegley. + +These timelog entries can appear in a separate file, or directly in +your main ledger file. The initial @samp{i} and @samp{o} characters +count as Ledger ``directives'', and are accepted anywhere that +ordinary transactions are valid. + +@node Value Expressions, Format Strings, Time Keeping, Top +@chapter Value Expressions +@findex --limit @var{EXPR} +@findex --display @var{EXPR} + +Ledger uses value expressions to make calculations for many different +purposes: + +@enumerate + +@item +The values displayed in reports. + +@item +For predicates (where truth is anything non-zero), to determine which +postings are calculated (option @option{--limit @var{EXPR} (-l)}) or +displayed (option @option{--display @var{EXPR} (-d)}). + +@item +For sorting criteria, to yield the sort key. + +@item +In the matching criteria used by automated postings. + +@end enumerate + +Value expressions support most simple math and logic operators, in +addition to a set of functions and variables. + +@c A function's argument is whatever follows it. The following is +@c a display predicate that I use with the @command{balance} command: + +@c @smallexample +@c ledger -d '/^Liabilities/?T<0:UT>100' balance +@c @end smallexample + +@c The effect is that account totals are displayed only if: 1) A +@c Liabilities account has a total less than zero; or 2) the absolute +@c value of the account's total exceeds 100 units of whatever commodity +@c contains. If it contains multiple commodities, only one of them must +@c exceed 100 units. + +Display predicates are also very handy with register reports, to +constrain which transactions are printed. For example, the following +command shows only transactions from the beginning of the current month, +while still calculating the running balance based on all transactions: + +@smallexample @c command:validate +$ ledger -d "d>[this month]" register checking +@end smallexample + +The advantage of this command's complexity is that it prints the +running total in terms of all transactions in the register. The +following, simpler command is similar, but totals only the displayed +postings: + +@smallexample @c command:validate +$ ledger -b "this month" register checking +@end smallexample + +@menu +* Variables:: +* Functions:: +* Operators:: +* Complex expressions:: +@end menu + +@node Variables, Functions, Value Expressions, Value Expressions +@section Variables +@findex --amount @var{EXPR} +@findex --total @var{VEXPR} + +Below are the one letter variables available in any value expression. +For the @command{register} and @command{print} commands, these variables +relate to individual postings, and sometimes the account affected by a +posting. For the @command{balance} command, these variables relate to +accounts, often with a subtle difference in meaning. The use of each +variable for both is specified. + +@table @code + +@item t +This maps to whatever the user specified with @option{--amount +@var{EXPR} (-t)}. In a @command{register} report, @option{--amount +@var{EXPR} (-t)} changes the value column; in a @command{balance} +report, it has no meaning by default. If @option{--amount @var{EXPR} +(-t)} was not specified, the current report style's value expression is +used. + +@item T +This maps to whatever the user specified with @option{--total +@var{VEXPR} (-T)}. In a register report, @option{--total @var{VEXPR} +(-T)} changes the totals column; in a balance report, this is the value +given for each account. If @option{--total @var{VEXPR} (-T)} was not +specified, the current report style's value expression is used. + +@item m +This is always the present moment/date. + +@end table + +@menu +* Posting/account details:: +* Calculated totals:: +@end menu + +@node Posting/account details, Calculated totals, Variables, Variables +@subsection Posting/account details + +@table @code + +@item d +A posting's date, as the number of seconds past the epoch. This +is always ``today'' for an account. + +@item a +The posting's amount; the balance of an account, without +considering children. + +@item b +The cost of a posting; the cost of an account, without its +children. + +@item v +The market value of a posting or an account, without its children. + +@item g +The net gain (market value minus cost basis), for a posting or an +account, without its children. It is the same as @samp{v-b}. + +@item l +The depth (``level'') of an account. If an account has one parent, +its depth is one. + +@item n +The index of a posting, or the count of postings affecting an +account. + +@item X +@samp{1} if a posting's transaction has been cleared, @samp{0} otherwise. + +@item R +@samp{1} if a posting is not virtual, @samp{0} otherwise. + +@item Z +@samp{1} if a posting is not automated, @samp{0} otherwise. + +@end table + +@node Calculated totals, , Posting/account details, Variables +@subsection Calculated totals + +@table @code + +@item O +The total of all postings seen so far, or the total of an account +and all its children. + +@item N +The total count of postings affecting an account and all its +children. + +@end table + +@node Functions, Operators, Variables, Value Expressions +@section Functions + +The available one letter functions are: + +@table @code + +@item - +Negates the argument. + +@item U +The absolute (unsigned) value of the argument. + +@item S +Strips the commodity from the argument. + +@item P +The present market value of the argument. The syntax @samp{P(x,d)} is +supported, which yields the market value at time @samp{d}. If no date +is given, then the current moment is used. + +@end table + +@node Operators, Complex expressions, Functions, Value Expressions +@section Operators + +The binary and ternary operators, in order of precedence, are: + +@enumerate +@item @code{* /} +@item @code{+ -} +@item @code{! < > =} +@item @code{& | ?:} +@end enumerate + +@menu +* Unary Operators:: +* Binary Operators:: +@end menu + +@node Unary Operators, Binary Operators, Operators, Operators +@subsection Unary Operators + +@code{not} +@code{neg} + +@node Binary Operators, , Unary Operators, Operators +@subsection Binary Operators + +@code{==} +@code{<} +@code{<=} +@code{>} +@code{>=} +@code{and} +@code{or} +@code{+} +@code{-} +@code{*} +@code{/} +@code{QUERY} +@code{COLON} +@code{CONS} +@code{SEQ} +@code{DEFINE} +@code{LOOKUP} +@code{LAMBDA} +@code{CALL} +@code{MATCH} + +@node Complex expressions, , Operators, Value Expressions +@section Complex expressions + +More complicated expressions are possible using: + +@table @code + +@item "amount == COMMODITY AMOUNT" +The amount can be any kind of amount supported by ledger, +with or without a commodity. Use this for decimal values. + +@item /REGEX/ +@itemx account =~ /REGEX/ +A regular expression that matches against an account's full name. If +a posting, this will match against the account affected by the +posting. + +@item @@/REGEX/ +@itemx expr payee =~ /REGEX/ +A regular expression that matches against a transaction's payee name. + +@item %/REGEX/ +@itemx expr has_tag(/REGEX/) +@itemx expr has_tag('TAG') +A regular expression (REGEX) or string (TAG) that checks for the tags of +a transaction. + +@item tag(REGEX) =~ /REGEX/ +A regular expression that matches a transaction's tags against its values. + +@item expr date =~ /REGEX/ +Useful for specifying a date in plain terms. For example, you could say +@samp{expr date =~ /2014/}. + +@item expr comment =~ /REGEX/ +A regular expression that matches against a posting's comment +field. This searches only a posting's field, not the transaction's note +or comment field. For example, @code{ledger reg "expr" "comment =~ +/landline/"} will match: + +@smallexample @c input:validate +2014/1/29 Phone bill + Assets:Checking $50.00 + Expenses:Phone $-50.00 ; landline bill +@end smallexample + +but will not match: + +@smallexample @c input:validate +2014/1/29 Phone bill ; landline bill + ; landline bill + Assets:Checking $50.00 + Expenses:Phone $-50.00 +@end smallexample + +To match the latter, use @samp{ledger reg "expr" "note =~ /landline/"} +instead. + +@item expr note =~ /REGEX/ +A regular expression that matches against a transaction's note field. +This searches all comments in the transaction, including comments on +individual postings. Thus, @samp{ledger reg "expr" "note =~ /landline/"} +will match both all the three examples below: + +@smallexample @c input:validate +2014/1/29 Phone bill + Assets:Checking $50.00 + Expenses:Phone $-50.00 ; landline bill +@end smallexample + +@smallexample @c input:validate +2014/1/29 Phone bill ; landline bill + Assets:Checking $50.00 + Expenses:Phone $-50.00 +@end smallexample + +@smallexample @c input:validate +2014/1/29 Phone bill + ; landline bill + Assets:Checking $50.00 + Expenses:Phone $-50.00 +@end smallexample + +@item (EXPR) +A sub-expression is nested in parenthesis. This can be useful passing +more complicated arguments to functions, or for overriding the natural +precedence order of operators. + +@item expr base =~ /REGEX/ +A regular expression that matches against an account's base name. If +a posting, this will match against the account affected by the +posting. + +@item expr code =~ /REGEX/ +A regular expression that matches against the transaction code (the +text that occurs between parentheses before the payee). + +@item expr any(KEYWORD =~ /REGEX/) +The @command{any} keyword is used to specify that at least one posting of +the transaction must match the expression in brackets. For example, +@samp{ledger -f d reg expr "any(account =~ /Assets:/)"} can be used to +display all transactions which involve at least one @samp{Assets:} +account. + +@item expr all(KEYWORD =~ /REGEX/) +The @command{all} keyword is used to specify that all postings of a +transactions must match the expression in brackets. For example, +@samp{ledger -f d reg expr "all(account =~ /Assets:/)"} can be used to +display all transactions where all accounts are @samp{Assets:}. + +@end table + +The @command{query} command can be used to see how Ledger interprets +your query. This can be useful if you are not getting the results you +expect (@pxref{Pre-Commands}). + +@menu +* Miscellaneous:: +@end menu + +@node Miscellaneous, , Complex expressions, Complex expressions +@subsection Miscellaneous + +The following Ledger journal data (saved as @file{expr.dat}) is used to explain the behaviour of the +functions and variables below: +@anchor{expr.dat} +@smallexample @c input:3406FC1 +2015/01/16 * (C0D3) Payee + Assets:Cash ¤ -123,45 + ; Payee: PiggyBank + Expenses:Office Supplies +@end smallexample + +@defun abs value +@defunx U value +Return the absolute value of the given @var{value}, e.g. @var{amount}. +@smallexample @c command:3406FC1 +$ ledger -f expr.dat --format "%(account) %(abs(amount))\n" reg assets +@end smallexample +@smallexample @c output:3406FC1 +Assets:Cash ¤ 123,45 +@end smallexample +@end defun + +@defun amount_expr +Return the calculated amount of the posting according to the @option{--amount} +option. +@end defun + +@defun ansify_if value color bool +Render the given @var{expression} as a string, applying the proper ANSI escape +codes to display it in the given @var{color} if @var{bool} is true. It +typically checks the value of the option @option{--color}. Since ANSI escape +codes include non-printable character sequences, such as escape @kbd{^[} +the following example may not appear as the final result on the command-line. +@smallexample @c command:4D836EE,with_input:3406FC1 +$ ledger -f expr.dat --format "%(ansify_if(account, blue, options.color))\n" reg +@end smallexample +@smallexample @c output:4D836EE +Assets:Cash +Expenses:Office Supplies +@end smallexample +@end defun + +@defun ceiling value +Return the next integer of @var{value} toward @math{+}infinity. +@smallexample @c command:FF9C18C,with_input:3406FC1 +$ ledger -f expr.dat --format "%(account) %(ceiling(amount))\n" reg +@end smallexample +@smallexample @c output:FF9C18C +Assets:Cash ¤ -123,00 +Expenses:Office Supplies ¤ 124,00 +@end smallexample +@end defun + +@defvar code +Return the transaction code, the string between the parenthesis after the date. +@smallexample @c command:46FCFD3,with_input:3406FC1 +$ ledger -f expr.dat --format "%(account) %(code)\n" reg assets +@end smallexample +@smallexample @c output:46FCFD3 +Assets:Cash C0D3 +@end smallexample +@end defvar + +@defvar commodity +Return the commodity of the posting amount. +@end defvar +@smallexample @c command:2CD27D7,with_input:3406FC1 +$ ledger -f expr.dat --format "%(account) %(commodity)\n" reg +@end smallexample +@smallexample @c output:2CD27D7 +Assets:Cash ¤ +Expenses:Office Supplies ¤ +@end smallexample + +@defvar date +Return the date of the posting. +@end defvar +@smallexample @c command:67EBA45,with_input:3406FC1 +$ ledger -f expr.dat --format "%(date) %(account)\n" reg assets +@end smallexample +@smallexample @c output:67EBA45 +2015/01/16 Assets:Cash +@end smallexample + +@defvar display_amount +@defvarx t +@value{FIXME:UNDOCUMENTED} +@end defvar + +@c FIXME +@defvar display_total +@defvarx T +@value{FIXME:UNDOCUMENTED} +@end defvar + +@defun floor value +Return the next integer of @var{value} toward @math{-}infinity. +@smallexample @c command:4FDC7C5,with_input:3406FC1 +$ ledger -f expr.dat --format "%(account) %(floor(amount))\n" reg +@end smallexample +@smallexample @c output:4FDC7C5 +Assets:Cash ¤ -124,00 +Expenses:Office Supplies ¤ 123,00 +@end smallexample +@end defun + +@defun format string +Evaluate @var{string} as format just like the @option{--format} option. +@end defun + +@defun format_date date format +Return the @var{date} as a string using @var{format}. See +@code{strftime (3)} for format string details. +@smallexample @c command:9605B13,with_input:3406FC1 +$ ledger -f expr.dat --format "%(format_date(date, '%A, %B %d. %Y'))\n" reg assets +@end smallexample +@smallexample @c output:9605B13 +Friday, January 16. 2015 +@end smallexample +@end defun + +@defun format_datetime datetime format +Return the @var{datetime} as a string using @var{format}. Refer to +@code{strftime (3)} for format string details. +@end defun + +@defun get_at sequence index +Return the value in @var{sequence} at @var{index}. The first element is @var{index} 0. +@value{InternalUseOnly} +@end defun + +@defun is_seq value +Return true if @var{value} is a sequence. @value{InternalUseOnly} +@end defun + +@defun join value +Replace all newlines in @var{value} with @code{\n}. +@end defun + +@defun justify value first_width latter_width right_justify colorize +Right or left justify the string representing @var{value}. The width +of the field in the first line is given by @var{first_width}. For +subsequent lines the width is given by @var{latter_width}. If +@var{latter_width=-1}, then @var{first_width} is used for all lines. +If @var{right_justify=true} then the field is right justified within +the width of the field. If it is @var{false}, then the field is left +justified and padded to the full width of the field. If +@var{colorize} is true, then ledger will honor color settings. +@smallexample @c command:082FB27,with_input:3406FC1 +$ ledger -f expr.dat --format "»%(justify(account, 30, 30, true))«\n" reg +@end smallexample +@smallexample @c output:082FB27 +» Assets:Cash« +» Expenses:Office Supplies« +@end smallexample +@end defun + +@defun market value datetime +@defunx P +Return the price of @var{value} at @var{datetime}. Note that @var{datetime} +must be surrounded by brackets in order to be parsed correctly, +e.g. @code{[2012/03/23]}. +@end defun + +@defun nail_down +@value{FIXME:UNDOCUMENTED} +@end defun + +@defvar now +@defvarx d +@defvarx m +Return the current datetime. +@end defvar + +@defvar options +A variable that allows access to the values of the given command-line options +using the long option names, e.g. to see whether @option{--daily} or @option{-D} +was given use @code{option.daily}. +@smallexample @c command:C1FC7A7,with_input:3406FC1 +$ ledger -f expr.dat -X $ -D --format "%(options.daily) %(options.exchange)\n" reg assets +@end smallexample +@smallexample @c output:C1FC7A7 +true $ +@end smallexample +@end defvar + +@defun percent value_a value_b +Return the percentage of @var{value_a} in relation to @var{value_b} (used as 100%) +@smallexample @c command:04959BF,with_input:3406FC1 +$ ledger -f expr.dat --format "%(percent(amount, 200))\n" reg +@end smallexample +@smallexample @c output:04959BF +-61.73% +61.73% +@end smallexample +@end defun + +@defun print value +Print @var{value} to stdout. @value{InternalUseOnly} +@end defun + +@defun quantity value +Return the quantity of @var{value} for values that have a per-unit cost. +@end defun + +@defun quoted expression +Surround @var{expression} with double-quotes. If expression contains a double-quote, it will be escaped with a backslash. +@smallexample @c command:EAD8AA7,with_input:3406FC1 +$ ledger -f expr.dat --format "%(quoted(account)) %(quoted(amount))\n" reg +@end smallexample +@smallexample @c output:EAD8AA7 +"Assets:Cash" "¤ -123,45" +"Expenses:Office Supplies" "¤ 123,45" +@end smallexample +@end defun + +@c @defun quoted_rfc4180 expression +@c Surround @var{expression} with double-quotes, compliant with RFC 4180. If expression contains a double-quote, it will be represented with two double-quotes. +@c @smallexample @c command:EAD8AA7,with_input:3406FC1 +@c $ ledger -f expr.dat --format "%(quoted_rfc4180(account)) %(quoted_rfc4180(amount))\n" reg +@c @end smallexample +@c @smallexample @c output:EAD8AA7 +@c "Assets:Cash" "¤ -123,45" +@c "Expenses:Office Supplies" "¤ 123,45" +@c @end smallexample +@c @end defun + +@defun round +@value{FIXME:UNDOCUMENTED} +@end defun + +@defun rounded +@value{FIXME:UNDOCUMENTED} +@end defun + +@defun roundto value n +Return @var{value} rounded to @var{n} digits. Does not affect formatting. +@smallexample @c command:B4DFB9F,with_input:3406FC1 +$ ledger -f expr.dat --format "%(account) %(roundto(amount, 1))\n" reg +@end smallexample +@smallexample @c output:B4DFB9F +Assets:Cash ¤ -123,40 +Expenses:Office Supplies ¤ 123,50 +@end smallexample +@end defun + +@defun scrub value +Clean @var{value} using various transformations such as @code{round}, stripping +value annotations, and more. +@end defun + +@defun should_bold +Return true if expression given to @option{--bold-if} evaluates to true. @value{InternalUseOnly} +@end defun + +@defun strip value +@defunx S +Strip value annotation from @var{value}. +@end defun + +@defun to_amount value +Convert @var{value} to an amount. @value{InternalUseOnly} +@end defun + +@defun to_balance value +Convert @var{value} to a balance. @value{InternalUseOnly} +@end defun + +@defun to_boolean value +Convert @var{value} to a boolean. @value{InternalUseOnly} +@end defun + +@defun to_date value +Convert @var{value} to a date. @value{InternalUseOnly} +@end defun + +@defun to_datetime value +Convert @var{value} to a datetime. @value{InternalUseOnly} +@end defun + +@defun to_int value +@defunx int value +Return the integer value for @var{value}. +@smallexample @c command:0B0CBA1,with_input:3406FC1 +$ ledger -f expr.dat --format "%(1 + to_int('1'))\n%(2,5 + int(2,5))\n" reg assets +@end smallexample +@smallexample @c output:0B0CBA1 +2 +4.5 +@end smallexample +@end defun + +@defun to_mask value +Convert @var{value} to a mask. @value{InternalUseOnly} +@end defun + +@defun to_sequence value +Convert @var{value} to a sequence. @value{InternalUseOnly} +@end defun + +@defun to_string value +@defunx str value +Convert @var{value} to a character string. +@end defun + +@defvar today +Return today's date. +@end defvar +@smallexample @c command:F2FDF4B,with_input:3406FC1 +$ ledger -f expr.dat --now 2015/01/01 --format "%(today)\n" reg assets +@end smallexample +@smallexample @c output:F2FDF4B +2015/01/01 +@end smallexample + +@defun top_amount +@value{FIXME:UNDOCUMENTED} +@end defun + +@defun total_expr +Return the calculated total of the posting according to the @option{--total} +option. +@end defun + +@defun trim value +Trim leading and trailing whitespace from @var{value}. +@smallexample @c command:377BBAB,with_input:3406FC1 +$ ledger -f expr.dat --format "»%(trim(' Trimmed '))«\n" reg assets +@end smallexample +@smallexample @c output:377BBAB +»Trimmed« +@end smallexample +@end defun + +@defun truncatedstring total_len account_len +Truncate @var{string} to @var{total_len} ensuring that each account is at least +@var{account_len} long. +@end defun + +@defun unround +@value{FIXME:UNDOCUMENTED} +@end defun + +@defun unrounded +@value{FIXME:UNDOCUMENTED} +@end defun + +@defun value_date +@value{FIXME:UNDOCUMENTED} +@end defun + + +@node Format Strings, Extending with Python, Value Expressions, Top +@chapter Format Strings + +@menu +* Format String Basics:: +* Format String Structure:: +* Format Expressions:: +* Balance format:: +* Formatting Functions and Codes:: +@end menu + +@node Format String Basics, Format String Structure, Format Strings, Format Strings +@section Format String Basics +@findex --format @var{FORMAT_STRING} +@findex --balance-format @var{FORMAT_STRING} +@findex --budget-format @var{FORMAT_STRING} +@findex --cleared-format @var{FORMAT_STRING} +@findex --csv-format @var{FORMAT_STRING} +@findex --plot-amount-format @var{FORMAT_STRING} +@findex --plot-total-format @var{FORMAT_STRING} +@findex --pricedb-format @var{FORMAT_STRING} +@findex --prices-format @var{FORMAT_STRING} +@findex --register-format @var{FORMAT_STRING} + +Format strings may be used to change the output format of reports. They +are specified by passing a formatting string to the @option{--format +@var{FORMAT_STRING} (-F)} option. Within that string, constructs are +allowed which make it possible to display the various parts of an +account or posting in custom ways. + +There are several additional flags that allow you to define formats +for specific reports. These are useful to define in your configuration +file and will allow you to run ledger reports from the command-line +without having to enter a new format for each command. + +@itemize +@item @option{--balance-format @var{FORMAT_STRING}} +@item @option{--budget-format @var{FORMAT_STRING}} +@item @option{--cleared-format @var{FORMAT_STRING}} +@item @option{--csv-format @var{FORMAT_STRING}} +@item @option{--plot-amount-format @var{FORMAT_STRING}} +@item @option{--plot-total-format @var{FORMAT_STRING}} +@item @option{--pricedb-format @var{FORMAT_STRING}} +@item @option{--prices-format @var{FORMAT_STRING}} +@item @option{--register-format @var{FORMAT_STRING}} +@end itemize + +@node Format String Structure, Format Expressions, Format String Basics, Format Strings +@section Format String Structure + +Within a format string, a substitution is specified using a percent +@samp{%} character. The basic format of all substitutions is: + +@smallexample +%[-][MIN WIDTH][.MAX WIDTH](VALEXPR) +@end smallexample + +If the optional minus sign @samp{-} follows the percent character +@samp{%}, whatever is substituted will be left justified. The default +is right justified. If a minimum width is given next, the substituted +text will be at least that wide, perhaps wider. If a period and +a maximum width is given, the substituted text will never be wider +than this, and will be truncated to fit. Here are some examples: + +@table @code + +@item %-20P +A transaction's payee, left justified and padded to 20 characters wide. + +@item %20P +The same, right justified, at least 20 chars wide. + +@item %.20P +The same, no more than 20 chars wide. + +@end table + +The expression following the format constraints can be a single letter, +or an expression enclosed in parentheses or brackets. + +@node Format Expressions, Balance format, Format String Structure, Format Strings +@section Format Expressions +@findex --amount @var{EXPR} +@findex --total @var{VEXPR} + +For demonstration purposes the journal data from @ref{expr.dat} is used. +The allowable expressions are: + +@table @code + +@item % +Inserts a percent sign. +@smallexample @c command:6F90EFC,with_input:3406FC1 +$ ledger -f expr.dat --format "%%\n" reg assets +@end smallexample +@smallexample @c output:6F90EFC +% +@end smallexample + +@item t +Inserts the results of the value expression specified by +@option{--amount @var{EXPR} (-t)}. If @option{--amount @var{EXPR} (-t)} +was not specified, the current report style's value expression is used. + +@item T +Inserts the results of the value expression specified by @option{--total +@var{VEXPR} (-T)}. If @option{--total @var{VEXPR} (-T)} was not +specified, the current report style's value expression is used. + +@item (EXPR) +Inserts the amount resulting from the value expression given in +parentheses. To insert five times the total value of an account, for +example, one could say @samp{%12(5*O)}. Note: It's important to put the +five first in that expression, so that the commodity doesn't get +stripped from the total. +@smallexample @c command:494256E,with_input:3406FC1 +$ ledger -f expr.dat --format "%12(5*O)\n" reg assets +@end smallexample +@smallexample @c output:494256E + ¤ -617,25 +@end smallexample + +@item [DATEFMT] +Inserts the result of formatting a posting's date with a date format +string, exactly like those supported by @code{strftime (3)}. For +example: @samp{%[%Y/%m/%d %H:%M:%S]}. + +@item S +Insert the path name of the file from which the transaction's data was +read. Only sensible in a @command{register} report. +@c Note: Unable to test this properly since the output depends on +@c where the ledger source tree resides in the filesystem. +@smallexample +$ ledger -f ~/journal.dat --format "%S\n" reg assets +@end smallexample +@smallexample +/home/jwiegley/journal.dat +@end smallexample + +@item B +Inserts the beginning character position of that transaction within the +file. +@smallexample @c command:2B669C9,with_input:3406FC1 +$ ledger -f expr.dat --format "%B\n" reg assets +@end smallexample +@smallexample @c output:2B669C9 +26 +@end smallexample + +@item b +Inserts the beginning line of that transaction within the file. +@smallexample @c command:F6E356F,with_input:3406FC1 +$ ledger -f expr.dat --format "%b\n" reg assets +@end smallexample +@smallexample @c output:F6E356F +2 +@end smallexample + +@item E +Inserts the ending character position of that transaction within the +file. +@smallexample @c command:0E55246,with_input:3406FC1 +$ ledger -f expr.dat --format "%E\n" reg assets +@end smallexample +@smallexample @c output:0E55246 +90 +@end smallexample + +@item e +Inserts the ending line of that transaction within the file. +@smallexample @c command:A26F4C0,with_input:3406FC1 +$ ledger -f expr.dat --format "%e\n" reg assets +@end smallexample +@smallexample @c output:A26F4C0 +3 +@end smallexample + +@item D +Returns the date according to the default format. + +@item d +Returns the date according to the default format. If the transaction +has an effective date, it prints @code{ACTUAL_DATE=EFFECTIVE_DATE}. + +@item X +If a posting has been cleared, this returns a 1, otherwise returns 0. + +@item Y +This is the same as @samp{%X}, except that it only displays a state +character if all of the member postings have the same state. + +@item C +Inserts the transaction code. This is the value specified between +parentheses on the first line of the transaction. +@smallexample @c command:C1CAAF3,with_input:3406FC1 +$ ledger -f expr.dat --format "%C\n" reg assets +@end smallexample +@c Note: The output needs a space character at the end +@c for this test to pass +@smallexample @c output:C1CAAF3 +(C0D3) +@end smallexample + +@item P +Inserts the payee related to a posting. +@smallexample @c command:F41A9BB,with_input:3406FC1 +$ ledger -f expr.dat --format "%P\n" reg assets +@end smallexample +@smallexample @c output:F41A9BB +PiggyBank +@end smallexample + +@c @item a +@c Inserts the optimal short name for an account. This is normally +@c used in balance reports. It prints a parent account's name if that +@c name has not been printed yet, otherwise it just prints the +@c account's name. + +@item A +Inserts the full name of an account. +@smallexample @c command:29A70DD,with_input:3406FC1 +$ ledger -f expr.dat --format "%A\n" reg +@end smallexample +@smallexample @c output:29A70DD +Assets:Cash +Expenses:Office Supplies +@end smallexample + +@c @item W +@c This is the same as @code{%A}, except that it first displays the +@c posting's state @emph{if the transaction's posting states are not +@c all the same}, followed by the full account name. This is offered +@c as a printing optimization, so that combined with @code{%Y}, only +@c the minimum amount of state detail is printed. + +@c @item o +@c Inserts the ``optimized'' form of a posting's amount. This is used +@c by the print report. In some cases, this inserts nothing; in +@c others, it inserts the posting amount and its cost. It's use is +@c not recommended unless you are modifying the print report. + +@item N +Inserts the note associated with a posting, if one exists. +@smallexample @c command:E6DC93A,with_input:3406FC1 +$ ledger -f expr.dat --format "%N\n" reg assets +@end smallexample +@smallexample @c output:E6DC93A + Payee: PiggyBank +@end smallexample + +@item / +The @samp{%/} construct is special. It separates a format string +between what is printed for the first posting of a transaction, and +what is printed for all subsequent postings. If not used, the +same format string is used for all postings. +@smallexample @c command:E80897D,with_input:3406FC1 +$ ledger -f expr.dat --format "%P\n%/%A\n" reg +@end smallexample +@smallexample @c output:E80897D +PiggyBank +Expenses:Office Supplies +@end smallexample + +@end table + +@node Balance format, Formatting Functions and Codes, Format Expressions, Format Strings +@section Balance format +@findex --balance-format @var{FORMAT_STRING} +@findex --format @var{FORMAT_STRING} + +As an example of how flexible the @option{--format @var{FORMAT_STRING}} +strings can be, the default balance format looks like this (the various +functions are described later): + +@smallexample +"%(justify(scrub(display_total), 20, -1, true, color))" +" %(!options.flat ? depth_spacer : \"\")" +"%-(ansify_if(partial_account(options.flat), blue if color))\n%/" +"%$1\n%/" +"--------------------\n" +@end smallexample + +@node Formatting Functions and Codes, , Balance format, Format Strings +@section Formatting Functions and Codes + +@menu +* Field Widths:: +* Colors:: +* Quantities and Calculations:: +* Date Functions:: +* Date and Time Format Codes:: +* Text Formatting:: +* Data File Parsing Information:: +@end menu + +@node Field Widths, Colors, Formatting Functions and Codes, Formatting Functions and Codes +@subsection Field Widths + +The following codes return the width allocated for the specific fields. +The defaults can be changed using the corresponding command-line +options: + +@itemize +@item @code{date_width} +@item @code{payee_width} +@item @code{account_width} +@item @code{amount_width} +@item @code{total_width} +@end itemize + +@node Colors, Quantities and Calculations, Field Widths, Formatting Functions and Codes +@subsection Colors + +The character-based formatting ledger can do is limited to the ANSI +terminal character colors and font highlights in a normal TTY session. + +@multitable @columnfractions .3 .3 .3 +@item @code{red} @tab @code{magenta} @tab @code{bold} +@item @code{green} @tab @code{cyan} @tab @code{underline} +@item @code{yellow} @tab @code{white} @tab @code{blink} +@item @code{blue} @tab @code{black} +@end multitable + +@node Quantities and Calculations, Date Functions, Colors, Formatting Functions and Codes +@subsection Quantities and Calculations + +@table @code +@item amount_expr +@item abs +@item commodity +@item display_amount +@item display_total +@item floor +@item get_at +@item is_seq +@item market +@item percent +@item price +@item quantity +@item rounded +@item truncated +@item total_expr +@item top_amount +@item to_boolean +@item to_int +@item to_amount +@item to_balance +@item unrounded +@end table + +@node Date Functions, Date and Time Format Codes, Quantities and Calculations, Formatting Functions and Codes +@subsection Date Functions +@findex --now @var{DATE} + +The following functions allow you to manipulate and format dates. + +@table @code + +@item date +Return the date of the current transaction. + +@item format_date(date, "FORMAT_STRING") +Format the date using the given format string. + +@item now +Return the current date and time. If the @option{--now @var{DATE}} +option is defined it will return that value. + +@item today +Return the current date. If the @option{--now @var{DATE}} option is +defined it will return that value. + +@item to_datetime +Convert a string to a date-time value. + +@item to_date +Convert a string to date value. + +@item value_date + +@end table + +@menu +* Date and Time Format Codes:: +@end menu + +@node Date and Time Format Codes, Text Formatting, Date Functions, Formatting Functions and Codes +@subsection Date and Time Format Codes + +Date and time format are specified as strings of single letter codes +preceded by percent signs. Any separator, or no separator can be +specified. + +@menu +* Days:: +* Weekdays:: +* Month:: +* Miscellaneous Date Codes:: +@end menu + +@node Days, Weekdays, Date and Time Format Codes, Date and Time Format Codes +@subsubsection Days + +Dates are formed from a combination of day, month and year codes, in +whatever order you prefer: + +@table @code + +@item %Y +Four digit year. + +@item %y +Two digit year. + +@item %m +Two digit month. + +@item %d +Two digit date. + +@end table + +@noindent +So @code{"%Y%m%d"} yields @samp{20111214} which provides a date that +is simple to sort on. + +@node Weekdays, Month, Days, Date and Time Format Codes +@subsubsection Weekdays + +You can have additional weekday information in your date with @samp{%A} +as + +@table @code + +@item %m-%d-%Y %A +yields @samp{02-10-2010 Wednesday}. + +@item %A %m-%d-%Y +yields @samp{Wednesday 02-10-2010}. + +@end table + +@noindent +These are options you can select for weekday + +@table @code + +@item %a +weekday, abbreviated Wed. + +@item %A +weekday, full Wednesday. + +@item %d +day of the month (dd), zero padded up to 10. + +@item %e +day of the month (dd), no leading zero up to 10. + +@item %j +day of year, zero padded 000--366. + +@item %u +day of week starting with Monday (1), i.e. @code{mtwtfss} 3. + +@item %w +day of week starting with Sunday (0), i.e. @code{smtwtfs} 3. + +@end table + +@node Month, Miscellaneous Date Codes, Weekdays, Date and Time Format Codes +@subsubsection Month + +You can have additional month information in your date with @samp{%B} +as + +@table @code + +@item %m-%d-%Y %B +yields @samp{02-10-2010 February}. + +@item %B %m-%d-%Y +yields @samp{February 02-10-2010}. + +@end table + +@noindent +These are options you can select for month + +@table @code + +@item %m +@samp{mm} month as two digits. + +@item %b +Locale’s abbreviated month, for example @samp{02} might be abbreviated +as @samp{Feb}. + +@item %B +Locale’s full month, variable length, e.g. February. + +@end table + +@node Miscellaneous Date Codes, , Month, Date and Time Format Codes +@subsubsection Miscellaneous Date Codes + +Additional date format parameters which can be used: + +@table @code + +@item %U +week number Sunday as first day of week, ranging 01--53. + +@item %W +week number Monday as first day of week, ranging 01--53. + +@item %V +week of the year, ranging 01--53. + +@item %C +century, ranging 00--99. + +@item %D +yields @code{%m/%d/%y} as in @samp{02/10/10}. + +@item %x +locale’s date representation, as @samp{02/10/2010} for the U.S. + +@item %F +yields @code{%Y-%m-%d} as in @samp{2010-02-10}. + +@end table + +@node Text Formatting, Data File Parsing Information, Date and Time Format Codes, Formatting Functions and Codes +@subsection Text Formatting + +The following format functions allow you limited formatting of text: + +@table @code + +@item ansify_if(value, color) +Surrounds the string representing value with ANSI codes to give it +@code{color} on an TTY display. Has no effect if directed to a file. + +@item justify(value, first_width, latter_width, right_justify, colorize) +Right or left justify the string representing @code{value}. The width +of the field in the first line is given by @code{first_width}. For +subsequent lines the width is given by @code{latter_width}. If +@code{latter_width=-1}, then @code{first_width} is use for all lines. +If @code{right_justify=true} then the field is right justify within +the width of the field. If it is @code{false}, then the field is left +justified and padded to the full width of the field. If +@code{colorize} is true, then ledger will honor color settings. + +@item join(STR) +Replaces line feeds in @code{STR} with @samp{\n}. + +@item quoted(STR) +Return @code{STR} surrounded by double quotes, @samp{"STR"}. + +@item strip(value) +Values can have numerous annotations, such as effective dates and lot +prices. @code{strip} removes these annotations. + +@end table + +@node Data File Parsing Information, , Text Formatting, Formatting Functions and Codes +@subsection Data File Parsing Information + +The following format strings provide locational metadata +regarding the coordinates of entries in the source data file(s) that +generated the posting. + +@table @code + +@item filename +the name of the ledger data file from whence the posting came, +abbreviated @samp{S}. + +@item beg_pos +character position in @code{filename} where entry for posting begins, +abbreviated @samp{B}. + +@item end_pos +character position in @code{filename} where entry for posting ends, +abbreviated @samp{E}. + +@item beg_line +line number in @code{filename} where entry for posting begins, +abbreviated @samp{b}. + +@item end_line +line number in @code{filename} where entry for posting ends, +abbreviated @samp{e}. + +@end table + +@node Extending with Python, Ledger for Developers, Format Strings, Top +@chapter Extending with Python + +Python can be used to extend your Ledger experience. But first, +a word must be said about Ledger's data model, so that other things +make sense later. + +@menu +* Basic data traversal:: +* Raw versus Cooked:: +* Queries:: +* Embedded Python:: +* Amounts:: +@end menu + +@node Basic data traversal, Raw versus Cooked, Extending with Python, Extending with Python +@section Basic data traversal + +Every interaction with Ledger happens in the context of a Session. +Even if you don't create a session manually, one is created for you by +the top-level interface functions. The Session is where objects live +like the Commodities that Amounts refer to. + +To make a Session useful, you must read a Journal into it, using the +function `@code{read_journal}`. This reads Ledger data from the given +file, populates a Journal object within the current Session, and +returns a reference to that Journal object. + +Within the Journal live all the Transactions, Postings, and other +objects related to your data. There are also AutomatedTransactions +and PeriodicTransactions, etc. + +Here is how you would traverse all the postings in your data file: + +@smallexample +import ledger + +for xact in ledger.read_journal("sample.dat").xacts(): + for post in xact.posts(): + print "Transferring %s to/from %s" % (post.amount, post.account) +@end smallexample + +@node Raw versus Cooked, Queries, Basic data traversal, Extending with Python +@section Raw versus Cooked + +Ledger data exists in one of two forms: raw and cooked. Raw objects are +what you get from a traversal like the above, and represent exactly what +was seen in the data file. Consider this journal: + +@smallexample @c input:validate += true + (Assets:Cash) $100 + +2012-03-01 KFC + Expenses:Food $100 + Assets:Credit +@end smallexample + +In this case, the @emph{raw} regular transaction in this file is: + +@smallexample @c input:validate +2012-03-01 KFC + Expenses:Food $100 + Assets:Credit +@end smallexample + +While the @emph{cooked} form is: + +@smallexample @c input:validate +2012-03-01 KFC + Expenses:Food $100 + Assets:Credit $-100 + (Assets:Cash) $100 +@end smallexample + +So the easy way to think about raw vs. cooked is that raw is the +unprocessed data, and cooked has had all considerations applied. + +When you traverse a Journal by iterating over its transactions, you are +generally looking at raw data. In order to look at cooked data, you +must generate a report of some kind by querying the journal: + +@smallexample +for post in ledger.read_journal("sample.dat").query("food"): + print "Transferring %s to/from %s" % (post.amount, post.account) +@end smallexample + +The reason why queries iterate over postings instead of transactions is +that queries often return only a ``slice'' of the transactions they +apply to. You can always get at a matching posting's transaction by +looking at its @code{xact} member: + +@smallexample +last_xact = None +for post in ledger.read_journal("sample.dat").query(""): + if post.xact != last_xact: + for post in post.xact.posts(): + print "Transferring %s to/from %s" % (post.amount, + post.account) + last_xact = post.xact +@end smallexample + +This query ends up reporting every cooked posting in the Journal, but +does it transaction-wise. It relies on the fact that an unsorted report +returns postings in the exact order they were parsed from the journal +file. + +@node Queries, Embedded Python, Raw versus Cooked, Extending with Python +@section Queries + +The Journal.query() method accepts every argument you can specify on the +command-line, including @option{--options}. + +Since a query ``cooks'' the journal it applies to, only one query may be +active for that journal at a given time. Once the query object is gone +(after the for loop), then the data reverts back to its raw state. + +@node Embedded Python, Amounts, Queries, Extending with Python +@section Embedded Python + +You can embed Python into your data files using the 'python' directive: + +@smallexample +python + import os + def check_path(path_value): + print "%s => %s" % (str(path_value), os.path.isfile(str(path_value))) + return os.path.isfile(str(path_value)) + +tag PATH + assert check_path(value) + +2012-02-29 KFC + ; PATH: somebogusfile.dat + Expenses:Food $20 + Assets:Cash +@end smallexample + +Any Python functions you define this way become immediately available as +valexpr functions. + +@node Amounts, , Embedded Python, Extending with Python +@section Amounts + +When numbers come from Ledger, like post.amount, the type of the value +is Amount. It can be used just like an ordinary number, except that +addition and subtraction are restricted to amounts with the same +commodity. If you need to create sums of multiple commodities, use +a Balance. For example: + +@smallexample +total = Balance() +for post in ledger.read_journal("sample.dat").query(""): + total += post.amount +print total +@end smallexample + +@node Ledger for Developers, Major Changes from version 2.6, Extending with Python, Top +@chapter Ledger for Developers + +@menu +* Internal Design:: +* Journal File Format for Developers:: +* Developer Commands:: +* Ledger Development Environment:: +@end menu + +@node Internal Design, Journal File Format for Developers, Ledger for Developers, Ledger for Developers +@section Internal Design + +Ledger is developed as a tiered set of functionality, where lower tiers +know nothing about the higher tiers. In fact, multiple libraries are +built during the development the process, and link unit tests to these +libraries, so that it is a link error for a lower tier to violate this +modularity. + +Those tiers are: + +@itemize + +@item Utility code + +There's lots of general utility in Ledger for doing time parsing, using +Boost.Regex, error handling, etc. It's all done in a way that can be +reused in other projects as needed. + +@item Commoditized Amounts (amount_t, commodity_t and friends) + +A numerical abstraction combining multi-precision rational numbers (via +GMP) with commodities. These structures can be manipulated like regular +numbers in either C++ or Python (as Amount objects). + +@item Commodity Pool + +Commodities are all owned by a commodity pool, so that future parsing of +amounts can link to the same commodity and established a consistent +price history and record of formatting details. + +@item Balances + +Adds the concept of multiple amounts with varying commodities. Supports +simple arithmetic, and multiplication and division with non-commoditized +values. + +@item Price history + +Amounts have prices, and these are kept in a data graph which the amount +code itself is only dimly aware of (there's three points of access so an +amount can query its revalued price on a given date). + +@item Values + +Often the higher layers in Ledger don't care if something is an amount +or a balance, they just want to add stuff to it or print it. For this, +I created a type-erasure class, value_t/Value, into which many things +can be stuffed and then operated on. They can contain amounts, +balances, dates, strings, etc. If you try to apply an operation between +two values that makes no sense (like dividing an amount by a balance), +an error occurs at runtime, rather than at compile-time (as would happen +if you actually tried to divide an @code{amount_t} by +a @code{balance_t}). + +This is the core data type for the value expression language. + +@item Value expressions + +The next layer up adds functions and operators around the Value concept. +This lets you apply transformations and tests to Values at runtime +without having to bake it into C++. The set of functions available is +defined by each object type in Ledger (posts, accounts, transactions, +etc.), though the core engine knows nothing about these. At its base, +it only knows how to apply operators to values, and how to pass them to +and receive them from functions. + +@item Query expressions + +Expressions can be onerous to type at the command-line, so there's +a shorthand for reporting called ``query expressions''. These add no +functionality of their own, but are purely translated from the input +string down to the corresponding value expression, for example the +input string @samp{cash} is translated to @samp{(account +=~ /cash/)}. This is a convenience layer. + +@item Format strings + +Format strings let you interpolate value expressions into strings, with +the requirement that any interpolated value have a string +representation. Really all this does is calculate the value expression +in the current report context, call the resulting value's +@code{to_string()} method, and stuffs the result into the output string. +It also provides printf-like behavior, such as min/max width, right/left +justification, etc. + +@item Journal items + +Next is a base type shared by anything that can appear in a journal: an +item_t. It contains details common to all such parsed entities, like +what file and line it was found on, etc. + +@item Journal posts + +The most numerous object found in a Journal, postings are a type of item +that contain an account, an amount, a cost, and metadata. There are +some other complications, like the account can be marked virtual, the +amount could be an expression, etc. + +@item Journal transactions + +Postings are owned by transactions, always. This subclass of @code{item_t} +knows about the date, the payee, etc. If a date or metadata tag is +requested from a posting and it doesn't have that information, the +transaction is queried to see if it can provide it. + +@item Journal accounts + +Postings are also shared by accounts, though the actual memory is +managed by the transaction. Each account knows all the postings within +it, but contains relatively little information of its own. + +@item The Journal object + +Finally, all transactions with their postings, and all accounts, are +owned by a @code{journal_t} object. This is the go-to object for +querying and reporting on your data. + +@item Textual journal parser + +There is a textual parser, wholly contained in @file{textual.cc}, which +knows how to parse text into journal objects, which then get +``finalized'' and added to the journal. Finalization is the step that +enforces the double-entry guarantee. + +@item Iterators + +Every journal object is ``iterable'', and these iterators are defined in +@file{iterators.h} and @file{iterators.cc}. This iteration logic is kept out of the +basic journal objects themselves for the sake of modularity. + +@item Comparators + +Another abstraction isolated to its own layer, this class encapsulating +the comparison of journal objects, based on whatever value expression +the user passed to @option{--sort @var{VEXPR}}. + +@item Temporaries + +Many reports bring pseudo-journal objects into existence, like postings +which report totals in a @samp{Total} account. These objects are +created and managed by a @code{temporaries_t} object, which gets used in +many places by the reporting filters. + +@item Option handling + +There is an option handling subsystem used by many of the layers further +down. It makes it relatively easy for me to add new options, and to +have those option settings immediately accessible to value expressions. + +@item Session objects + +Every journal object is owned by a session, with the session providing +support for that object. In GUI terms, this is the Controller object +for the journal Data object, where every document window would be +a separate session. They are all owned by the global scope. + +@item Report objects + +Every time you create any report output, a report object is created to +determine what you want to see. In the Ledger REPL, a new report object +is created every time a command is executed. In CLI mode, only one +report object ever comes into being, as Ledger immediately exits after +displaying the results. + +@item Reporting filters + +The way Ledger generates data is this: it asks the session for the +current journal, and then creates an iterator applied to that journal. +The kind of iterator depends on the type of report. + +This iterator is then walked, and every object yielded from the iterator +is passed to an ``item handler'', whose type is directly related to the +type of the iterator. + +There are many, many item handlers, which can be chained together. Each +one receives an item (post, account, xact, etc.), performs some action +on it, and then passes it down to the next handler in the chain. There +are filters which compute the running totals; that queue and sort all +the input items before playing them back out in a new order; that filter +out items which fail to match a predicate, etc. Almost every reporting +feature in Ledger is related to one or more filters. Looking at +@file{filters.h}, there are over 25 of them defined currently. + +@item The filter chain + +How filters get wired up, and in what order, is a complex process based +on all the various options specified by the user. This is the job of +the chain logic, found entirely in @file{chain.cc}. It took a really +long time to get this logic exactly right, which is why I haven't +exposed this layer to the Python bridge yet. + +@item Output modules + +Although filters are great and all, in the end you want to see stuff. +This is the job of special ``leaf'' filters called output modules. They +are implemented just like a regular filter, but they don't have +a ``next'' filter to pass the data on down to. Instead, they are the +end of the line and must do something with the item that results in the +user seeing something on their screen or in a file. + +@item Select queries + +Select queries know a lot about everything, even though they implement +their logic by implementing the user's query in terms of all the other +features thus presented. Select queries have no functionality of their +own, they are simple a shorthand to provide access to much of Ledger's +functionality via a cleaner, more consistent syntax. + +@item The Global Scope + +There is a master object which owns every other objects, and this is +Ledger's global scope. It creates the other objects, provides REPL +behavior for the command-line utility, etc. In GUI terms, this is the +Application object. + +@item The Main Driver + +This creates the global scope object, performs error reporting, and +handles command-line options which must precede even the creation of the +global scope, such as @option{--debug @var{CODE}}. + +@end itemize + +And that's Ledger in a nutshell. All the rest are details, such as +which value expressions each journal item exposes, how many filters +currently exist, which options the report and session scopes define, +etc. + +@node Journal File Format for Developers, Developer Commands, Internal Design, Ledger for Developers +@section Journal File Format for Developers + +This chapter offers a complete description of the journal data format, +suitable for implementers in other languages to follow. For users, the +chapter on keeping a journal is less extensive, but more typical of +common usage (@pxref{Keeping a Journal}). + +Data is collected in the form of @dfn{transactions} which occur in one +or more @dfn{journal files}. Each transaction, in turn, is made up of +one or more @dfn{postings}, which describe how @dfn{amounts} flow from +one @dfn{account} to another. Here is an example of the simplest of +journal files: + +@smallexample @c input:validate +2010/05/31 Just an example + Expenses:Some:Account $100.00 + Income:Another:Account +@end smallexample + +In this example, there is a transaction date, a payee, or description of +the transaction, and two postings. The postings show movement of one +hundred dollars from an account within the Income hierarchy, to the +specified expense account. The name and meaning of these accounts is +arbitrary, with no preferences implied, although you will find it useful +to follow standard accounting practices (@pxref{Principles of Accounting +with Ledger}). + +Since an amount is missing from the second posting, it is assumed to be +the inverse of the first. This guarantees the cardinal rule of +double-entry accounting: the sum of every transaction must balance to +zero, or it is in error. Whenever Ledger encounters a @dfn{null +posting} in a transaction, it uses it to balance the remainder. + +It is also typical, though not enforced, to think of the first posting +as the destination, and the final as the source. Thus, the amount of +the first posting is typically positive. Consider: + +@smallexample @c input:validate +2010/05/31 An income transaction + Assets:Checking $1,000.00 + Income:Salary + +2010/05/31 An expense transaction + Expenses:Dining $100.00 + Assets:Checking +@end smallexample + +@menu +* Comments and meta-data:: +* Specifying Amounts:: +* Posting costs:: +* Primary commodities:: +@end menu + +@node Comments and meta-data, Specifying Amounts, Journal File Format for Developers, Journal File Format for Developers +@subsection Comments and meta-data + +Comments are generally started using a @samp{;}. However, in order to +increase compatibility with other text manipulation programs and methods +three additional comment characters are valid if used at the beginning +of a line: @samp{#}, @samp{|}, and @samp{*}. + +@node Specifying Amounts, Posting costs, Comments and meta-data, Journal File Format for Developers +@subsection Specifying Amounts +@cindex amounts + +The heart of a journal is the amounts it records, and this fact is +reflected in the diversity of amount expressions allowed. All of them +are covered here, though it must be said that sometimes, there are +multiple ways to achieve a desired result. + +@emph{Note:} It is important to note that there must be at least two +spaces between the end of the account and the beginning of the amount +(including a commodity designator). + +@menu +* Integer Amounts:: +* Commoditized Amounts:: +@end menu + +@node Integer Amounts, Commoditized Amounts, Specifying Amounts, Specifying Amounts +@subsubsection Integer Amounts + +In the simplest form, bare decimal numbers are accepted: + +@smallexample @c input:validate +2010/05/31 An income transaction + Assets:Checking 1000.00 + Income:Salary +@end smallexample + +@cindex uncommoditized amounts +Such amounts may only use an optional period for a decimal point. These +are referred to as @dfn{integer amounts} or @dfn{uncommoditized +amounts}. In most ways they are similar to @dfn{commoditized amounts}, +but for one significant difference: They always display in reports with +@dfn{full precision}. More on this in a moment. For now, a word must +be said about how Ledger stores numbers. + +Every number parsed by Ledger is stored internally as an +infinite-precision rational value. Floating-point math is never used, +as it cannot be trusted to maintain precision of values. So, in the +case of @samp{1000.00} above, the internal value is @samp{100000/100}. + +While rational numbers are great at not losing precision, the question +arises: How should they be displayed? A number like @samp{100000/100} +is no problem, since it represents a clean decimal fraction. But what +about when the number @samp{1/1} is divided by three? How should one +print @samp{1/3}, an infinitely repeating decimal? + +Ledger gets around this problem by rendering rationals into decimal at +the last possible moment, and only for display. As such, some +rounding must, at times, occur. If this rounding would affect the +calculation of a running total, special accommodation postings are +generated to make you aware it has happened. In practice, it happens +rarely, but even then it does not reflect adjustment of the +@emph{internal amount}, only the displayed amount. + +What has still not been answered is how Ledger rounds values. Should +@samp{1/3} be printed as @samp{0.33} or @samp{0.33333}? For +commoditized amounts, the number of decimal places is decided by +observing how each commodity is used; but in the case of integer +amounts, an arbitrary factor must be chosen. Initially, this factor +is six. Thus, @samp{1/3} is printed back as @samp{0.333333}. +Further, this rounding factor becomes associated with each particular +value, and is carried through mathematical operations. For example, +if that particular number were multiplied by itself, the decimal +precision of the result would be twelve. Addition and subtraction do +not affect precision. + +Since each integer amount retains its own display precision, this is +called @dfn{full precision}, as opposed to commoditized amounts, which +always look to their commodity to know what precision they should +round to, and so use @dfn{commodity precision}. + +@node Commoditized Amounts, , Integer Amounts, Specifying Amounts +@subsubsection Commoditized Amounts + +A @dfn{commoditized amount} is an integer amount which has an +associated commodity. This commodity can appear before or after the +amount, and may or may not be separated from it by a space. Most +characters are allowed in a commodity name, except for the following: + +@itemize @bullet +@item Any kind of white-space +@item Numerical digits +@item Punctuation: @code{.,;:?!} +@item Mathematical and logical operators: @code{-+*/^&|=} +@item Bracketing characters: @code{<>[]()}@{@} +@item The at symbol: @code{@@} +@end itemize + +And yet, any of these may appear in a commodity name if it is +surrounded by double quotes, for example: + +@smallexample +100 "EUN+133" +@end smallexample + +If a @dfn{quoted commodity} is found, it is displayed in quotes as +well, to avoid any confusion as to which part is the amount, and which +part is the commodity. + +Another feature of commoditized amounts is that they are reported back +in the same form as parsed. If you specify dollar amounts using +@samp{$100}, they will print the same; likewise with @samp{100 $} or +@samp{$100.000}. You may even use decimal commas, such as +@samp{$100,00}, or thousand-marks, as in @samp{$10,000.00}. + +These display characteristics become associated with the commodity, +with the result being that all amounts of the same commodity are +reported consistently. Where this is most noticeable is the +@dfn{display precision}, which is determined by the most precise value +seen for a given commodity---in most cases. + +Ledger makes a distinction between @dfn{observed amounts} and +unobserved amounts. An observed amount is critiqued by Ledger to +determine how amounts using that commodity should be displayed; +unobserved amounts are significant in their value only---no matter how +they are specified, it does not change how other amounts in that +commodity will be displayed. + +An example of this is found in cost expressions, covered next. + +@node Posting costs, Primary commodities, Specifying Amounts, Journal File Format for Developers +@subsection Posting costs + +You have seen how to specify either a commoditized or an integer +amount for a posting. But what if the amount you paid for something +was in one commodity, and the amount received was another? There are +two main ways to express this: + +@smallexample @c input:validate +2010/05/31 Farmer's Market + Assets:My Larder 100 apples + Assets:Checking -$20.00 +@end smallexample + +In this example, you have paid twenty dollars for one hundred apples. +The cost to you is twenty cents per apple, and Ledger calculates this +implied cost for you. You can also make the cost explicit using a +@dfn{cost amount}: + +@smallexample @c input:validate +2010/05/31 Farmer's Market + Assets:My Larder 100 apples @@ $0.200000 + Assets:Checking +@end smallexample + +Here the @dfn{per-unit cost} is given explicitly in the form of a cost +amount; and since cost amounts are @emph{unobserved}, the use of six +decimal places has no effect on how dollar amounts are displayed in +the final report. You can also specify the @dfn{total cost}: + +@smallexample @c input:validate +2010/05/31 Farmer's Market + Assets:My Larder 100 apples @@@@ $20 + Assets:Checking +@end smallexample + +These three forms have identical meaning. In most cases the first is +preferred, but the second two are necessary when more than two +postings are involved: + +@smallexample @c input:validate +2010/05/31 Farmer's Market + Assets:My Larder 100 apples @@ $0.200000 + Assets:My Larder 100 pineapples @@ $0.33 + Assets:My Larder 100 "crab apples" @@ $0.04 + Assets:Checking +@end smallexample + +Here the implied cost is @samp{$57.00}, which is entered into the null +posting automatically so that the transaction balances. + +@node Primary commodities, , Posting costs, Journal File Format for Developers +@subsection Primary commodities +@findex --market +@findex --basis + +In every transaction involving more than one commodity, there is +always one which is the @dfn{primary commodity}. This commodity +should be thought of as the exchange commodity, or the commodity used +to buy and sell units of the other commodity. In the fruit examples +above, dollars are the primary commodity. This is decided by Ledger +based on the placement of the commodity in the transaction: + +@smallexample @c input:validate +2010/05/31 Sample Transaction + Expenses 100 secondary + Assets -50 primary + +2010/05/31 Sample Transaction + Expenses 100 secondary @@ 0.5 primary + Assets + +2010/05/31 Sample Transaction + Expenses 100 secondary @@@@ 50 primary + Assets +@end smallexample + +The only case where knowledge of primary versus secondary comes into +play is in reports that use the @option{--market (-V)} or +@option{--basis (-B)} options. With these, only primary commodities are +shown. + +If a transaction uses only one commodity, this commodity is also +considered a primary. In fact, when Ledger goes about ensuring that +all transactions balance to zero, it only ever asks this of primary +commodities. + +@node Developer Commands, Ledger Development Environment, Journal File Format for Developers, Ledger for Developers +@section Developer Commands + +@menu +* @command{echo}:: +* @command{reload}:: +* @command{source}:: +* Debug Options:: +* Pre-Commands:: +@end menu + +@node @command{echo}, @command{reload}, Developer Commands, Developer Commands +@subsection @command{echo} +@findex echo + +This command simply echoes its argument back to the output. + +@node @command{reload}, @command{source}, @command{echo}, Developer Commands +@subsection @command{reload} +@findex reload + +Forces ledger to reload any journal files. This function exists to +support external programs controlling a running ledger process and does +nothing for a command-line user. + +@node @command{source}, Debug Options, @command{reload}, Developer Commands +@subsection @command{source} +@findex source + +The @command{source} command takes a journal file as an argument and +parses it checking for errors; no other reports are generated, and no +other arguments are necessary. Ledger will return success if no errors +are found. + +@node Debug Options, Pre-Commands, @command{source}, Developer Commands +@subsection Debug Options + +These options are primarily for Ledger developers, but may be of some +use to a user trying something new. + +@ftable @option + +@item --args-only +Ignore init files and environment variables for the ledger run. + +@item --debug @var{CODE} +If Ledger has been built with debug options this will provide extra +data during the run. The following are the available @var{CODES} to +debug: + +@multitable @columnfractions .32 .43 .27 +@item @code{account.display} @tab @code{draft.xact} @tab @code{option.names} +@item @code{account.sorted} @tab @code{expr.calc} @tab @code{org.next_amount} +@item @code{amount.commodities} @tab @code{expr.compile} @tab @code{org.next_total} +@item @code{amount.convert} @tab @code{expr.merged.compile} @tab @code{parser.error} +@item @code{amount.is_zero} @tab @code{filters.changed_value} @tab @code{pool.commodities} +@item @code{amount.parse} @tab @code{filters.changed_value.rounding} @tab @code{post.assign} +@item @code{amount.price} @tab @code{filters.collapse} @tab @code{python.init} +@item @code{amount.refs} @tab @code{filters.forecast} @tab @code{python.interp} +@item @code{amount.roundto} @tab @code{filters.interval} @tab @code{query.mask} +@item @code{amount.truncate} @tab @code{filters.revalued} @tab @code{report.predicate} +@item @code{amount.unround} @tab @code{format.abbrev} @tab @code{scope.search} +@item @code{annotate.less} @tab @code{format.expr} @tab @code{scope.symbols} +@item @code{archive.journal} @tab @code{generate.post} @tab @code{select.parse} +@item @code{auto.columns} @tab @code{generate.post.string} @tab @code{textual.include} +@item @code{budget.generate} @tab @code{history.find} @tab @code{textual.parse} +@item @code{commodity.annotated.strip} @tab @code{history.map} @tab @code{timelog} +@item @code{commodity.annotations} @tab @code{item.meta} @tab @code{times.epoch} +@item @code{commodity.compare} @tab @code{ledger.read} @tab @code{times.interval} +@item @code{commodity.download} @tab @code{ledger.validate} @tab @code{times.parse} +@item @code{commodity.exchange} @tab @code{lookup} @tab @code{value.sort} +@item @code{commodity.price.find} @tab @code{lookup.account} @tab @code{value.storage.refcount} +@item @code{commodity.prices.add} @tab @code{mask.match} @tab @code{xact.extend} +@item @code{commodity.prices.find} @tab @code{memory.debug} @tab @code{xact.extend.cleared} +@item @code{csv.mappings} @tab @code{op.memory} @tab @code{xact.extend.fail} +@item @code{csv.parse} @tab @code{option.args} @tab @code{xact.finalize} +@end multitable +@ + +@item --trace @var{INT} +Enable tracing. The @var{INT} specifies the level of trace desired: + +@multitable @columnfractions .3 .7 +@item @code{LOG_OFF} @tab 0 +@item @code{LOG_CRIT} @tab 1 +@item @code{LOG_FATAL} @tab 2 +@item @code{LOG_ASSERT} @tab 3 +@item @code{LOG_ERROR} @tab 4 +@item @code{LOG_VERIFY} @tab 5 +@item @code{LOG_WARN} @tab 6 +@item @code{LOG_INFO} @tab 7 +@item @code{LOG_EXCEPT} @tab 8 +@item @code{LOG_DEBUG} @tab 9 +@item @code{LOG_TRACE} @tab 10 +@item @code{LOG_ALL} @tab 11 +@end multitable +@ + +@item --verbose +@itemx -v +Print detailed information on the execution of Ledger. + +@item --verify +Enable additional assertions during run-time. This causes a significant +slowdown. When combined with @option{--debug @var{CODE}} ledger will +produce memory trace information. + +@item --verify-memory +Verify that every constructed object is properly destructed. This is for +debugging purposes only. + +@item --version +Print version information and exit. + +@end ftable + +@node Pre-Commands, , Debug Options, Developer Commands +@subsection Pre-Commands +@cindex pre-commands + +Pre-commands are useful when you aren't sure how a command or option +will work. The difference between a pre-command and a regular command +is that pre-commands ignore the journal data file completely, nor is +the user's init file read. + +@ftable @command + +@item eval @var{VEXPR} +Evaluate the given value expression against the model transaction. + +@item format @var{FORMAT_STRING} +Print details of how ledger uses the given formatting description and +apply it against a model transaction. + +@item generate +Randomly generates syntactically valid Ledger data from a seed. Used +by the @samp{GenerateTests} harness for development testing. + +@item parse @var{VEXPR} +@itemx expr @var{VEXPR} +Print details of how ledger uses the given value expression description +and apply it against a model transaction. + +@item period @var{PERIOD_EXPRESSION} +Evaluate the given period and report how Ledger interprets it: + +@smallexample @c command:51F6A2C +$ ledger period "this year" --now 2011-01-01 +@end smallexample +@smallexample @c output:51F6A2C +--- Period expression tokens --- +TOK_THIS: this +TOK_YEAR: year +END_REACHED: <EOF> + +--- Before stabilization --- + range: in year 2011 + +--- After stabilization --- + range: in year 2011 + start: 11-Jan-01 + finish: 12-Jan-01 + +--- Sample dates in range (max. 20) --- + 1: 11-Jan-01 +@end smallexample + +@item query +@itemx args +Evaluate the given arguments and report how Ledger interprets it against +the following model transaction: + +@smallexample @c command:validate +$ ledger query "/Book/" +@end smallexample +@smallexample +--- Input arguments --- +("/Book/") + +--- Context is first posting of the following transaction --- +2004/05/27 Book Store + ; This note applies to all postings. :SecondTag: + Expenses:Books 20 BOOK @@ $10 + ; Metadata: Some Value + ; Typed:: $100 + $200 + ; :ExampleTag: + ; Here follows a note describing the posting. + Liabilities:MasterCard $-200.00 + +--- Input expression --- +(account =~ /Book/) + +--- Text as parsed --- +(account =~ /Book/) + +--- Expression tree --- +0x7fd639c0da40 O_MATCH (1) +0x7fd639c10170 IDENT: account (1) +0x7fd639c10780 VALUE: /Book/ (1) + +--- Compiled tree --- +0x7fd639c10520 O_MATCH (1) +0x7fd639c0d6c0 IDENT: account (1) +0x7fd639c0d680 FUNCTION (1) +0x7fd639c10780 VALUE: /Book/ (1) + +--- Calculated value --- +true +@end smallexample + +@item script +@value{FIXME:UNDOCUMENTED} + +@item template +Shows the insertion template that the @command{xact} sub-command +generates. This is a debugging command. + +@end ftable + +@node Ledger Development Environment, , Developer Commands, Ledger for Developers +@section Ledger Development Environment + +@menu +* @file{acprep} build configuration tool:: +* Testing Framework:: +@end menu + +@node @file{acprep} build configuration tool, Testing Framework, Ledger Development Environment, Ledger Development Environment +@subsection @file{acprep} build configuration tool + +@node Testing Framework, , @file{acprep} build configuration tool, Ledger Development Environment +@subsection Testing Framework + +Ledger source ships with a fairly complete set of tests to verify that +all is well, and no old errors have resurfaced. Tests are run +individually with @file{ctest}. All tests can be run using @code{make +check} or @code{ninja check} depending on which build tool you prefer. + +Once built, the ledger executable resides under the @file{build} +subdirectory in the source tree. Tests are built and stored in the +@file{test} subdirectory for the build. For example, +@file{~/ledger/build/ledger/opt/test}. + +@menu +* Running Tests:: +* Writing Tests:: +@end menu + +@node Running Tests, Writing Tests, Testing Framework, Testing Framework +@subsubsection Running Tests + +The complete test suite can be run from the build directory using the +check option for the build tool you use. For example, @code{make +check}. The entire test suit lasts around a minute for the optimized +built and many times longer for the debug version. While developing +and debugging, running individual tests can save a great deal of time. + +Individual tests can be run from the @file{test} subdirectory of the +build location. To execute a single test use @code{ctest -V -R regex}, +where the regex matches the name of the test you want to build. + +There are nearly 300 tests stored under the @file{test} subdirectory +in the main source distribution. They are broken into two broad +categories, baseline and regression. To run the @file{5FBF2ED8} test, +for example, issue @code{ctest -V -R "5FB"}. + +@node Writing Tests, , Running Tests, Testing Framework +@subsubsection Writing Tests + +To write a new test first decide to which broad category the test belongs: +baseline or regression. Depending on the category tests are named differently +baseline tests are prefixed with their type, e.g. @samp{cmd} +(@pxref{Baseline Test Types} for valid types), whereas regressions are either +named after the bug id, e.g. @samp{1234.test} or uuid @samp{91416D62.test}. +In case several test files belong to the same bug number the files by appending +@code{_X} where @samp{X} is the number of the test, e.g. @samp{1234_1.test}, +@samp{1234_2.test}. + +Baseline Test Types: +@anchor{Baseline Test Types} +@table @code +@item cmd +Ledger commands like @command{register} or @command{balance} +@item dir +Ledger directives like @code{account} or @code{alias} +@item feat +Ledger features such as balance assertions in journal file +@item opt +Ledger options such as @option{--period} or @option{--format} +@end table + +A ledger test file contains three sections: + +@enumerate +@item the journal data used for the test, this can be empty in certain +scenarios +@item the ledger command-line options used for the test +@item the expected output +@end enumerate + +Ledger has a special command directive for tests, everything between +@code{test} and @code{end test} is treated like a comment, so every +Ledger test is automatically a valid Ledger file. +The test scripts take the remainder of the @code{test} line and use +it as command-line arguments for ledger, the text enclosed in @code{test} +and @code{end test} is expected output, for example: + +@smallexample @c input:validate +; This is the journal data +year 2014 +12/24 (C0d3) Santa Claus + Assets:Bank ¤ -150,00 + Expenses:Presents + +; The following line specifies the ledger command-line options for this test and +; everything between the next line and `end test` specifies the expected output +test reg --payee=code +14-Dec-24 C0d3 Assets:Bank ¤ -150,00 ¤ -150,00 +14-Dec-24 C0d3 Expenses:Presents ¤ 150,00 0 +end test +@end smallexample + +When it is necessary to test for errors printed to @code{stderr} redirect +the test output by adding @code{->} to the @code{test} line and match the +expected error text in an @code{__ERROR__} section: + +@smallexample +2014/01/01 * Acme Corporation + Assets:Bank:Checking ¤ 1.000,00 + [Fund:Vacation] ¤ 300,00 + [Fund:Studies] ¤ 600,00 + Income:Salary ¤ -2.000,00 + +test reg -> +__ERROR__ +While parsing file "$FILE", line 5: +While balancing transaction from "$FILE", lines 1-5: +> 2014/01/01 * Acme Corporation +> Assets:Bank:Checking ¤ 1.000,00 +> [Fund:Vacation] ¤ 300,00 +> [Fund:Studies] ¤ 600,00 +> Income:Salary ¤ -2.000,00 +Unbalanced remainder is: + ¤ -100,00 +Amount to balance against: + ¤ 1.900,00 +Error: Transaction does not balance +end test +@end smallexample + +A special @code{$FILE} variable can be used to match the journal filename +used during the test. + +To add new tests to the test suite use the rebuild_cache option for the +build tool you use, for example @code{make rebuild_cache}, now the +new tests can be run as documented in @ref{Running Tests}. + +@node Major Changes from version 2.6, Example Journal File, Ledger for Developers, Top +@chapter Major Changes from version 2.6 + +The following have been removed from Ledger 3.0: + +@itemize + +@item +OFX support. + +@item +GnuCash file import. + +@item +The option @option{--performance (-g)}. + +@item +The balance report now defaults to showing all relevant accounts. This +is the opposite of 2.x. That is, @command{bal} in 3.0 does what @samp{-s +bal} did in 2.x. To see 2.6 behavior, use @option{--collapse (-n)} +option in 3.0, like @samp{bal -n}. The @option{--subtotal (-s)} option +no longer has any effect on balance reports. + +@end itemize + +@noindent +The following are deprecated in Ledger 3.0: + +@itemize + +@item +Single character value expressions are deprecated and should be changed +to the new value expressions available in 3.0 + +@item +The following environment variables have been renamed in Ledger 3.0: +@table @env +@item LEDGER +is now @env{LEDGER_FILE}, +@item LEDGER_INIT +is now @env{LEDGER_INIT_FILE}, +@item PRICE_HIST +is now @env{LEDGER_PRICE_DB}, +@item PRICE_EXP +is now @env{LEDGER_PRICE_EXP}. +@end table + +@end itemize + +@node Example Journal File, Miscellaneous Notes, Major Changes from version 2.6, Top +@appendix Example Journal File + +The following journal file is included with the source distribution of +ledger. It is called @file{drewr3.dat} and exhibits many ledger +features, include automatic and virtual transactions, + +@smallexample @c input:validate +; -*- ledger -*- + += /^Income/ + (Liabilities:Tithe) 0.12 + +;~ Monthly +; Assets:Checking $500.00 +; Income:Salary + +;~ Monthly +; Expenses:Food $100 +; Assets + +2010/12/01 * Checking balance + Assets:Checking $1,000.00 + Equity:Opening Balances + +2010/12/20 * Organic Co-op + Expenses:Food:Groceries $ 37.50 ; [=2011/01/01] + Expenses:Food:Groceries $ 37.50 ; [=2011/02/01] + Expenses:Food:Groceries $ 37.50 ; [=2011/03/01] + Expenses:Food:Groceries $ 37.50 ; [=2011/04/01] + Expenses:Food:Groceries $ 37.50 ; [=2011/05/01] + Expenses:Food:Groceries $ 37.50 ; [=2011/06/01] + Assets:Checking $ -225.00 + +2010/12/28=2011/01/01 Acme Mortgage + Liabilities:Mortgage:Principal $ 200.00 + Expenses:Interest:Mortgage $ 500.00 + Expenses:Escrow $ 300.00 + Assets:Checking $ -1000.00 + +2011/01/02 Grocery Store + Expenses:Food:Groceries $ 65.00 + Assets:Checking + +2011/01/05 Employer + Assets:Checking $ 2000.00 + Income:Salary + +2011/01/14 Bank + ; Regular monthly savings transfer + Assets:Savings $ 300.00 + Assets:Checking + +2011/01/19 Grocery Store + Expenses:Food:Groceries $ 44.00 ; hastag: not block + Assets:Checking + +2011/01/25 Bank + ; Transfer to cover car purchase + Assets:Checking $ 5,500.00 + Assets:Savings + ; :nobudget: + +apply tag hastag: true +apply tag nestedtag: true +2011/01/25 Tom's Used Cars + Expenses:Auto $ 5,500.00 + ; :nobudget: + Assets:Checking + +2011/01/27 Book Store + Expenses:Books $20.00 + Liabilities:MasterCard +end tag +2011/12/01 Sale + Assets:Checking:Business $ 30.00 + Income:Sales +end tag +@end smallexample + +@node Miscellaneous Notes, Concepts Index, Example Journal File, Top +@appendix Miscellaneous Notes + +Various notes from the discussion list that I haven't incorporated in +to the main body of the documentation. + +@menu +* Cookbook:: +@end menu + +@node Cookbook, , Miscellaneous Notes, Miscellaneous Notes +@section Cookbook + +@menu +* Invoking Ledger:: +* Ledger Files:: +@end menu + +@node Invoking Ledger, Ledger Files, Cookbook, Cookbook +@subsection Invoking Ledger + + +@smallexample @c command:validate +$ ledger --group-by "tag('trip')" bal +@end smallexample +@c FIXME: The following example fails to validate due to: +@c While applying is_realzero to : +@c Error: Cannot determine if an uninitialized value is really zero +@c @smallexample @c command:validate +@c $ ledger reg --sort "tag('foo')" %foo +@c @end smallexample +@smallexample @c command:validate +$ ledger cleared VWCU NFCU Tithe Misentry +@end smallexample +@smallexample @c command:validate +$ ledger register Joint --uncleared +@end smallexample +@smallexample @c command:validate +$ ledger register Checking --sort d -d 'd>[2011/04/01]' until 2011/05/25 +@end smallexample + +@node Ledger Files, , Invoking Ledger, Cookbook +@subsection Ledger Files + +@smallexample @c input:validate += /^Income:Taxable/ + (Liabilities:Tithe Owed) -0.1 += /Noah/ + (Liabilities:Tithe Owed) -0.1 += /Jonah/ + (Liabilities:Tithe Owed) -0.1 += /Tithe/ + (Liabilities:Tithe Owed) -1.0 +@end smallexample + +@node Concepts Index, Commands & Options Index, Miscellaneous Notes, Top +@unnumbered Concepts Index + +@printindex cp + +@node Commands & Options Index, , Concepts Index, Top +@unnumbered Commands & Options Index + +@printindex fn + +@bye diff --git a/doc/version.texi.in b/doc/version.texi.in new file mode 100644 index 00000000..c477db70 --- /dev/null +++ b/doc/version.texi.in @@ -0,0 +1,8 @@ +@set Ledger_VERSION_MAJOR @Ledger_VERSION_MAJOR@ +@set Ledger_VERSION_MINOR @Ledger_VERSION_MINOR@ +@set Ledger_VERSION_PATCH @Ledger_VERSION_PATCH@ +@set Ledger_VERSION_PRERELEASE @Ledger_VERSION_PRERELEASE@ +@set Ledger_VERSION_DATE @Ledger_VERSION_DATE@ + +@set VERSION @value{Ledger_VERSION_MAJOR}.@value{Ledger_VERSION_MINOR}.@value{Ledger_VERSION_PATCH}@value{Ledger_VERSION_PRERELEASE} + diff --git a/lib/Makefile b/lib/Makefile new file mode 100644 index 00000000..ddbe0585 --- /dev/null +++ b/lib/Makefile @@ -0,0 +1,56 @@ +# Uncomment these if you are on OS X and want to build universal libraries. +# This is only important if you intend to produce a Ledger binary for +# installation. + +STOW_ROOT = /usr/local/Cellar/boost +PRODUCTS = $(HOME)/Products + +GCC_VERSION = 4.7 +BOOST_VERSION = 1_52_0 + +CC = gcc-mp-$(GCC_VERSION) +ifeq ($(CC),clang) +CXX = clang++ +LD = llvm-ld +DIR_SUFFIX = clang +OPTJ = +else +CXX = g++-mp-$(GCC_VERSION) +LD = gcc-mp-$(GCC_VERSION) +DIR_SUFFIX = gcc$(subst .,,$(GCC_VERSION)) +OPTJ = #-j8 +endif +CFLAGS = $(CPPFLAGS) -g2 -ggdb +LDFLAGS = -g2 -ggdb + +BOOST_SOURCE = boost-release +ifeq ($(GCC_VERSION),4.7) +BOOST_DEFINES = define=_GLIBCXX__PTHREADS=1 +else +BOOST_DEFINES = +endif +ifeq ($(CC),clang) +BOOST_TOOLSET = clang +else +BOOST_TOOLSET = darwin +endif +BOOST_FLAGS = toolset=$(BOOST_TOOLSET) --layout=versioned \ + link=shared threading=single $(BOOST_DEFINES) +BOOST_DIR = boost_$(BOOST_VERSION)-$(DIR_SUFFIX) +BOOST_STOW = $(STOW_ROOT)/$(BOOST_VERSION) +BOOST_BUILD = $(PRODUCTS)/$(BOOST_DIR) + +all: boost-build + +prepare-boost: + perl -i -pe 's/local command = \[ common\.get-invocation-command darwin : g\+\+ : .*/local command = [ common.get-invocation-command darwin : g++ : $(CXX) ] ;/;' $(BOOST_SOURCE)/tools/build/v2/tools/darwin.jam + perl -i -pe 's/flags darwin\.compile OPTIONS : -no-cpp-precomp -gdwarf-2 (-fexceptions )?;/flags darwin\.compile OPTIONS : -gdwarf-2 \1;/;' $(BOOST_SOURCE)/tools/build/v2/tools/darwin.jam + +boost-build: prepare-boost + (cd $(BOOST_SOURCE) && \ + sh bootstrap.sh && \ + ./b2 $(OPTJ) debug release --prefix=$(BOOST_STOW) \ + --build-dir=$(BOOST_BUILD) $(BOOST_FLAGS) install) + +clean: + -rm -fr $(BOOST_STOW) $(BOOST_BUILD) diff --git a/lib/build-gcc.sh b/lib/build-gcc.sh new file mode 100755 index 00000000..af3decb8 --- /dev/null +++ b/lib/build-gcc.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# This build script is for OS X Lion users who have compiled openmpi and +# clang-3.1 from MacPorts. I build my own Boost instead of using MacPorts' +# Boost in order to get better debugging support, and to link with libc++. + +export PATH=$PATH:/opt/local/lib/openmpi/bin + +cat > ~/user-config.jam <<EOF +using clang-darwin : : "/usr/local/bin/clang++" : <cxxflags>-std=c++11 <include>/usr/local/include ; +EOF + +# jww (2012-04-24): This is still linking against /usr/lib/libc++.1.dylib +# instead of /usr/local/lib/libc++.1.dylib +make "$@" OPTJ=-j20 \ + BOOST_DEFINES="-sICU_PATH=/opt/local cxxflags=\"-I/opt/local/include\" linkflags=\"-L/opt/local/lib\"" diff --git a/lib/build-icc.sh b/lib/build-icc.sh new file mode 100755 index 00000000..406397de --- /dev/null +++ b/lib/build-icc.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# This build script is for OS X Lion users who have compiled openmpi and +# clang-3.1 from MacPorts. I build my own Boost instead of using MacPorts' +# Boost in order to get better debugging support, and to link with libc++. + +export PATH=$PATH:/opt/local/lib/openmpi/bin + +cat > ~/user-config.jam <<EOF +using intel : : "/opt/intel/bin/icc" : ; +EOF + +# jww (2012-04-24): This is still linking against /usr/lib/libc++.1.dylib +# instead of /usr/local/lib/libc++.1.dylib +make CXX=icc LD=icc CC=icc AR=xiar OPTJ=-j20 \ + BOOST_TOOLSET=intel DIR_SUFFIX=intel \ + BOOST_DEFINES="-sINTEL_PATH=/opt/intel/bin -sINTEL_VERSION=12 -sICU_PATH=/usr/local cxxflags=\"-I/usr/local/include -I/opt/local/include\" linkflags=\"-L/usr/local/lib -L/opt/local/lib\"" diff --git a/lib/build.sh b/lib/build.sh new file mode 100755 index 00000000..4f3f2e7f --- /dev/null +++ b/lib/build.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# This build script is for OS X Lion users who have compiled openmpi and +# clang-3.1 from MacPorts. I build my own Boost instead of using MacPorts' +# Boost in order to get better debugging support, and to link with libc++. + +#export PATH=$PATH:/opt/local/lib/openmpi/bin + +cat > ~/user-config.jam <<EOF +using clang-darwin : : "/usr/local/bin/clang++" : <cxxflags>-std=c++11 ; +EOF + +# jww (2012-04-24): This is still linking against /usr/lib/libc++.1.dylib +# instead of /usr/local/lib/libc++.1.dylib +make CXX=clang++ LD=clang++ CC=clang OPTJ=-j20 \ + BOOST_TOOLSET=clang-darwin DIR_SUFFIX=clang31 \ + BOOST_DEFINES="-sHAVE_ICONV=1 -sHAVE_ICU=1 -sICU_PATH=/usr/local/opt/icu4c cxxflags=\"-g -std=c++11 $* -nostdlibinc -isystem /usr/local/include -isystem /usr/local/include/c++/v1 -isystem /usr/include -stdlib=libc++\" linkflags=\"-g $* -L/usr/local/lib -L/usr/lib /usr/local/lib/libc++.dylib -stdlib=libc++\"" diff --git a/lib/utfcpp/v2_0/LICENSE b/lib/utfcpp/v2_0/LICENSE new file mode 100644 index 00000000..36b7cd93 --- /dev/null +++ b/lib/utfcpp/v2_0/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/lib/utfcpp/v2_0/buildrelease.pl b/lib/utfcpp/v2_0/buildrelease.pl new file mode 100755 index 00000000..ed3a17bb --- /dev/null +++ b/lib/utfcpp/v2_0/buildrelease.pl @@ -0,0 +1,18 @@ +#! /usr/bin/perl + +$release_files = 'source/utf8.h source/utf8/core.h source/utf8/checked.h source/utf8/unchecked.h doc/utf8cpp.html doc/ReleaseNotes'; + +# First get the latest version +`svn update`; + +# Then construct the name of the zip file +$argc = @ARGV; +if ($argc > 0) { + $zip_name = $ARGV[0]; +} +else { + $zip_name = "utf8"; +} + +# Zip the files to an archive +`zip $zip_name $release_files`; diff --git a/lib/utfcpp/v2_0/samples/Makefile b/lib/utfcpp/v2_0/samples/Makefile new file mode 100644 index 00000000..6cdd3c85 --- /dev/null +++ b/lib/utfcpp/v2_0/samples/Makefile @@ -0,0 +1,5 @@ +CC = g++ +CFLAGS = -g -Wall -pedantic + +docsample: docsample.cpp ../source/utf8.h + $(CC) $(CFLAGS) docsample.cpp -odocsample diff --git a/lib/utfcpp/v2_0/samples/docsample.cpp b/lib/utfcpp/v2_0/samples/docsample.cpp new file mode 100644 index 00000000..6f8953b9 --- /dev/null +++ b/lib/utfcpp/v2_0/samples/docsample.cpp @@ -0,0 +1,52 @@ +#include "../source/utf8.h" +#include <iostream> +#include <fstream> +#include <string> +#include <vector> + + +using namespace std; + +int main(int argc, char** argv) +{ + if (argc != 2) { + cout << "\nUsage: docsample filename\n"; + return 0; + } + const char* test_file_path = argv[1]; + // Open the test file (must be UTF-8 encoded) + ifstream fs8(test_file_path); + if (!fs8.is_open()) { + cout << "Could not open " << test_file_path << endl; + return 0; + } + + unsigned line_count = 1; + string line; + // Play with all the lines in the file + while (getline(fs8, line)) { + // check for invalid utf-8 (for a simple yes/no check, there is also utf8::is_valid function) + string::iterator end_it = utf8::find_invalid(line.begin(), line.end()); + if (end_it != line.end()) { + cout << "Invalid UTF-8 encoding detected at line " << line_count << "\n"; + cout << "This part is fine: " << string(line.begin(), end_it) << "\n"; + } + // Get the line length (at least for the valid part) + int length = utf8::distance(line.begin(), end_it); + cout << "Length of line " << line_count << " is " << length << "\n"; + + // Convert it to utf-16 + vector<unsigned short> utf16line; + utf8::utf8to16(line.begin(), end_it, back_inserter(utf16line)); + // And back to utf-8; + string utf8line; + utf8::utf16to8(utf16line.begin(), utf16line.end(), back_inserter(utf8line)); + // Confirm that the conversion went OK: + if (utf8line != string(line.begin(), end_it)) + cout << "Error in UTF-16 conversion at line: " << line_count << "\n"; + + line_count++; + } + + return 0; +} diff --git a/lib/utfcpp/v2_0/source/utf8.h b/lib/utfcpp/v2_0/source/utf8.h new file mode 100644 index 00000000..4e445140 --- /dev/null +++ b/lib/utfcpp/v2_0/source/utf8.h @@ -0,0 +1,34 @@ +// Copyright 2006 Nemanja Trifunovic + +/* +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + + +#ifndef UTF8_FOR_CPP_2675DCD0_9480_4c0c_B92A_CC14C027B731 +#define UTF8_FOR_CPP_2675DCD0_9480_4c0c_B92A_CC14C027B731 + +#include "utf8/checked.h" +#include "utf8/unchecked.h" + +#endif // header guard diff --git a/lib/utfcpp/v2_0/source/utf8/checked.h b/lib/utfcpp/v2_0/source/utf8/checked.h new file mode 100644 index 00000000..13311551 --- /dev/null +++ b/lib/utfcpp/v2_0/source/utf8/checked.h @@ -0,0 +1,327 @@ +// Copyright 2006 Nemanja Trifunovic + +/* +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + + +#ifndef UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 +#define UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 + +#include "core.h" +#include <stdexcept> + +namespace utf8 +{ + // Base for the exceptions that may be thrown from the library + class exception : public ::std::exception { + }; + + // Exceptions that may be thrown from the library functions. + class invalid_code_point : public exception { + uint32_t cp; + public: + invalid_code_point(uint32_t cp) : cp(cp) {} + virtual const char* what() const throw() { return "Invalid code point"; } + uint32_t code_point() const {return cp;} + }; + + class invalid_utf8 : public exception { + uint8_t u8; + public: + invalid_utf8 (uint8_t u) : u8(u) {} + virtual const char* what() const throw() { return "Invalid UTF-8"; } + uint8_t utf8_octet() const {return u8;} + }; + + class invalid_utf16 : public exception { + uint16_t u16; + public: + invalid_utf16 (uint16_t u) : u16(u) {} + virtual const char* what() const throw() { return "Invalid UTF-16"; } + uint16_t utf16_word() const {return u16;} + }; + + class not_enough_room : public exception { + public: + virtual const char* what() const throw() { return "Not enough space"; } + }; + + /// The library API - functions intended to be called by the users + + template <typename octet_iterator> + octet_iterator append(uint32_t cp, octet_iterator result) + { + if (!utf8::internal::is_code_point_valid(cp)) + throw invalid_code_point(cp); + + if (cp < 0x80) // one octet + *(result++) = static_cast<uint8_t>(cp); + else if (cp < 0x800) { // two octets + *(result++) = static_cast<uint8_t>((cp >> 6) | 0xc0); + *(result++) = static_cast<uint8_t>((cp & 0x3f) | 0x80); + } + else if (cp < 0x10000) { // three octets + *(result++) = static_cast<uint8_t>((cp >> 12) | 0xe0); + *(result++) = static_cast<uint8_t>(((cp >> 6) & 0x3f) | 0x80); + *(result++) = static_cast<uint8_t>((cp & 0x3f) | 0x80); + } + else { // four octets + *(result++) = static_cast<uint8_t>((cp >> 18) | 0xf0); + *(result++) = static_cast<uint8_t>(((cp >> 12) & 0x3f) | 0x80); + *(result++) = static_cast<uint8_t>(((cp >> 6) & 0x3f) | 0x80); + *(result++) = static_cast<uint8_t>((cp & 0x3f) | 0x80); + } + return result; + } + + template <typename octet_iterator, typename output_iterator> + output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out, uint32_t replacement) + { + while (start != end) { + octet_iterator sequence_start = start; + internal::utf_error err_code = utf8::internal::validate_next(start, end); + switch (err_code) { + case internal::UTF8_OK : + for (octet_iterator it = sequence_start; it != start; ++it) + *out++ = *it; + break; + case internal::NOT_ENOUGH_ROOM: + throw not_enough_room(); + case internal::INVALID_LEAD: + out = utf8::append (replacement, out); + ++start; + break; + case internal::INCOMPLETE_SEQUENCE: + case internal::OVERLONG_SEQUENCE: + case internal::INVALID_CODE_POINT: + out = utf8::append (replacement, out); + ++start; + // just one replacement mark for the sequence + while (start != end && utf8::internal::is_trail(*start)) + ++start; + break; + } + } + return out; + } + + template <typename octet_iterator, typename output_iterator> + inline output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out) + { + static const uint32_t replacement_marker = utf8::internal::mask16(0xfffd); + return utf8::replace_invalid(start, end, out, replacement_marker); + } + + template <typename octet_iterator> + uint32_t next(octet_iterator& it, octet_iterator end) + { + uint32_t cp = 0; + internal::utf_error err_code = utf8::internal::validate_next(it, end, cp); + switch (err_code) { + case internal::UTF8_OK : + break; + case internal::NOT_ENOUGH_ROOM : + throw not_enough_room(); + case internal::INVALID_LEAD : + case internal::INCOMPLETE_SEQUENCE : + case internal::OVERLONG_SEQUENCE : + throw invalid_utf8(*it); + case internal::INVALID_CODE_POINT : + throw invalid_code_point(cp); + } + return cp; + } + + template <typename octet_iterator> + uint32_t peek_next(octet_iterator it, octet_iterator end) + { + return utf8::next(it, end); + } + + template <typename octet_iterator> + uint32_t prior(octet_iterator& it, octet_iterator start) + { + // can't do much if it == start + if (it == start) + throw not_enough_room(); + + octet_iterator end = it; + // Go back until we hit either a lead octet or start + while (utf8::internal::is_trail(*(--it))) + if (it == start) + throw invalid_utf8(*it); // error - no lead byte in the sequence + return utf8::peek_next(it, end); + } + + /// Deprecated in versions that include "prior" + template <typename octet_iterator> + uint32_t previous(octet_iterator& it, octet_iterator pass_start) + { + octet_iterator end = it; + while (utf8::internal::is_trail(*(--it))) + if (it == pass_start) + throw invalid_utf8(*it); // error - no lead byte in the sequence + octet_iterator temp = it; + return utf8::next(temp, end); + } + + template <typename octet_iterator, typename distance_type> + void advance (octet_iterator& it, distance_type n, octet_iterator end) + { + for (distance_type i = 0; i < n; ++i) + utf8::next(it, end); + } + + template <typename octet_iterator> + typename std::iterator_traits<octet_iterator>::difference_type + distance (octet_iterator first, octet_iterator last) + { + typename std::iterator_traits<octet_iterator>::difference_type dist; + for (dist = 0; first < last; ++dist) + utf8::next(first, last); + return dist; + } + + template <typename u16bit_iterator, typename octet_iterator> + octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) + { + while (start != end) { + uint32_t cp = utf8::internal::mask16(*start++); + // Take care of surrogate pairs first + if (utf8::internal::is_lead_surrogate(cp)) { + if (start != end) { + uint32_t trail_surrogate = utf8::internal::mask16(*start++); + if (utf8::internal::is_trail_surrogate(trail_surrogate)) + cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; + else + throw invalid_utf16(static_cast<uint16_t>(trail_surrogate)); + } + else + throw invalid_utf16(static_cast<uint16_t>(cp)); + + } + // Lone trail surrogate + else if (utf8::internal::is_trail_surrogate(cp)) + throw invalid_utf16(static_cast<uint16_t>(cp)); + + result = utf8::append(cp, result); + } + return result; + } + + template <typename u16bit_iterator, typename octet_iterator> + u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) + { + while (start != end) { + uint32_t cp = utf8::next(start, end); + if (cp > 0xffff) { //make a surrogate pair + *result++ = static_cast<uint16_t>((cp >> 10) + internal::LEAD_OFFSET); + *result++ = static_cast<uint16_t>((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); + } + else + *result++ = static_cast<uint16_t>(cp); + } + return result; + } + + template <typename octet_iterator, typename u32bit_iterator> + octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) + { + while (start != end) + result = utf8::append(*(start++), result); + + return result; + } + + template <typename octet_iterator, typename u32bit_iterator> + u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) + { + while (start != end) + (*result++) = utf8::next(start, end); + + return result; + } + + // The iterator class + template <typename octet_iterator> + class iterator : public std::iterator <std::bidirectional_iterator_tag, uint32_t> { + octet_iterator it; + octet_iterator range_start; + octet_iterator range_end; + public: + iterator () {} + explicit iterator (const octet_iterator& octet_it, + const octet_iterator& range_start, + const octet_iterator& range_end) : + it(octet_it), range_start(range_start), range_end(range_end) + { + if (it < range_start || it > range_end) + throw std::out_of_range("Invalid utf-8 iterator position"); + } + // the default "big three" are OK + octet_iterator base () const { return it; } + uint32_t operator * () const + { + octet_iterator temp = it; + return utf8::next(temp, range_end); + } + bool operator == (const iterator& rhs) const + { + if (range_start != rhs.range_start || range_end != rhs.range_end) + throw std::logic_error("Comparing utf-8 iterators defined with different ranges"); + return (it == rhs.it); + } + bool operator != (const iterator& rhs) const + { + return !(operator == (rhs)); + } + iterator& operator ++ () + { + utf8::next(it, range_end); + return *this; + } + iterator operator ++ (int) + { + iterator temp = *this; + utf8::next(it, range_end); + return temp; + } + iterator& operator -- () + { + utf8::prior(it, range_start); + return *this; + } + iterator operator -- (int) + { + iterator temp = *this; + utf8::prior(it, range_start); + return temp; + } + }; // class iterator + +} // namespace utf8 + +#endif //header guard + + diff --git a/lib/utfcpp/v2_0/source/utf8/core.h b/lib/utfcpp/v2_0/source/utf8/core.h new file mode 100644 index 00000000..693d388c --- /dev/null +++ b/lib/utfcpp/v2_0/source/utf8/core.h @@ -0,0 +1,329 @@ +// Copyright 2006 Nemanja Trifunovic + +/* +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + + +#ifndef UTF8_FOR_CPP_CORE_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 +#define UTF8_FOR_CPP_CORE_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 + +#include <iterator> + +namespace utf8 +{ + // The typedefs for 8-bit, 16-bit and 32-bit unsigned integers + // You may need to change them to match your system. + // These typedefs have the same names as ones from cstdint, or boost/cstdint + typedef unsigned char uint8_t; + typedef unsigned short uint16_t; + typedef unsigned int uint32_t; + +// Helper code - not intended to be directly called by the library users. May be changed at any time +namespace internal +{ + // Unicode constants + // Leading (high) surrogates: 0xd800 - 0xdbff + // Trailing (low) surrogates: 0xdc00 - 0xdfff + const uint16_t LEAD_SURROGATE_MIN = 0xd800u; + const uint16_t LEAD_SURROGATE_MAX = 0xdbffu; + const uint16_t TRAIL_SURROGATE_MIN = 0xdc00u; + const uint16_t TRAIL_SURROGATE_MAX = 0xdfffu; + const uint16_t LEAD_OFFSET = LEAD_SURROGATE_MIN - (0x10000 >> 10); + const uint32_t SURROGATE_OFFSET = 0x10000u - (LEAD_SURROGATE_MIN << 10) - TRAIL_SURROGATE_MIN; + + // Maximum valid value for a Unicode code point + const uint32_t CODE_POINT_MAX = 0x0010ffffu; + + template<typename octet_type> + inline uint8_t mask8(octet_type oc) + { + return static_cast<uint8_t>(0xff & oc); + } + template<typename u16_type> + inline uint16_t mask16(u16_type oc) + { + return static_cast<uint16_t>(0xffff & oc); + } + template<typename octet_type> + inline bool is_trail(octet_type oc) + { + return ((utf8::internal::mask8(oc) >> 6) == 0x2); + } + + template <typename u16> + inline bool is_lead_surrogate(u16 cp) + { + return (cp >= LEAD_SURROGATE_MIN && cp <= LEAD_SURROGATE_MAX); + } + + template <typename u16> + inline bool is_trail_surrogate(u16 cp) + { + return (cp >= TRAIL_SURROGATE_MIN && cp <= TRAIL_SURROGATE_MAX); + } + + template <typename u16> + inline bool is_surrogate(u16 cp) + { + return (cp >= LEAD_SURROGATE_MIN && cp <= TRAIL_SURROGATE_MAX); + } + + template <typename u32> + inline bool is_code_point_valid(u32 cp) + { + return (cp <= CODE_POINT_MAX && !utf8::internal::is_surrogate(cp)); + } + + template <typename octet_iterator> + inline typename std::iterator_traits<octet_iterator>::difference_type + sequence_length(octet_iterator lead_it) + { + uint8_t lead = utf8::internal::mask8(*lead_it); + if (lead < 0x80) + return 1; + else if ((lead >> 5) == 0x6) + return 2; + else if ((lead >> 4) == 0xe) + return 3; + else if ((lead >> 3) == 0x1e) + return 4; + else + return 0; + } + + template <typename octet_difference_type> + inline bool is_overlong_sequence(uint32_t cp, octet_difference_type length) + { + if (cp < 0x80) { + if (length != 1) + return true; + } + else if (cp < 0x800) { + if (length != 2) + return true; + } + else if (cp < 0x10000) { + if (length != 3) + return true; + } + + return false; + } + + enum utf_error {UTF8_OK, NOT_ENOUGH_ROOM, INVALID_LEAD, INCOMPLETE_SEQUENCE, OVERLONG_SEQUENCE, INVALID_CODE_POINT}; + + /// Helper for get_sequence_x + template <typename octet_iterator> + utf_error increase_safely(octet_iterator& it, octet_iterator end) + { + if (++it == end) + return NOT_ENOUGH_ROOM; + + if (!utf8::internal::is_trail(*it)) + return INCOMPLETE_SEQUENCE; + + return UTF8_OK; + } + + #define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) {utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) return ret;} + + /// get_sequence_x functions decode utf-8 sequences of the length x + template <typename octet_iterator> + utf_error get_sequence_1(octet_iterator& it, octet_iterator end, uint32_t& code_point) + { + if (it == end) + return NOT_ENOUGH_ROOM; + + code_point = utf8::internal::mask8(*it); + + return UTF8_OK; + } + + template <typename octet_iterator> + utf_error get_sequence_2(octet_iterator& it, octet_iterator end, uint32_t& code_point) + { + if (it == end) + return NOT_ENOUGH_ROOM; + + code_point = utf8::internal::mask8(*it); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = ((code_point << 6) & 0x7ff) + ((*it) & 0x3f); + + return UTF8_OK; + } + + template <typename octet_iterator> + utf_error get_sequence_3(octet_iterator& it, octet_iterator end, uint32_t& code_point) + { + if (it == end) + return NOT_ENOUGH_ROOM; + + code_point = utf8::internal::mask8(*it); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = ((code_point << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point += (*it) & 0x3f; + + return UTF8_OK; + } + + template <typename octet_iterator> + utf_error get_sequence_4(octet_iterator& it, octet_iterator end, uint32_t& code_point) + { + if (it == end) + return NOT_ENOUGH_ROOM; + + code_point = utf8::internal::mask8(*it); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = ((code_point << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point += (utf8::internal::mask8(*it) << 6) & 0xfff; + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point += (*it) & 0x3f; + + return UTF8_OK; + } + + #undef UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR + + template <typename octet_iterator> + utf_error validate_next(octet_iterator& it, octet_iterator end, uint32_t& code_point) + { + // Save the original value of it so we can go back in case of failure + // Of course, it does not make much sense with i.e. stream iterators + octet_iterator original_it = it; + + uint32_t cp = 0; + // Determine the sequence length based on the lead octet + typedef typename std::iterator_traits<octet_iterator>::difference_type octet_difference_type; + const octet_difference_type length = utf8::internal::sequence_length(it); + + // Get trail octets and calculate the code point + utf_error err = UTF8_OK; + switch (length) { + case 0: + return INVALID_LEAD; + case 1: + err = utf8::internal::get_sequence_1(it, end, cp); + break; + case 2: + err = utf8::internal::get_sequence_2(it, end, cp); + break; + case 3: + err = utf8::internal::get_sequence_3(it, end, cp); + break; + case 4: + err = utf8::internal::get_sequence_4(it, end, cp); + break; + } + + if (err == UTF8_OK) { + // Decoding succeeded. Now, security checks... + if (utf8::internal::is_code_point_valid(cp)) { + if (!utf8::internal::is_overlong_sequence(cp, length)){ + // Passed! Return here. + code_point = cp; + ++it; + return UTF8_OK; + } + else + err = OVERLONG_SEQUENCE; + } + else + err = INVALID_CODE_POINT; + } + + // Failure branch - restore the original value of the iterator + it = original_it; + return err; + } + + template <typename octet_iterator> + inline utf_error validate_next(octet_iterator& it, octet_iterator end) { + uint32_t ignored; + return utf8::internal::validate_next(it, end, ignored); + } + +} // namespace internal + + /// The library API - functions intended to be called by the users + + // Byte order mark + const uint8_t bom[] = {0xef, 0xbb, 0xbf}; + + template <typename octet_iterator> + octet_iterator find_invalid(octet_iterator start, octet_iterator end) + { + octet_iterator result = start; + while (result != end) { + utf8::internal::utf_error err_code = utf8::internal::validate_next(result, end); + if (err_code != internal::UTF8_OK) + return result; + } + return result; + } + + template <typename octet_iterator> + inline bool is_valid(octet_iterator start, octet_iterator end) + { + return (utf8::find_invalid(start, end) == end); + } + + template <typename octet_iterator> + inline bool starts_with_bom (octet_iterator it, octet_iterator end) + { + return ( + ((it != end) && (utf8::internal::mask8(*it++)) == bom[0]) && + ((it != end) && (utf8::internal::mask8(*it++)) == bom[1]) && + ((it != end) && (utf8::internal::mask8(*it)) == bom[2]) + ); + } + + //Deprecated in release 2.3 + template <typename octet_iterator> + inline bool is_bom (octet_iterator it) + { + return ( + (utf8::internal::mask8(*it++)) == bom[0] && + (utf8::internal::mask8(*it++)) == bom[1] && + (utf8::internal::mask8(*it)) == bom[2] + ); + } +} // namespace utf8 + +#endif // header guard + + diff --git a/lib/utfcpp/v2_0/source/utf8/unchecked.h b/lib/utfcpp/v2_0/source/utf8/unchecked.h new file mode 100644 index 00000000..cb242716 --- /dev/null +++ b/lib/utfcpp/v2_0/source/utf8/unchecked.h @@ -0,0 +1,228 @@ +// Copyright 2006 Nemanja Trifunovic + +/* +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + + +#ifndef UTF8_FOR_CPP_UNCHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 +#define UTF8_FOR_CPP_UNCHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 + +#include "core.h" + +namespace utf8 +{ + namespace unchecked + { + template <typename octet_iterator> + octet_iterator append(uint32_t cp, octet_iterator result) + { + if (cp < 0x80) // one octet + *(result++) = static_cast<uint8_t>(cp); + else if (cp < 0x800) { // two octets + *(result++) = static_cast<uint8_t>((cp >> 6) | 0xc0); + *(result++) = static_cast<uint8_t>((cp & 0x3f) | 0x80); + } + else if (cp < 0x10000) { // three octets + *(result++) = static_cast<uint8_t>((cp >> 12) | 0xe0); + *(result++) = static_cast<uint8_t>(((cp >> 6) & 0x3f) | 0x80); + *(result++) = static_cast<uint8_t>((cp & 0x3f) | 0x80); + } + else { // four octets + *(result++) = static_cast<uint8_t>((cp >> 18) | 0xf0); + *(result++) = static_cast<uint8_t>(((cp >> 12) & 0x3f)| 0x80); + *(result++) = static_cast<uint8_t>(((cp >> 6) & 0x3f) | 0x80); + *(result++) = static_cast<uint8_t>((cp & 0x3f) | 0x80); + } + return result; + } + + template <typename octet_iterator> + uint32_t next(octet_iterator& it) + { + uint32_t cp = utf8::internal::mask8(*it); + typename std::iterator_traits<octet_iterator>::difference_type length = utf8::internal::sequence_length(it); + switch (length) { + case 1: + break; + case 2: + it++; + cp = ((cp << 6) & 0x7ff) + ((*it) & 0x3f); + break; + case 3: + ++it; + cp = ((cp << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); + ++it; + cp += (*it) & 0x3f; + break; + case 4: + ++it; + cp = ((cp << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); + ++it; + cp += (utf8::internal::mask8(*it) << 6) & 0xfff; + ++it; + cp += (*it) & 0x3f; + break; + } + ++it; + return cp; + } + + template <typename octet_iterator> + uint32_t peek_next(octet_iterator it) + { + return utf8::unchecked::next(it); + } + + template <typename octet_iterator> + uint32_t prior(octet_iterator& it) + { + while (utf8::internal::is_trail(*(--it))) ; + octet_iterator temp = it; + return utf8::unchecked::next(temp); + } + + // Deprecated in versions that include prior, but only for the sake of consistency (see utf8::previous) + template <typename octet_iterator> + inline uint32_t previous(octet_iterator& it) + { + return utf8::unchecked::prior(it); + } + + template <typename octet_iterator, typename distance_type> + void advance (octet_iterator& it, distance_type n) + { + for (distance_type i = 0; i < n; ++i) + utf8::unchecked::next(it); + } + + template <typename octet_iterator> + typename std::iterator_traits<octet_iterator>::difference_type + distance (octet_iterator first, octet_iterator last) + { + typename std::iterator_traits<octet_iterator>::difference_type dist; + for (dist = 0; first < last; ++dist) + utf8::unchecked::next(first); + return dist; + } + + template <typename u16bit_iterator, typename octet_iterator> + octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) + { + while (start != end) { + uint32_t cp = utf8::internal::mask16(*start++); + // Take care of surrogate pairs first + if (utf8::internal::is_lead_surrogate(cp)) { + uint32_t trail_surrogate = utf8::internal::mask16(*start++); + cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; + } + result = utf8::unchecked::append(cp, result); + } + return result; + } + + template <typename u16bit_iterator, typename octet_iterator> + u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) + { + while (start < end) { + uint32_t cp = utf8::unchecked::next(start); + if (cp > 0xffff) { //make a surrogate pair + *result++ = static_cast<uint16_t>((cp >> 10) + internal::LEAD_OFFSET); + *result++ = static_cast<uint16_t>((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); + } + else + *result++ = static_cast<uint16_t>(cp); + } + return result; + } + + template <typename octet_iterator, typename u32bit_iterator> + octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) + { + while (start != end) + result = utf8::unchecked::append(*(start++), result); + + return result; + } + + template <typename octet_iterator, typename u32bit_iterator> + u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) + { + while (start < end) + (*result++) = utf8::unchecked::next(start); + + return result; + } + + // The iterator class + template <typename octet_iterator> + class iterator : public std::iterator <std::bidirectional_iterator_tag, uint32_t> { + octet_iterator it; + public: + iterator () {} + explicit iterator (const octet_iterator& octet_it): it(octet_it) {} + // the default "big three" are OK + octet_iterator base () const { return it; } + uint32_t operator * () const + { + octet_iterator temp = it; + return utf8::unchecked::next(temp); + } + bool operator == (const iterator& rhs) const + { + return (it == rhs.it); + } + bool operator != (const iterator& rhs) const + { + return !(operator == (rhs)); + } + iterator& operator ++ () + { + ::std::advance(it, utf8::internal::sequence_length(it)); + return *this; + } + iterator operator ++ (int) + { + iterator temp = *this; + ::std::advance(it, utf8::internal::sequence_length(it)); + return temp; + } + iterator& operator -- () + { + utf8::unchecked::prior(it); + return *this; + } + iterator operator -- (int) + { + iterator temp = *this; + utf8::unchecked::prior(it); + return temp; + } + }; // class iterator + + } // namespace utf8::unchecked +} // namespace utf8 + + +#endif // header guard + diff --git a/python/__init__.py b/python/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/demo.py b/python/demo.py new file mode 100755 index 00000000..88931b17 --- /dev/null +++ b/python/demo.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python + +import sys +from datetime import datetime + +# The following literate program will demonstrate, by example, how to use the +# Ledger Python module to access your data and build custom reports using the +# magic of Python. + +import ledger + +print "Welcome to the Ledger.Python demo!" + +# Some quick helper functions to help us assert various types of truth +# throughout the script. + +def assertEqual(pat, candidate): + if pat != candidate: + raise Exception("FAILED: %s != %s" % (pat, candidate)) + sys.exit(1) + +############################################################################### +# +# COMMODITIES +# +# Every amount in Ledger has a commodity, even if it is the "null commodity". +# What's special about commodities are not just their symbol, but how they +# alter the way amounts are displayed. +# +# For example, internally Ledger uses infinite precision rational numbers, +# which have no decimal point. So how does it know that $1.00 / $0.75 should +# be displayed as $1.33, and not with an infinitely repeating decimal? It +# does it by consulting the commodity. +# +# Whenever an amount is encountered in your data file, Ledger observes how you +# specified it: +# - How many digits of precision did you use? +# - Was the commodity name before or after the amount? +# - Was the commodity separated from the amount by a space? +# - Did you use thousands markers (1,000)? +# - Did you use European-style numbers (1.000,00)? +# +# By tracking this information for each commodity, Ledger knows how you want +# to see the amount in your reports. This way, dollars can be output as +# $123.56, while stock options could be output as 10.113 AAPL. +# +# Your program can access the known set of commodities using the global +# `ledger.commodities'. This object behaves like a dict, and support all of +# the non-modifying dict protocol methods. If you wish to create a new +# commodity without parsing an amount, you can use the method +# `find_or_create': + +comms = ledger.commodities + +usd = comms.find_or_create('$') +eur = comms.find_or_create('EUR') +xcd = comms.find_or_create('XCD') + +assert not comms.find('CAD') +assert not comms.has_key('CAD') +assert not 'CAD' in comms + +# The above mentioned commodity display attributes can be set using commodity +# display flags. This is not something you will usually be doing, however, as +# these flags can be inferred correctly from a large enough set of sample +# amounts, such as those found in your data file. If you live in Europe and +# want all amounts to default to the European-style, set the static variable +# `european_by_default'. + +eur.add_flags(ledger.COMMODITY_STYLE_DECIMAL_COMMA) +assert eur.has_flags(ledger.COMMODITY_STYLE_DECIMAL_COMMA) +assert not eur.has_flags(ledger.COMMODITY_STYLE_THOUSANDS) + +comms.european_by_default = True + +# There are a few built-in commodities: null, %, h, m and s. Normally you +# don't need to worry about them, but they'll show up if you examine all the +# keys in the commodities dict. + +assertEqual([u'', u'$', u'%', u'EUR', u'XCD', u'h', u'm', u's'], + sorted(comms.keys())) + +# All the styles of dict iteration are supported: + +for symbol in comms.iterkeys(): + pass +for commodity in comms.itervalues(): + pass +#for symbol, commodity in comms.iteritems(): +# pass +#for symbol, commodity in comms: +# pass + +# Another important thing about commodities is that they remember if they've +# been exchanged for another commodity, and what the conversion rate was on +# that date. You can record specific conversion rates for any date using the +# `exchange' method. + +comms.exchange(eur, ledger.Amount('$0.77')) # Trade 1 EUR for $0.77 +comms.exchange(eur, ledger.Amount('$0.66'), datetime.now()) + +# For the most part, however, you won't be interacting with commodities +# directly, except maybe to look at their `symbol'. + +assertEqual('$', usd.symbol) +assertEqual('$', comms['$'].symbol) + +############################################################################### +# +# AMOUNTS & BALANCES +# +# Ledger deals with two basic numerical values: Amount and Balance objects. +# An Amount is an infinite-precision rational with an associated commodity +# (even if it is the null commodity, which is called an "uncommoditized +# amount"). A Balance is a collection of Amounts of differing commodities. +# +# Amounts support all the math operations you might expect of an integer, +# except it carries a commodity. Let's take dollars for example: + +zero = ledger.Amount("$0") +one = ledger.Amount("$1") +oneb = ledger.Amount("$1") +two = ledger.Amount("$2") +three = ledger.Amount("3") # uncommoditized + +assert one == oneb # numeric equality, not identity +assert one != two +assert not zero # tests if it would *display* as a zero +assert one < two +assert one > zero + +# For addition and subtraction, only amounts of the same commodity may be +# used, unless one of the amounts has no commodity at all -- in which case the +# result uses the commodity of the other value. Adding $10 to 10 EUR, for +# example, causes an ArithmeticError exception, but adding 10 to $10 gives +# $20. + +four = ledger.Amount(two) # make a copy +four += two +assertEqual(four, two + two) +assertEqual(zero, one - one) + +try: + two += ledger.Amount("20 EUR") + assert False +except ArithmeticError: + pass + +# Use `number' to get the uncommoditized version of an Amount + +assertEqual(three, (two + one).number()) + +# Multiplication and division does supports Amounts of different commodities, +# however: +# - If either amount is uncommoditized, the result carries the commodity of +# the other amount. +# - Otherwise, the result always carries the commodity of the first amount. + +five = ledger.Amount("5 CAD") + +assertEqual(one, two / two) +assertEqual(five, (five * ledger.Amount("$2")) - ledger.Amount("5")) + +# An amount's commodity determines the decimal precision it's displayed with. +# However, this "precision" is a notional thing only. You can tell an amount +# to ignore its display precision by setting `keep_precision' to True. +# (Uncommoditized amounts ignore the value of `keep_precision', and assume it +# is always True). In this case, Ledger does its best to maintain maximal +# precision by watching how the Amount is used. That is, 1.01 * 1.01 yields a +# precision of 4. This tracking is just a best estimate, however, since +# internally Ledger never uses floating-point values. + +amt = ledger.Amount('$100.12') +mini = ledger.Amount('0.00045') + +assert not amt.keep_precision + +assertEqual(5, mini.precision) +assertEqual(5, mini.display_precision) # display_precision == precision +assertEqual(2, amt.precision) +assertEqual(2, amt.display_precision) + +mini *= mini +amt *= amt + +assertEqual(10, mini.precision) +assertEqual(10, mini.display_precision) +assertEqual(4, amt.precision) +assertEqual(2, amt.display_precision) + +# There are several other supported math operations: + +amt = ledger.Amount('$100.12') +market = ((ledger.Amount('1 EUR') / ledger.Amount('$0.77')) * amt) + +assertEqual(market, amt.value(eur)) # find present market value + +assertEqual('$-100.12', str(amt.negated())) # negate the amount +assertEqual('$-100.12', str(- amt)) # negate it more simply +assertEqual('$0.01', str(amt.inverted())) # reverse NUM/DEM +assertEqual('$100.12', str(amt.rounded())) # round it to display precision +assertEqual('$100.12', str(amt.truncated())) # truncate to display precision +assertEqual('$100.00', str(amt.floored())) # floor it to nearest integral +assertEqual('$100.12', str(abs(amt))) # absolute value +assertEqual('$100.12', str(amt)) # render to a string +assertEqual('100.12', amt.quantity_string()) # render quantity to a string +assertEqual('100.12', str(amt.number())) # strip away commodity +assertEqual(1, amt.sign()) # -1, 0 or 1 +assert amt.is_nonzero() # True if display amount nonzero +assert not amt.is_zero() # True if display amount is zero +assert not amt.is_realzero() # True only if value is 0/0 +assert not amt.is_null() # True if uninitialized + +# Amounts can also be converted the standard floats and integers, although +# this is not recommend since it can lose precision. + +assertEqual(100.12, amt.to_double()) +assert amt.fits_in_long() # there is no `fits_in_double' +assertEqual(100, amt.to_long()) + +# Finally, amounts can be annotated to provide additional information about +# "lots" of a given commodity. This example shows $100.12 that was purchased +# on 2009/10/01 for 140 EUR. Lot information can be accessed through via the +# Amount's `annotation' property. You can also strip away lot details to get +# the underlying amount. If you want the total price of any Amount, by +# multiplying by its per-unit lot price, call the `Amount.price' method +# instead of the `Annotation.price' property. + +amt2 = ledger.Amount('$100.12 {140 EUR} [2009/10/01]') + +assert amt2.has_annotation() +assertEqual(amt, amt2.strip_annotations()) + +assertEqual(ledger.Amount('140 EUR'), amt2.annotation.price) +assertEqual(ledger.Amount('14016,8 EUR'), amt2.price()) # european amount! + +############################################################################### +# +# VALUES +# +# As common as Amounts and Balances are, there is a more prevalent numeric +# type you will encounter when generating reports: Value objects. A Value is +# a variadic type that can be any of the following types: +# - Amount +# - Balance +# - boolean +# - integer +# - datetime +# - date +# - string +# - regex +# - sequence +# +# The reason for the variadic type is that it supports dynamic self-promotion. +# For example, it is illegal to add two Amounts of different commodities, but +# it is not illegal to add two Value amounts of different commodities. In the +# former case an exception in raised, but in the latter the Value simply +# promotes itself to a Balance object to make the addition valid. +# +# Values are not used by any of Ledger's data objects (Journal, Transaction, +# Posting or Account), but they are used extensively by value expressions. + +val = ledger.Value('$100.00') + +assert val.is_amount() +assertEqual('$', val.to_amount().commodity.symbol) + +# JOURNALS + +#journal.find_account('') +#journal.find_or_create_account('') + +# ACCOUNTS + +#account.name +#account.fullname() +#account.amount +#account.total + +# TRANSACTIONS + +#txn.payee + +# POSTINGS + +#post.account + +# REPORTING + +#journal.collect('-M food') +#journal.collect_accounts('^assets ^liab ^equity') + +print 'Demo completed successfully.' diff --git a/python/res/asc.gif b/python/res/asc.gif new file mode 100644 index 00000000..74157867 Binary files /dev/null and b/python/res/asc.gif differ diff --git a/python/res/bg.gif b/python/res/bg.gif new file mode 100644 index 00000000..fac668fc Binary files /dev/null and b/python/res/bg.gif differ diff --git a/python/res/desc.gif b/python/res/desc.gif new file mode 100644 index 00000000..3b30b3c5 Binary files /dev/null and b/python/res/desc.gif differ diff --git a/python/res/icons/first.png b/python/res/icons/first.png new file mode 100644 index 00000000..6f11fcb0 Binary files /dev/null and b/python/res/icons/first.png differ diff --git a/python/res/icons/last.png b/python/res/icons/last.png new file mode 100644 index 00000000..72079357 Binary files /dev/null and b/python/res/icons/last.png differ diff --git a/python/res/icons/next.png b/python/res/icons/next.png new file mode 100644 index 00000000..4a2f9d4e Binary files /dev/null and b/python/res/icons/next.png differ diff --git a/python/res/icons/prev.png b/python/res/icons/prev.png new file mode 100644 index 00000000..15d1584b Binary files /dev/null and b/python/res/icons/prev.png differ diff --git a/python/res/jquery-latest.js b/python/res/jquery-latest.js new file mode 100644 index 00000000..3747929d --- /dev/null +++ b/python/res/jquery-latest.js @@ -0,0 +1,32 @@ +/* + * jQuery 1.2.3 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2008-02-06 00:21:25 -0500 (Wed, 06 Feb 2008) $ + * $Rev: 4663 $ + */ +(function(){if(window.jQuery)var _jQuery=window.jQuery;var jQuery=window.jQuery=function(selector,context){return new jQuery.prototype.init(selector,context);};if(window.$)var _$=window.$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;var isSimple=/^.[^:#\[\.]*$/;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}else if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem)if(elem.id!=match[3])return jQuery().find(selector);else{this[0]=elem;this.length=1;return this;}else +selector=[];}}else +return new jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return new jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(selector.constructor==Array&&selector||(selector.jquery||selector.length&&selector!=window&&!selector.nodeType&&selector[0]!=undefined&&selector[0].nodeType)&&jQuery.makeArray(selector)||[selector]);},jquery:"1.2.3",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;this.each(function(i){if(this==elem)ret=i;});return ret;},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value==undefined)return this.length&&jQuery[type||"attr"](this[0],name)||undefined;else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else +return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else +selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return!selector?this:this.pushStack(jQuery.merge(this.get(),selector.constructor==String?jQuery(selector).get():selector.length!=undefined&&(!selector.nodeName||jQuery.nodeName(selector,"form"))?selector:[selector]));},is:function(selector){return selector?jQuery.multiFilter(selector,this).length>0:false;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i<max;i++){var option=options[i];if(option.selected){value=jQuery.browser.msie&&!option.attributes.value.specified?option.text:option.value;if(one)return value;values.push(value);}}return values;}else +return(this[0].value||"").replace(/\r/g,"");}return undefined;}return this.each(function(){if(this.nodeType!=1)return;if(value.constructor==Array&&/radio|checkbox/.test(this.type))this.checked=(jQuery.inArray(this.value,value)>=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=value.constructor==Array?value:[value];jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else +this.value=value;});},html:function(value){return value==undefined?(this.length?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value==null){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data==undefined&&this.length)data=jQuery.data(this[0],key);return data==null&&parts[1]?this.data(parts[0]):data;}else +return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script")){scripts=scripts.add(elem);}else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.prototype.init.prototype=jQuery.prototype;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else +jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==1){target=this;i=0;}for(;i<length;i++)if((options=arguments[i])!=null)for(var name in options){if(target===options[name])continue;if(deep&&options[name]&&typeof options[name]=="object"&&target[name]&&!options[name].nodeType)target[name]=jQuery.extend(target[name],options[name]);else if(options[name]!=undefined)target[name]=options[name];}return target;};var expando="jQuery"+(new Date()).getTime(),uuid=0,windowData={};var exclude=/z-?index|font-?weight|opacity|zoom|line-?height/i;jQuery.extend({noConflict:function(deep){window.$=_$;if(deep)window.jQuery=_jQuery;return jQuery;},isFunction:function(fn){return!!fn&&typeof fn!="string"&&!fn.nodeName&&fn.constructor!=Array&&/function/i.test(fn+"");},isXMLDoc:function(elem){return elem.documentElement&&!elem.body||elem.tagName&&elem.ownerDocument&&!elem.ownerDocument.body;},globalEval:function(data){data=jQuery.trim(data);if(data){var head=document.getElementsByTagName("head")[0]||document.documentElement,script=document.createElement("script");script.type="text/javascript";if(jQuery.browser.msie)script.text=data;else +script.appendChild(document.createTextNode(data));head.appendChild(script);head.removeChild(script);}},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toUpperCase()==name.toUpperCase();},cache:{},data:function(elem,name,data){elem=elem==window?windowData:elem;var id=elem[expando];if(!id)id=elem[expando]=++uuid;if(name&&!jQuery.cache[id])jQuery.cache[id]={};if(data!=undefined)jQuery.cache[id][name]=data;return name?jQuery.cache[id][name]:id;},removeData:function(elem,name){elem=elem==window?windowData:elem;var id=elem[expando];if(name){if(jQuery.cache[id]){delete jQuery.cache[id][name];name="";for(name in jQuery.cache[id])break;if(!name)jQuery.removeData(elem);}}else{try{delete elem[expando];}catch(e){if(elem.removeAttribute)elem.removeAttribute(expando);}delete jQuery.cache[id];}},each:function(object,callback,args){if(args){if(object.length==undefined){for(var name in object)if(callback.apply(object[name],args)===false)break;}else +for(var i=0,length=object.length;i<length;i++)if(callback.apply(object[i],args)===false)break;}else{if(object.length==undefined){for(var name in object)if(callback.call(object[name],name,object[name])===false)break;}else +for(var i=0,length=object.length,value=object[0];i<length&&callback.call(value,i,value)!==false;value=object[++i]){}}return object;},prop:function(elem,value,type,i,name){if(jQuery.isFunction(value))value=value.call(elem,i);return value&&value.constructor==Number&&type=="curCSS"&&!exclude.test(name)?value+"px":value;},className:{add:function(elem,classNames){jQuery.each((classNames||"").split(/\s+/),function(i,className){if(elem.nodeType==1&&!jQuery.className.has(elem.className,className))elem.className+=(elem.className?" ":"")+className;});},remove:function(elem,classNames){if(elem.nodeType==1)elem.className=classNames!=undefined?jQuery.grep(elem.className.split(/\s+/),function(className){return!jQuery.className.has(classNames,className);}).join(" "):"";},has:function(elem,className){return jQuery.inArray(className,(elem.className||elem).toString().split(/\s+/))>-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else +jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret;function color(elem){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=elem.style.outline;elem.style.outline="0 solid black";elem.style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&elem.style&&elem.style[name])ret=elem.style[name];else if(document.defaultView&&document.defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var getComputedStyle=document.defaultView.getComputedStyle(elem,null);if(getComputedStyle&&!color(elem))ret=getComputedStyle.getPropertyValue(name);else{var swap=[],stack=[];for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(var i=0;i<stack.length;i++)if(color(stack[i])){swap[i]=stack[i].style.display;stack[i].style.display="block";}ret=name=="display"&&swap[stack.length-1]!=null?"none":(getComputedStyle&&getComputedStyle.getPropertyValue(name))||"";for(var i=0;i<swap.length;i++)if(swap[i]!=null)stack[i].style.display=swap[i];}if(name=="opacity"&&ret=="")ret="1";}else if(elem.currentStyle){var camelCase=name.replace(/\-(\w)/g,function(all,letter){return letter.toUpperCase();});ret=elem.currentStyle[name]||elem.currentStyle[camelCase];if(!/^\d+(px)?$/i.test(ret)&&/^\d/.test(ret)){var style=elem.style.left,runtimeStyle=elem.runtimeStyle.left;elem.runtimeStyle.left=elem.currentStyle.left;elem.style.left=ret||0;ret=elem.style.pixelLeft+"px";elem.style.left=style;elem.runtimeStyle.left=runtimeStyle;}}return ret;},clean:function(elems,context){var ret=[];context=context||document;if(typeof context.createElement=='undefined')context=context.ownerDocument||context[0]&&context[0].ownerDocument||document;jQuery.each(elems,function(i,elem){if(!elem)return;if(elem.constructor==Number)elem=elem.toString();if(typeof elem=="string"){elem=elem.replace(/(<(\w+)[^>]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+"></"+tag+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!tags.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!tags.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!tags.indexOf("<td")||!tags.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!tags.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||jQuery.browser.msie&&[1,"div<div>","</div>"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf("<table")&&tags.indexOf("<tbody")<0?div.firstChild&&div.firstChild.childNodes:wrap[1]=="<table>"&&tags.indexOf("<tbody")<0?div.childNodes:[];for(var j=tbody.length-1;j>=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else +ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var fix=jQuery.isXMLDoc(elem)?{}:jQuery.props;if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(fix[name]){if(value!=undefined)elem[fix[name]]=value;return elem[fix[name]];}else if(jQuery.browser.msie&&name=="style")return jQuery.attr(elem.style,"cssText",value);else if(value==undefined&&jQuery.browser.msie&&jQuery.nodeName(elem,"form")&&(name=="action"||name=="method"))return elem.getAttributeNode(name).nodeValue;else if(elem.tagName){if(value!=undefined){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem.setAttribute(name,""+value);}if(jQuery.browser.msie&&/href|src/.test(name)&&!jQuery.isXMLDoc(elem))return elem.getAttribute(name,2);return elem.getAttribute(name);}else{if(name=="opacity"&&jQuery.browser.msie){if(value!=undefined){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseFloat(value).toString()=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100).toString():"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(value!=undefined)elem[name]=value;return elem[name];}},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(typeof array!="array")for(var i=0,length=array.length;i<length;i++)ret.push(array[i]);else +ret=array.slice(0);return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i<length;i++)if(array[i]==elem)return i;return-1;},merge:function(first,second){if(jQuery.browser.msie){for(var i=0;second[i];i++)if(second[i].nodeType!=8)first.push(second[i]);}else +for(var i=0;second[i];i++)first.push(second[i]);return first;},unique:function(array){var ret=[],done={};try{for(var i=0,length=array.length;i<length;i++){var id=jQuery.data(array[i]);if(!done[id]){done[id]=true;ret.push(array[i]);}}}catch(e){ret=array;}return ret;},grep:function(elems,callback,inv){var ret=[];for(var i=0,length=elems.length;i<length;i++)if(!inv&&callback(elems[i],i)||inv&&!callback(elems[i],i))ret.push(elems[i]);return ret;},map:function(elems,callback){var ret=[];for(var i=0,length=elems.length;i<length;i++){var value=callback(elems[i],i);if(value!==null&&value!=undefined){if(value.constructor!=Array)value=[value];ret=ret.concat(value);}}return ret;}});var userAgent=navigator.userAgent.toLowerCase();jQuery.browser={version:(userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1],safari:/webkit/.test(userAgent),opera:/opera/.test(userAgent),msie:/msie/.test(userAgent)&&!/opera/.test(userAgent),mozilla:/mozilla/.test(userAgent)&&!/(compatible|webkit)/.test(userAgent)};var styleFloat=jQuery.browser.msie?"styleFloat":"cssFloat";jQuery.extend({boxModel:!jQuery.browser.msie||document.compatMode=="CSS1Compat",props:{"for":"htmlFor","class":"className","float":styleFloat,cssFloat:styleFloat,styleFloat:styleFloat,innerHTML:"innerHTML",className:"className",value:"value",disabled:"disabled",checked:"checked",readonly:"readOnly",selected:"selected",maxlength:"maxLength",selectedIndex:"selectedIndex",defaultValue:"defaultValue",tagName:"tagName",nodeName:"nodeName"}});jQuery.each({parent:function(elem){return elem.parentNode;},parents:function(elem){return jQuery.dir(elem,"parentNode");},next:function(elem){return jQuery.nth(elem,2,"nextSibling");},prev:function(elem){return jQuery.nth(elem,2,"previousSibling");},nextAll:function(elem){return jQuery.dir(elem,"nextSibling");},prevAll:function(elem){return jQuery.dir(elem,"previousSibling");},siblings:function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},children:function(elem){return jQuery.sibling(elem.firstChild);},contents:function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}},function(name,fn){jQuery.fn[name]=function(selector){var ret=jQuery.map(this,fn);if(selector&&typeof selector=="string")ret=jQuery.multiFilter(selector,ret);return this.pushStack(jQuery.unique(ret));};});jQuery.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(name,original){jQuery.fn[name]=function(){var args=arguments;return this.each(function(){for(var i=0,length=args.length;i<length;i++)jQuery(args[i])[original](this);});};});jQuery.each({removeAttr:function(name){jQuery.attr(this,name,"");if(this.nodeType==1)this.removeAttribute(name);},addClass:function(classNames){jQuery.className.add(this,classNames);},removeClass:function(classNames){jQuery.className.remove(this,classNames);},toggleClass:function(classNames){jQuery.className[jQuery.className.has(this,classNames)?"remove":"add"](this,classNames);},remove:function(selector){if(!selector||jQuery.filter(selector,[this]).r.length){jQuery("*",this).add(this).each(function(){jQuery.event.remove(this);jQuery.removeData(this);});if(this.parentNode)this.parentNode.removeChild(this);}},empty:function(){jQuery(">*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return i<m[3]-0;},gt:function(a,i,m){return i>m[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false;var re=quickChild;var m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j<rl;j++){var n=m=="~"||m=="+"?ret[j].nextSibling:ret[j].firstChild;for(;n;n=n.nextSibling)if(n.nodeType==1){var id=jQuery.data(n);if(m=="~"&&merge[id])break;if(!nodeName||n.nodeName.toUpperCase()==nodeName){if(m=="~")merge[id]=true;r.push(n);}if(m=="+")break;}}ret=r;t=jQuery.trim(t.replace(re,""));foundToken=true;}}if(t&&!foundToken){if(!t.indexOf(",")){if(context==ret[0])ret.shift();done=jQuery.merge(done,ret);r=ret=[context];t=" "+t.substr(1,t.length);}else{var re2=quickID;var m=re2.exec(t);if(m){m=[0,m[2],m[3],m[1]];}else{re2=quickClass;m=re2.exec(t);}m[2]=m[2].replace(/\\/g,"");var elem=ret[ret.length-1];if(m[1]=="#"&&elem&&elem.getElementById&&!jQuery.isXMLDoc(elem)){var oid=elem.getElementById(m[2]);if((jQuery.browser.msie||jQuery.browser.opera)&&oid&&typeof oid.id=="string"&&oid.id!=m[2])oid=jQuery('[@id="'+m[2]+'"]',elem)[0];ret=r=oid&&(!m[3]||jQuery.nodeName(oid,m[3]))?[oid]:[];}else{for(var i=0;ret[i];i++){var tag=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];if(tag=="*"&&ret[i].nodeName.toLowerCase()=="object")tag="param";r=jQuery.merge(r,ret[i].getElementsByTagName(tag));}if(m[1]==".")r=jQuery.classFilter(r,m[2]);if(m[1]=="#"){var tmp=[];for(var i=0;r[i];i++)if(r[i].getAttribute("id")==m[2]){tmp=[r[i]];break;}r=tmp;}ret=r;}t=t.replace(re2,"");}}if(t){var val=jQuery.filter(t,r);ret=r=val.r;t=jQuery.trim(val.t);}}if(t)ret=[];if(ret&&context==ret[0])ret.shift();done=jQuery.merge(done,ret);return done;},classFilter:function(r,m,not){m=" "+m+" ";var tmp=[];for(var i=0;r[i];i++){var pass=(" "+r[i].className+" ").indexOf(m)>=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i<rl;i++){var a=r[i],z=a[jQuery.props[m[2]]||m[2]];if(z==null||/href|src|selected/.test(m[2]))z=jQuery.attr(a,m[2])||'';if((type==""&&!!z||type=="="&&z==m[5]||type=="!="&&z!=m[5]||type=="^="&&z&&!z.indexOf(m[5])||type=="$="&&z.substr(z.length-m[5].length)==m[5]||(type=="*="||type=="~=")&&z.indexOf(m[5])>=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i<rl;i++){var node=r[i],parentNode=node.parentNode,id=jQuery.data(parentNode);if(!merge[id]){var c=1;for(var n=parentNode.firstChild;n;n=n.nextSibling)if(n.nodeType==1)n.nodeIndex=c++;merge[id]=true;}var add=false;if(first==0){if(node.nodeIndex==last)add=true;}else if((node.nodeIndex-last)%first==0&&(node.nodeIndex-last)/first>=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[];var cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&(!elem||n!=elem))r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval!=undefined)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=function(){return fn.apply(this,arguments);};handler.data=data;handler.guid=fn.guid;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){var val;if(typeof jQuery=="undefined"||jQuery.event.triggered)return val;val=jQuery.event.handle.apply(arguments.callee.elem,arguments);return val;});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else +for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data||[]);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event)data.unshift(this.fix({type:type,target:elem}));data[0].type=type;if(exclusive)data[0].exclusive=true;if(jQuery.isFunction(jQuery.data(elem,"handle")))val=jQuery.data(elem,"handle").apply(elem,data);if(!fn&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val;event=jQuery.event.fix(event||window.event||{});var parts=event.type.split(".");event.type=parts[0];var handlers=jQuery.data(this,"events")&&jQuery.data(this,"events")[event.type],args=Array.prototype.slice.call(arguments,1);args.unshift(event);for(var j in handlers){var handler=handlers[j];args[0].handler=handler;args[0].data=handler.data;if(!parts[1]&&!event.exclusive||handler.type==parts[1]){var ret=handler.apply(this,args);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}if(jQuery.browser.msie)event.target=event.preventDefault=event.stopPropagation=event.handler=event.data=null;return val;},fix:function(event){var originalEvent=event;event=jQuery.extend({},originalEvent);event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=originalEvent.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;arguments[0].type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;arguments[0].type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){return this.each(function(){jQuery.event.add(this,type,function(event){jQuery(this).unbind(event);return(fn||data).apply(this,arguments);},fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){if(this[0])return jQuery.event.trigger(type,data,this[0],false,fn);return undefined;},toggle:function(){var args=arguments;return this.click(function(event){this.lastToggle=0==this.lastToggle?1:0;event.preventDefault();return args[this.lastToggle].apply(this,arguments)||false;});},hover:function(fnOver,fnOut){return this.bind('mouseenter',fnOver).bind('mouseleave',fnOut);},ready:function(fn){bindReady();if(jQuery.isReady)fn.call(document,jQuery);else +jQuery.readyList.push(function(){return fn.call(this,jQuery);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.apply(document);});jQuery.readyList=null;}jQuery(document).triggerHandler("ready");}}});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(document.addEventListener&&!jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);if(jQuery.browser.msie&&window==top)(function(){if(jQuery.isReady)return;try{document.documentElement.doScroll("left");}catch(error){setTimeout(arguments.callee,0);return;}jQuery.ready();})();if(jQuery.browser.opera)document.addEventListener("DOMContentLoaded",function(){if(jQuery.isReady)return;for(var i=0;i<document.styleSheets.length;i++)if(document.styleSheets[i].disabled){setTimeout(arguments.callee,0);return;}jQuery.ready();},false);if(jQuery.browser.safari){var numStyles;(function(){if(jQuery.isReady)return;if(document.readyState!="loaded"&&document.readyState!="complete"){setTimeout(arguments.callee,0);return;}if(numStyles===undefined)numStyles=jQuery("style, link[rel=stylesheet]").length;if(document.styleSheets.length!=numStyles){setTimeout(arguments.callee,0);return;}jQuery.ready();})();}jQuery.event.add(window,"load",jQuery.ready);}jQuery.each(("blur,focus,load,resize,scroll,unload,click,dblclick,"+"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"+"submit,keydown,keypress,keyup,error").split(","),function(i,name){jQuery.fn[name]=function(fn){return fn?this.bind(name,fn):this.trigger(name);};});var withinElement=function(event,elem){var parent=event.relatedTarget;while(parent&&parent!=elem)try{parent=parent.parentNode;}catch(error){parent=elem;}return parent==elem;};jQuery(window).bind("unload",function(){jQuery("*").add(document).unbind();});jQuery.fn.extend({load:function(url,params,callback){if(jQuery.isFunction(url))return this.bind("load",url);var off=url.indexOf(" ");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("<div/>").append(res.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=(new Date).getTime();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){var jsonp,jsre=/=\?(&|$)/g,status,data;s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(s.type.toLowerCase()=="get"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&s.type.toLowerCase()=="get"){var ts=(new Date()).getTime();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&s.type.toLowerCase()=="get"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");if((!s.url.indexOf("http")||!s.url.indexOf("//"))&&s.dataType=="script"&&s.type.toLowerCase()=="get"){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xml=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();xml.open(s.type,s.url,s.async,s.username,s.password);try{if(s.data)xml.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xml.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xml.setRequestHeader("X-Requested-With","XMLHttpRequest");xml.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend)s.beforeSend(xml);if(s.global)jQuery.event.trigger("ajaxSend",[xml,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xml&&(xml.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xml)&&"error"||s.ifModified&&jQuery.httpNotModified(xml,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xml,s.dataType);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xml.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else +jQuery.handleError(s,xml,status);complete();if(s.async)xml=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xml){xml.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xml.send(s.data);}catch(e){jQuery.handleError(s,xml,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xml,s]);}function complete(){if(s.complete)s.complete(xml,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xml,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xml;},handleError:function(s,xml,status,e){if(s.error)s.error(xml,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xml,s,e]);},active:0,httpSuccess:function(r){try{return!r.status&&location.protocol=="file:"||(r.status>=200&&r.status<300)||r.status==304||r.status==1223||jQuery.browser.safari&&r.status==undefined;}catch(e){}return false;},httpNotModified:function(xml,url){try{var xmlRes=xml.getResponseHeader("Last-Modified");return xml.status==304||xmlRes==jQuery.lastModified[url]||jQuery.browser.safari&&xml.status==undefined;}catch(e){}return false;},httpData:function(r,type){var ct=r.getResponseHeader("content-type");var xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0;var data=xml?r.responseXML:r.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else +for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else +s.push(encodeURIComponent(j)+"="+encodeURIComponent(a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle(fn,fn2):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall);var hidden=jQuery(this).is(":hidden"),self=this;for(var p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return jQuery.isFunction(opt.complete)&&opt.complete.apply(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else +e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.apply(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(!elem)return undefined;type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",array?jQuery.makeArray(array):[]);return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].apply(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:{slow:600,fast:200}[opt.duration])||400;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.apply(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.apply(this.elem,[this.now,this]);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=(new Date()).getTime();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;i<timers.length;i++)if(!timers[i]())timers.splice(i--,1);if(!timers.length){clearInterval(jQuery.timerId);jQuery.timerId=null;}},13);}},show:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.show=true;this.custom(0,this.cur());if(this.prop=="width"||this.prop=="height")this.elem.style[this.prop]="1px";jQuery(this.elem).show();},hide:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0);},step:function(gotoEnd){var t=(new Date()).getTime();if(gotoEnd||t>this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done&&jQuery.isFunction(this.options.complete))this.options.complete.apply(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.fx.step={scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}};jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),fixed=jQuery.css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&jQuery.css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(jQuery.css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&jQuery.css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||jQuery.css(offsetChild,"position")=="absolute"))||(mozilla&&jQuery.css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l)||0;top+=parseInt(t)||0;}return results;};})(); \ No newline at end of file diff --git a/python/res/jquery.dimensions.min.js b/python/res/jquery.dimensions.min.js new file mode 100644 index 00000000..34c06dee --- /dev/null +++ b/python/res/jquery.dimensions.min.js @@ -0,0 +1,12 @@ +/* Copyright (c) 2007 Paul Bakaus (paul.bakaus@googlemail.com) and Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net) + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * $LastChangedDate: 2007-12-20 08:43:48 -0600 (Thu, 20 Dec 2007) $ + * $Rev: 4257 $ + * + * Version: 1.2 + * + * Requires: jQuery 1.2+ + */ +(function($){$.dimensions={version:'1.2'};$.each(['Height','Width'],function(i,name){$.fn['inner'+name]=function(){if(!this[0])return;var torl=name=='Height'?'Top':'Left',borr=name=='Height'?'Bottom':'Right';return this.is(':visible')?this[0]['client'+name]:num(this,name.toLowerCase())+num(this,'padding'+torl)+num(this,'padding'+borr);};$.fn['outer'+name]=function(options){if(!this[0])return;var torl=name=='Height'?'Top':'Left',borr=name=='Height'?'Bottom':'Right';options=$.extend({margin:false},options||{});var val=this.is(':visible')?this[0]['offset'+name]:num(this,name.toLowerCase())+num(this,'border'+torl+'Width')+num(this,'border'+borr+'Width')+num(this,'padding'+torl)+num(this,'padding'+borr);return val+(options.margin?(num(this,'margin'+torl)+num(this,'margin'+borr)):0);};});$.each(['Left','Top'],function(i,name){$.fn['scroll'+name]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(name=='Left'?val:$(window)['scrollLeft'](),name=='Top'?val:$(window)['scrollTop']()):this['scroll'+name]=val;}):this[0]==window||this[0]==document?self[(name=='Left'?'pageXOffset':'pageYOffset')]||$.boxModel&&document.documentElement['scroll'+name]||document.body['scroll'+name]:this[0]['scroll'+name];};});$.fn.extend({position:function(){var left=0,top=0,elem=this[0],offset,parentOffset,offsetParent,results;if(elem){offsetParent=this.offsetParent();offset=this.offset();parentOffset=offsetParent.offset();offset.top-=num(elem,'marginTop');offset.left-=num(elem,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&$.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return $(offsetParent);}});function num(el,prop){return parseInt($.curCSS(el.jquery?el[0]:el,prop,true))||0;};})(jQuery); \ No newline at end of file diff --git a/python/res/jquery.metadata.js b/python/res/jquery.metadata.js new file mode 100644 index 00000000..6a984dbc --- /dev/null +++ b/python/res/jquery.metadata.js @@ -0,0 +1,122 @@ +/* + * Metadata - jQuery plugin for parsing metadata from elements + * + * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id$ + * + */ + +/** + * Sets the type of metadata to use. Metadata is encoded in JSON, and each property + * in the JSON will become a property of the element itself. + * + * There are three supported types of metadata storage: + * + * attr: Inside an attribute. The name parameter indicates *which* attribute. + * + * class: Inside the class attribute, wrapped in curly braces: { } + * + * elem: Inside a child element (e.g. a script tag). The + * name parameter indicates *which* element. + * + * The metadata for an element is loaded the first time the element is accessed via jQuery. + * + * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements + * matched by expr, then redefine the metadata type and run another $(expr) for other elements. + * + * @name $.metadata.setType + * + * @example <p id="one" class="some_class {item_id: 1, item_label: 'Label'}">This is a p</p> + * @before $.metadata.setType("class") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from the class attribute + * + * @example <p id="one" class="some_class" data="{item_id: 1, item_label: 'Label'}">This is a p</p> + * @before $.metadata.setType("attr", "data") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a "data" attribute + * + * @example <p id="one" class="some_class"><script>{item_id: 1, item_label: 'Label'}</script>This is a p</p> + * @before $.metadata.setType("elem", "script") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a nested script element + * + * @param String type The encoding type + * @param String name The name of the attribute to be used to get metadata (optional) + * @cat Plugins/Metadata + * @descr Sets the type of encoding to be used when loading metadata for the first time + * @type undefined + * @see metadata() + */ + +(function($) { + +$.extend({ + metadata : { + defaults : { + type: 'class', + name: 'metadata', + cre: /({.*})/, + single: 'metadata' + }, + setType: function( type, name ){ + this.defaults.type = type; + this.defaults.name = name; + }, + get: function( elem, opts ){ + var settings = $.extend({},this.defaults,opts); + // check for empty string in single property + if ( !settings.single.length ) settings.single = 'metadata'; + + var data = $.data(elem, settings.single); + // returned cached data if it already exists + if ( data ) return data; + + data = "{}"; + + if ( settings.type == "class" ) { + var m = settings.cre.exec( elem.className ); + if ( m ) + data = m[1]; + } else if ( settings.type == "elem" ) { + if( !elem.getElementsByTagName ) + return undefined; + var e = elem.getElementsByTagName(settings.name); + if ( e.length ) + data = $.trim(e[0].innerHTML); + } else if ( elem.getAttribute != undefined ) { + var attr = elem.getAttribute( settings.name ); + if ( attr ) + data = attr; + } + + if ( data.indexOf( '{' ) <0 ) + data = "{" + data + "}"; + + data = eval("(" + data + ")"); + + $.data( elem, settings.single, data ); + return data; + } + } +}); + +/** + * Returns the metadata object for the first member of the jQuery object. + * + * @name metadata + * @descr Returns element's metadata object + * @param Object opts An object contianing settings to override the defaults + * @type jQuery + * @cat Plugins/Metadata + */ +$.fn.metadata = function( opts ){ + return $.metadata.get( this[0], opts ); +}; + +})(jQuery); \ No newline at end of file diff --git a/python/res/jquery.tablesorter.min.js b/python/res/jquery.tablesorter.min.js new file mode 100644 index 00000000..64c70071 --- /dev/null +++ b/python/res/jquery.tablesorter.min.js @@ -0,0 +1,2 @@ + +(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,cells[i]);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,node){var l=parsers.length;for(var i=1;i<l;i++){if(parsers[i].is($.trim(getElementText(table.config,node)),table,node)){return parsers[i];}}return parsers[0];}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=table.tBodies[0].rows[i],cols=[];cache.row.push($(c));for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c.cells[j]),table,c.cells[j]));}cols.push(i);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){if(!node)return"";var t="";if(config.textExtraction=="simple"){if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){t=node.childNodes[0].innerHTML;}else{t=node.innerHTML;}}else{if(typeof(config.textExtraction)=="function"){t=config.textExtraction(node);}else{t=$(node).text();}}return t;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){rows.push(r[n[i][checkCell]]);if(!table.config.appender){var o=r[n[i][checkCell]];var l=o.length;for(var j=0;j<l;j++){tableBody[0].appendChild(o[j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false,tableHeadersRows=[];for(var i=0;i<table.tHead.rows.length;i++){tableHeadersRows[i]=0;};$tableHeaders=$("thead th",table);$tableHeaders.each(function(index){this.count=0;this.column=index;this.order=formatSortingOrder(table.config.sortInitialOrder);if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(!this.sortDisabled){$(this).addClass(table.config.cssHeader);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){i=(v.toLowerCase()=="desc")?1:0;}else{i=(v==(0||1))?v:0;}return i;}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(getCachedSortType(table.config.parsers,c)=="text")?((order==0)?"sortText":"sortTextDesc"):((order==0)?"sortNumeric":"sortNumericDesc");var e="e"+i;dynamicExp+="var "+e+" = "+s+"(a["+c+"],b["+c+"]); ";dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function sortText(a,b){return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){var DECIMAL='\\'+config.decimal;var exp='/(^[+]?0('+DECIMAL+'0+)?$)|(^([-+]?[1-9][0-9]*)$)|(^([-+]?((0?|[1-9][0-9]*)'+DECIMAL+'(0*[1-9][0-9]*)))$)|(^[-+]?[1-9]+[0-9]*'+DECIMAL+'0+$)/';return RegExp(exp).test($.trim(s));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[^0-9.]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}$("tr:visible",table.tBodies[0]).filter(':even').removeClass(table.config.widgetZebra.css[1]).addClass(table.config.widgetZebra.css[0]).end().filter(':odd').removeClass(table.config.widgetZebra.css[0]).addClass(table.config.widgetZebra.css[1]);if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery); \ No newline at end of file diff --git a/python/res/jquery.tablesorter.pager.css b/python/res/jquery.tablesorter.pager.css new file mode 100644 index 00000000..8c8e9fca --- /dev/null +++ b/python/res/jquery.tablesorter.pager.css @@ -0,0 +1,25 @@ +div.tablesorterPager { + padding: 10px 0 10px 0; + background-color: #D6D2C2; + text-align: center; +} +div.tablesorterPager span { + padding: 0 5px 0 5px; +} +div.tablesorterPager input.prev { + width: auto; + margin-right: 10px; +} +div.tablesorterPager input.next { + width: auto; + margin-left: 10px; +} +div.tablesorterPager input { + font-size: 8px; + width: 50px; + border: 1px solid #330000; + text-align: center; +} + + + \ No newline at end of file diff --git a/python/res/jquery.tablesorter.pager.js b/python/res/jquery.tablesorter.pager.js new file mode 100644 index 00000000..cce6ea26 --- /dev/null +++ b/python/res/jquery.tablesorter.pager.js @@ -0,0 +1,184 @@ +(function($) { + $.extend({ + tablesorterPager: new function() { + + function updatePageDisplay(c) { + var s = $(c.cssPageDisplay,c.container).val((c.page+1) + c.seperator + c.totalPages); + } + + function setPageSize(table,size) { + var c = table.config; + c.size = size; + c.totalPages = Math.ceil(c.totalRows / c.size); + c.pagerPositionSet = false; + moveToPage(table); + fixPosition(table); + } + + function fixPosition(table) { + var c = table.config; + if(!c.pagerPositionSet && c.positionFixed) { + var c = table.config, o = $(table); + if(o.offset) { + c.container.css({ + top: o.offset().top + o.height() + 'px', + position: 'absolute' + }); + } + c.pagerPositionSet = true; + } + } + + function moveToFirstPage(table) { + var c = table.config; + c.page = 0; + moveToPage(table); + } + + function moveToLastPage(table) { + var c = table.config; + c.page = (c.totalPages-1); + moveToPage(table); + } + + function moveToNextPage(table) { + var c = table.config; + c.page++; + if(c.page >= (c.totalPages-1)) { + c.page = (c.totalPages-1); + } + moveToPage(table); + } + + function moveToPrevPage(table) { + var c = table.config; + c.page--; + if(c.page <= 0) { + c.page = 0; + } + moveToPage(table); + } + + + function moveToPage(table) { + var c = table.config; + if(c.page < 0 || c.page > (c.totalPages-1)) { + c.page = 0; + } + + renderTable(table,c.rowsCopy); + } + + function renderTable(table,rows) { + + var c = table.config; + var l = rows.length; + var s = (c.page * c.size); + var e = (s + c.size); + if(e > rows.length ) { + e = rows.length; + } + + + var tableBody = $(table.tBodies[0]); + + // clear the table body + + $.tablesorter.clearTableBody(table); + + for(var i = s; i < e; i++) { + + //tableBody.append(rows[i]); + + var o = rows[i]; + var l = o.length; + for(var j=0; j < l; j++) { + + tableBody[0].appendChild(o[j]); + + } + } + + fixPosition(table,tableBody); + + $(table).trigger("applyWidgets"); + + if( c.page >= c.totalPages ) { + moveToLastPage(table); + } + + updatePageDisplay(c); + } + + this.appender = function(table,rows) { + + var c = table.config; + + c.rowsCopy = rows; + c.totalRows = rows.length; + c.totalPages = Math.ceil(c.totalRows / c.size); + + renderTable(table,rows); + }; + + this.defaults = { + size: 10, + offset: 0, + page: 0, + totalRows: 0, + totalPages: 0, + container: null, + cssNext: '.next', + cssPrev: '.prev', + cssFirst: '.first', + cssLast: '.last', + cssPageDisplay: '.pagedisplay', + cssPageSize: '.pagesize', + seperator: "/", + positionFixed: true, + appender: this.appender + }; + + this.construct = function(settings) { + + return this.each(function() { + + config = $.extend(this.config, $.tablesorterPager.defaults, settings); + + var table = this, pager = config.container; + + $(this).trigger("appendCache"); + + config.size = parseInt($(".pagesize",pager).val()); + + $(config.cssFirst,pager).click(function() { + moveToFirstPage(table); + return false; + }); + $(config.cssNext,pager).click(function() { + moveToNextPage(table); + return false; + }); + $(config.cssPrev,pager).click(function() { + moveToPrevPage(table); + return false; + }); + $(config.cssLast,pager).click(function() { + moveToLastPage(table); + return false; + }); + $(config.cssPageSize,pager).change(function() { + setPageSize(table,parseInt($(this).val())); + return false; + }); + }); + }; + + } + }); + // extend plugin scope + $.fn.extend({ + tablesorterPager: $.tablesorterPager.construct + }); + +})(jQuery); \ No newline at end of file diff --git a/python/res/style.css b/python/res/style.css new file mode 100644 index 00000000..eb41f70c --- /dev/null +++ b/python/res/style.css @@ -0,0 +1,39 @@ +/* tables */ +table.tablesorter { + font-family:arial; + background-color: #CDCDCD; + margin:10px 0pt 15px; + font-size: 8pt; + width: 100%; + text-align: left; +} +table.tablesorter thead tr th, table.tablesorter tfoot tr th { + background-color: #e6EEEE; + border: 1px solid #FFF; + font-size: 8pt; + padding: 4px; +} +table.tablesorter thead tr .header { + background-image: url(bg.gif); + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; +} +table.tablesorter tbody td { + color: #3D3D3D; + padding: 4px; + background-color: #FFF; + vertical-align: top; +} +table.tablesorter tbody tr.odd td { + background-color:#F0F0F6; +} +table.tablesorter thead tr .headerSortUp { + background-image: url(asc.gif); +} +table.tablesorter thead tr .headerSortDown { + background-image: url(desc.gif); +} +table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp { +background-color: #8dbdd8; +} diff --git a/python/server.py b/python/server.py new file mode 100644 index 00000000..13182836 --- /dev/null +++ b/python/server.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +import ledger +import cgi +import sys +import types +import posixpath +import urllib +import shutil +import os +import re + +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from os.path import exists, join, isfile + +from Cheetah.Template import Template +from Cheetah.Filters import Filter, WebSafe + +webroot = join(os.getcwd(), 'python', 'res') + +class UnicodeFilter(Filter): + def filter(self, s, **kargs): + return Filter.filter(self, s, str=unicode, **kargs) + +def strip(value): + #return re.sub('\n', '<br />', value.strip_annotations().to_string()) + return value.strip_annotations().to_string() + +templateDef = '''#encoding utf-8 + <html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <title>$title + + + + + + + + +
+

Register report

+ + + + + + + + + + + + + + + + + + + + + #for $post in $posts + #set $total = $total + $post.amount + + + + + + + + + #set $last_xact = $post.xact + #end for + +
DatePayeeAccountAmountTotal
DatePayeeAccountAmountTotal
$post.xact.date$post.xact.payee$post.account${strip($post.amount)}${strip($total)}
+
+
+ + + + + + +
+
+ + +''' + +class LedgerHandler(BaseHTTPRequestHandler): + def __init__(self, *args): + self.journal = ledger.Journal(sys.argv[1]) + BaseHTTPRequestHandler.__init__(self, *args) + + def do_GET(self): + path = self.translate_path(self.path) + + if path and exists(path) and isfile(path): + self.copyfile(open(path), self.wfile) + else: + tmpl = Template(templateDef, filter=UnicodeFilter) + + tmpl.title = 'Ledger Journal' + tmpl.posts = self.journal.collect(sys.argv[2]) + tmpl.total = ledger.Value(0) + tmpl.strip = strip + tmpl.last_xact = None + tmpl.empty = "" + + html = unicode(tmpl) + html = html.encode('utf-8') + self.wfile.write(html) + + def do_POST(self): + print "Saw a POST request!" + try: + ctype, pdict = cgi.parse_header(self.headers.getheader('content-type')) + if ctype == 'multipart/form-data': + query = cgi.parse_multipart(self.rfile, pdict) + self.send_response(301) + self.end_headers() + except Exception: + print "Saw exception in POST handler" + + # This code is straight from SimpleHTTPServer.py + def copyfile(self, source, outputfile): + """Copy all data between two file objects. + + The SOURCE argument is a file object open for reading + (or anything with a read() method) and the DESTINATION + argument is a file object open for writing (or + anything with a write() method). + + The only reason for overriding this would be to change + the block size or perhaps to replace newlines by CRLF + -- note however that this the default server uses this + to copy binary data as well. + + """ + shutil.copyfileobj(source, outputfile) + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + path = posixpath.normpath(urllib.unquote(path)) + words = path.split('/') + words = filter(None, words) + path = webroot + for word in words: + drive, word = os.path.splitdrive(word) + head, word = os.path.split(word) + if word in (os.curdir, os.pardir): continue + path = os.path.join(path, word) + return path + +def main(*args): + try: + port = 9000 + server = HTTPServer(('', port), LedgerHandler) + print "Local HTTP server listening on port %d... (Control-C to exit)" \ + % port + server.serve_forever() + except KeyboardInterrupt: + print "Shutting down server" + server.socket.close() + +if __name__ == '__main__': + if len(sys.argv) < 3: + print "usage: server.py " + sys.exit(1) + main() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..756df376 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,334 @@ +set(LEDGER_CLI_SOURCES + global.cc + main.cc) + +set(LEDGER_SOURCES + stats.cc + generate.cc + csv.cc + convert.cc + draft.cc + emacs.cc + ptree.cc + print.cc + output.cc + precmd.cc + chain.cc + filters.cc + report.cc + views.cc + select.cc + session.cc + option.cc + lookup.cc + compare.cc + iterators.cc + timelog.cc + textual.cc + temps.cc + journal.cc + account.cc + xact.cc + post.cc + item.cc + format.cc + query.cc + scope.cc + expr.cc + op.cc + parser.cc + token.cc + value.cc + balance.cc + quotes.cc + history.cc + pool.cc + annotate.cc + commodity.cc + amount.cc + stream.cc + mask.cc + times.cc + error.cc + utils.cc + wcwidth.cc) + +if (HAVE_BOOST_PYTHON) + list(APPEND LEDGER_SOURCES + py_account.cc + py_amount.cc + py_balance.cc + py_commodity.cc + py_expr.cc + py_format.cc + py_item.cc + py_journal.cc + py_post.cc + py_session.cc + py_times.cc + py_utils.cc + py_value.cc + py_xact.cc + pyinterp.cc + pyledger.cc) +endif() + +set(LEDGER_INCLUDES + account.h + amount.h + annotate.h + balance.h + chain.h + commodity.h + compare.h + context.h + convert.h + csv.h + draft.h + emacs.h + error.h + expr.h + exprbase.h + filters.h + flags.h + format.h + generate.h + global.h + history.h + item.h + iterators.h + journal.h + lookup.h + mask.h + op.h + option.h + output.h + parser.h + pool.h + post.h + precmd.h + predicate.h + print.h + pstream.h + ptree.h + pyfstream.h + pyinterp.h + pyutils.h + query.h + quotes.h + report.h + scope.h + select.h + session.h + stats.h + stream.h + temps.h + timelog.h + times.h + token.h + unistring.h + utils.h + value.h + views.h + xact.h + ${PROJECT_BINARY_DIR}/system.hh) + +# Windows provides no strptime(), so supply our own. +if (WIN32 OR CYGWIN) + list(APPEND LEDGER_INCLUDES + strptime.h) + list(APPEND LEDGER_SOURCES + strptime.cc) +endif() + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_definitions( + # -Weverything + # -Wno-disabled-macro-expansion + # -Wno-padded + # -Wno-weak-vtables + # -Wno-exit-time-destructors + # -Wno-global-constructors + # -Wno-switch-enum + # -Wno-missing-prototypes + # -Wno-missing-noreturn + # -Wno-unused-parameter + # -Wno-c++98-compat + # -fno-limit-debug-info + -Wno-\#pragma-messages + -Wno-unused-local-typedef + --system-header-prefix=include/boost/ + --system-header-prefix=boost/) + + macro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + set(_pch_filename "${_header_filename}.pch") + + set_source_files_properties( + ${${_src_list}} PROPERTIES COMPILE_FLAGS "-include ${_header_filename}") + if (_other_srcs) + set_source_files_properties( + ${_other_srcs} PROPERTIES COMPILE_FLAGS "-include ${_header_filename}") + endif() + list(APPEND ${_src_list} ${_pch_filename}) + + set(_args ${CMAKE_CXX_FLAGS}) + list(APPEND _args ${CMAKE_CXX_FLAGS_DEBUG}) + if (BUILD_LIBRARY) + list(APPEND _args ${CMAKE_SHARED_LIBRARY_CXX_FLAGS}) + endif() + list(APPEND _args "-std=c++11 ") + if (CYGWIN) + list(APPEND _args "-U__STRICT_ANSI__") + endif() + list(APPEND _args "-x c++-header " ${_inc}) + list(APPEND _args -c ${_header_filename} -o ${_pch_filename}) + + get_directory_property(DIRINC INCLUDE_DIRECTORIES) + foreach(_inc ${DIRINC}) + list(APPEND _args "-isystem " ${_inc}) + endforeach(_inc ${DIRINC}) + + separate_arguments(_args) + + add_custom_command(OUTPUT ${_pch_filename} + COMMAND rm -f ${_pch_filename} + COMMAND ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_ARG1} ${_args} + DEPENDS ${_header_filename}) + endmacro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(GXX_WARNING_FLAGS + -pedantic + -Wall + -Winvalid-pch + -Wextra + -Wcast-align + -Wcast-qual + -Wfloat-equal + -Wmissing-field-initializers + -Wno-endif-labels + -Wno-overloaded-virtual + -Wsign-compare + -Wsign-promo + -Wwrite-strings + -Wno-unused-parameter + -Wno-old-style-cast + -Wno-deprecated + -Wno-strict-aliasing) + + add_definitions(${GXX_WARNING_FLAGS}) + + macro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + set(_gch_filename "${_header_filename}.gch") + + set_source_files_properties( + ${${_src_list}} PROPERTIES COMPILE_FLAGS "-Winvalid-pch") + if (_other_srcs) + set_source_files_properties( + ${_other_srcs} PROPERTIES COMPILE_FLAGS "-Winvalid-pch") + endif() + list(APPEND ${_src_list} ${_gch_filename}) + + set(_args ${CMAKE_CXX_FLAGS}) + list(APPEND _args ${CMAKE_CXX_FLAGS_DEBUG}) + if (BUILD_LIBRARY) + list(APPEND _args ${CMAKE_SHARED_LIBRARY_CXX_FLAGS}) + endif() + list(APPEND _args ${GXX_WARNING_FLAGS}) + list(APPEND _args "-std=c++11 ") + if (CYGWIN) + list(APPEND _args "-U__STRICT_ANSI__") + endif() + list(APPEND _args "-x c++-header " ${_inc}) + list(APPEND _args -c ${_header_filename} -o ${_gch_filename}) + + get_directory_property(DIRINC INCLUDE_DIRECTORIES) + foreach(_inc ${DIRINC}) + list(APPEND _args "-I" ${_inc}) + endforeach(_inc ${DIRINC}) + + separate_arguments(_args) + + add_custom_command(OUTPUT ${_gch_filename} + COMMAND rm -f ${_gch_filename} + COMMAND ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_ARG1} ${_args} + DEPENDS ${_header_filename}) + endmacro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + + else() + macro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + endmacro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + endif() +else() + macro(ADD_PCH_RULE _header_filename _src_list _other_srcs) + endmacro(ADD_PCH_RULE _header_filename _src_list _other_srcs) +endif() + +if(PRECOMPILE_SYSTEM_HH) + add_pch_rule(${PROJECT_BINARY_DIR}/system.hh LEDGER_SOURCES LEDGER_CLI_SOURCES) +endif() + +include(GNUInstallDirs) + +if (BUILD_LIBRARY) + set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + add_library(libledger SHARED ${LEDGER_SOURCES}) + add_ledger_library_dependencies(libledger) + set_target_properties(libledger PROPERTIES + PREFIX "" + INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}" + VERSION ${Ledger_VERSION_MAJOR} + SOVERSION ${Ledger_VERSION_MAJOR}) + + add_executable(ledger main.cc global.cc) + target_link_libraries(ledger libledger) + if (CMAKE_SYSTEM_NAME STREQUAL Darwin AND HAVE_BOOST_PYTHON) + target_link_libraries(ledger ${PYTHON_LIBRARIES}) + endif() + + install(TARGETS libledger DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(FILES ${LEDGER_INCLUDES} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/ledger) +else() + add_executable(ledger ${LEDGER_SOURCES} main.cc global.cc) + add_ledger_library_dependencies(ledger) +endif() + +if (USE_PYTHON) + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c + "from __future__ import print_function +import distutils.sysconfig as s +print(s.get_python_lib(True, prefix=''))" + OUTPUT_VARIABLE _TMP_PYTHON_SITE_PACKAGES OUTPUT_STRIP_TRAILING_WHITESPACE) + set(PYTHON_SITE_PACKAGES ${_TMP_PYTHON_SITE_PACKAGES} + CACHE PATH "python module directory (${_TMP_PYTHON_SITE_PACKAGES})") + + if (PYTHON_SITE_PACKAGES) + if (WIN32 AND NOT CYGWIN) + set(_ledger_python_module_name "ledger.pyd") + elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(_ledger_python_module_name "ledger.so") + else() + set(_ledger_python_module_name "ledger${CMAKE_SHARED_LIBRARY_SUFFIX}") + endif() + + # FIXME: symlink would be sufficient: + # maybe using install(CODE "...") and + # execute_process(COMMAND "${CMAKE_COMMAND}" -E create_symlink ...). + # Windows will need a special case due to not supporting symlinks. + add_custom_command( + TARGET libledger POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ "${CMAKE_BINARY_DIR}/${_ledger_python_module_name}") + install( + FILES "${CMAKE_BINARY_DIR}/${_ledger_python_module_name}" + DESTINATION ${PYTHON_SITE_PACKAGES}) + else() + message(WARNING "PYTHON_SITE_PACKAGES not set. Will not install python module.") + endif() +endif() + +install(TARGETS ledger DESTINATION ${CMAKE_INSTALL_BINDIR}) + +### CMakeLists.txt ends here diff --git a/src/account.cc b/src/account.cc new file mode 100644 index 00000000..29c28866 --- /dev/null +++ b/src/account.cc @@ -0,0 +1,779 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "account.h" +#include "post.h" +#include "xact.h" + +namespace ledger { + +account_t::~account_t() +{ + TRACE_DTOR(account_t); + + foreach (accounts_map::value_type& pair, accounts) { + if (! pair.second->has_flags(ACCOUNT_TEMP) || + has_flags(ACCOUNT_TEMP)) { + checked_delete(pair.second); + } + } +} + +account_t * account_t::find_account(const string& acct_name, + const bool auto_create) +{ + accounts_map::const_iterator i = accounts.find(acct_name); + if (i != accounts.end()) + return (*i).second; + + char buf[8192]; + + string::size_type sep = acct_name.find(':'); + assert(sep < 256|| sep == string::npos); + + const char * first, * rest; + if (sep == string::npos) { + first = acct_name.c_str(); + rest = NULL; + } else { + std::strncpy(buf, acct_name.c_str(), sep); + buf[sep] = '\0'; + + first = buf; + rest = acct_name.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); + + // An account created within a temporary or generated account is itself + // temporary or generated, so that the whole tree has the same status. + if (has_flags(ACCOUNT_TEMP)) + account->add_flags(ACCOUNT_TEMP); + if (has_flags(ACCOUNT_GENERATED)) + account->add_flags(ACCOUNT_GENERATED); + +#if DEBUG_ON + std::pair result = +#endif + accounts.insert(accounts_map::value_type(first, account)); +#if DEBUG_ON + assert(result.second); +#endif + } else { + account = (*i).second; + } + + if (rest) + account = account->find_account(rest, auto_create); + + return account; +} + +namespace { + account_t * find_account_re_(account_t * account, const mask_t& regexp) + { + if (regexp.match(account->fullname())) + return account; + + foreach (accounts_map::value_type& pair, account->accounts) + if (account_t * a = find_account_re_(pair.second, regexp)) + return a; + + return NULL; + } +} + +account_t * account_t::find_account_re(const string& regexp) +{ + return find_account_re_(this, mask_t(regexp)); +} + +void account_t::add_post(post_t * post) +{ + posts.push_back(post); + + // Adding a new post changes the possible totals that may have been + // computed before. + if (xdata_) { + xdata_->self_details.gathered = false; + xdata_->self_details.calculated = false; + xdata_->family_details.gathered = false; + xdata_->family_details.calculated = false; + if (! xdata_->family_details.total.is_null()) { + xdata_->family_details.total = ledger::value_t(); + } + account_t *ancestor = this; + while (ancestor->parent) { + ancestor = ancestor->parent; + if (ancestor->has_xdata()) { + xdata_t &xdata = ancestor->xdata(); + xdata.family_details.gathered = false; + xdata.family_details.calculated = false; + xdata.family_details.total = ledger::value_t(); + } + } + } +} + +void account_t::add_deferred_post(const string& uuid, post_t * post) +{ + if (! deferred_posts) + deferred_posts = deferred_posts_map_t(); + + deferred_posts_map_t::iterator i = deferred_posts->find(uuid); + if (i == deferred_posts->end()) { + posts_list lst; + lst.push_back(post); + deferred_posts->insert(deferred_posts_map_t::value_type(uuid, lst)); + } else { + (*i).second.push_back(post); + } +} + +void account_t::apply_deferred_posts() +{ + if (deferred_posts) { + foreach (deferred_posts_map_t::value_type& pair, *deferred_posts) { + foreach (post_t * post, pair.second) + post->account->add_post(post); + } + deferred_posts = none; + } + + // Also apply in child accounts + foreach (const accounts_map::value_type& pair, accounts) + pair.second->apply_deferred_posts(); +} + +bool account_t::remove_post(post_t * post) +{ + // It's possible that 'post' wasn't yet in this account, but try to + // remove it anyway. This can happen if there is an error during + // parsing, when the posting knows what it's account is, but + // xact_t::finalize has not yet added that posting to the account. + posts.remove(post); + post->account = NULL; + return true; +} + +string account_t::fullname() const +{ + if (! _fullname.empty()) { + return _fullname; + } else { + const account_t * first = this; + string fullname = name; + + while (first->parent) { + first = first->parent; + if (! first->name.empty()) + fullname = first->name + ":" + fullname; + } + + _fullname = fullname; + + return fullname; + } +} + +string account_t::partial_name(bool flat) const +{ + string pname = name; + + for (const account_t * acct = parent; + acct && acct->parent; + acct = acct->parent) { + if (! flat) { + std::size_t count = acct->children_with_flags(ACCOUNT_EXT_TO_DISPLAY); + assert(count > 0); + if (count > 1 || acct->has_xflags(ACCOUNT_EXT_TO_DISPLAY)) + break; + } + pname = acct->name + ":" + pname; + } + return pname; +} + +std::ostream& operator<<(std::ostream& out, const account_t& account) +{ + out << account.fullname(); + return out; +} + +namespace { + value_t get_partial_name(call_scope_t& args) + { + return string_value(args.context() + .partial_name(args.has(0) && + args.get(0))); + } + + value_t get_account(call_scope_t& args) { // this gets the name + account_t& account(args.context()); + if (args.has(0)) { + account_t * acct = account.parent; + for (; acct && acct->parent; acct = acct->parent) ; + if (args[0].is_string()) + return scope_value(acct->find_account(args.get(0), false)); + else if (args[0].is_mask()) + return scope_value(acct->find_account_re(args.get(0).str())); + else + return NULL_VALUE; + } + else if (args.type_context() == value_t::SCOPE) { + return scope_value(&account); + } + else { + return string_value(account.fullname()); + } + } + + value_t get_account_base(account_t& account) { + return string_value(account.name); + } + + value_t get_amount(account_t& account) { + return SIMPLIFIED_VALUE_OR_ZERO(account.amount()); + } + + value_t get_total(account_t& account) { + return SIMPLIFIED_VALUE_OR_ZERO(account.total()); + } + + value_t get_subcount(account_t& account) { + return long(account.self_details().posts_count); + } + + value_t get_count(account_t& account) { + return long(account.family_details().posts_count); + } + value_t get_cost(account_t&) { + throw_(calc_error, _("An account does not have a 'cost' value")); + return false; + } + + value_t get_depth(account_t& account) { + return long(account.depth); + } + + value_t get_note(account_t& account) { + return account.note ? string_value(*account.note) : NULL_VALUE; + } + + value_t ignore(account_t&) { + return false; + } + + value_t get_true(account_t&) { + return true; + } + + value_t get_addr(account_t& account) { + return long(&account); + } + + value_t get_depth_parent(account_t& account) + { + std::size_t depth = 0; + for (const account_t * acct = account.parent; + acct && acct->parent; + acct = acct->parent) { + std::size_t count = acct->children_with_flags(ACCOUNT_EXT_TO_DISPLAY); + assert(count > 0); + if (count > 1 || acct->has_xflags(ACCOUNT_EXT_TO_DISPLAY)) + depth++; + } + return long(depth); + } + + value_t get_depth_spacer(account_t& account) + { + std::size_t depth = 0; + for (const account_t * acct = account.parent; + acct && acct->parent; + acct = acct->parent) { + std::size_t count = acct->children_with_flags(ACCOUNT_EXT_TO_DISPLAY); + assert(count > 0); + if (count > 1 || acct->has_xflags(ACCOUNT_EXT_TO_DISPLAY)) + depth++; + } + + std::ostringstream out; + for (std::size_t i = 0; i < depth; i++) + out << " "; + + return string_value(out.str()); + } + + value_t get_latest_cleared(account_t& account) + { + return account.self_details().latest_cleared_post; + } + + value_t get_earliest(account_t& account) + { + return account.self_details().earliest_post; + } + value_t get_earliest_checkin(account_t& account) + { + return (! account.self_details().earliest_checkin.is_not_a_date_time() ? + value_t(account.self_details().earliest_checkin) : NULL_VALUE); + } + + value_t get_latest(account_t& account) + { + return account.self_details().latest_post; + } + value_t get_latest_checkout(account_t& account) + { + return (! account.self_details().latest_checkout.is_not_a_date_time() ? + value_t(account.self_details().latest_checkout) : NULL_VALUE); + } + value_t get_latest_checkout_cleared(account_t& account) + { + return account.self_details().latest_checkout_cleared; + } + + template + value_t get_wrapper(call_scope_t& args) { + return (*Func)(args.context()); + } + + value_t get_parent(account_t& account) { + return scope_value(account.parent); + } + + value_t fn_any(call_scope_t& args) + { + account_t& account(args.context()); + expr_t::ptr_op_t expr(args.get(0)); + + foreach (post_t * p, account.posts) { + bind_scope_t bound_scope(args, *p); + if (expr->calc(bound_scope, args.locus, args.depth).to_boolean()) + return true; + } + return false; + } + + value_t fn_all(call_scope_t& args) + { + account_t& account(args.context()); + expr_t::ptr_op_t expr(args.get(0)); + + foreach (post_t * p, account.posts) { + bind_scope_t bound_scope(args, *p); + if (! expr->calc(bound_scope, args.locus, args.depth).to_boolean()) + return false; + } + return true; + } +} + +expr_t::ptr_op_t account_t::lookup(const symbol_t::kind_t kind, + const string& fn_name) +{ + if (kind != symbol_t::FUNCTION) + return NULL; + + switch (fn_name[0]) { + case 'a': + if (fn_name[1] == '\0' || fn_name == "amount") + return WRAP_FUNCTOR(get_wrapper<&get_amount>); + else if (fn_name == "account") + return WRAP_FUNCTOR(&get_account); + else if (fn_name == "account_base") + return WRAP_FUNCTOR(get_wrapper<&get_account_base>); + else if (fn_name == "addr") + return WRAP_FUNCTOR(get_wrapper<&get_addr>); + else if (fn_name == "any") + return WRAP_FUNCTOR(&fn_any); + else if (fn_name == "all") + return WRAP_FUNCTOR(&fn_all); + break; + + case 'c': + if (fn_name == "count") + return WRAP_FUNCTOR(get_wrapper<&get_count>); + else if (fn_name == "cost") + return WRAP_FUNCTOR(get_wrapper<&get_cost>); + break; + + case 'd': + if (fn_name == "depth") + return WRAP_FUNCTOR(get_wrapper<&get_depth>); + else if (fn_name == "depth_parent") + return WRAP_FUNCTOR(get_wrapper<&get_depth_parent>); + else if (fn_name == "depth_spacer") + return WRAP_FUNCTOR(get_wrapper<&get_depth_spacer>); + break; + + case 'e': + if (fn_name == "earliest") + return WRAP_FUNCTOR(get_wrapper<&get_earliest>); + else if (fn_name == "earliest_checkin") + return WRAP_FUNCTOR(get_wrapper<&get_earliest_checkin>); + break; + + case 'i': + if (fn_name == "is_account") + return WRAP_FUNCTOR(get_wrapper<&get_true>); + else if (fn_name == "is_index") + return WRAP_FUNCTOR(get_wrapper<&get_subcount>); + break; + + case 'l': + if (fn_name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_depth>); + else if (fn_name == "latest_cleared") + return WRAP_FUNCTOR(get_wrapper<&get_latest_cleared>); + else if (fn_name == "latest") + return WRAP_FUNCTOR(get_wrapper<&get_latest>); + else if (fn_name == "latest_checkout") + return WRAP_FUNCTOR(get_wrapper<&get_latest_checkout>); + else if (fn_name == "latest_checkout_cleared") + return WRAP_FUNCTOR(get_wrapper<&get_latest_checkout_cleared>); + break; + + case 'n': + if (fn_name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_subcount>); + else if (fn_name == "note") + return WRAP_FUNCTOR(get_wrapper<&get_note>); + break; + + case 'p': + if (fn_name == "partial_account") + return WRAP_FUNCTOR(get_partial_name); + else if (fn_name == "parent") + return WRAP_FUNCTOR(get_wrapper<&get_parent>); + break; + + case 's': + if (fn_name == "subcount") + return WRAP_FUNCTOR(get_wrapper<&get_subcount>); + break; + + case 't': + if (fn_name == "total") + return WRAP_FUNCTOR(get_wrapper<&get_total>); + break; + + case 'u': + if (fn_name == "use_direct_amount") + return WRAP_FUNCTOR(get_wrapper<&ignore>); + break; + + case 'N': + if (fn_name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_count>); + break; + + case 'O': + if (fn_name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_total>); + break; + } + + return NULL; +} + +bool account_t::valid() const +{ + if (depth > 256) { + DEBUG("ledger.validate", "account_t: depth > 256"); + return false; + } + + foreach (const accounts_map::value_type& pair, accounts) { + if (this == pair.second) { + DEBUG("ledger.validate", "account_t: parent refers to itself!"); + return false; + } + + if (! pair.second->valid()) { + DEBUG("ledger.validate", "account_t: child not valid"); + return false; + } + } + + return true; +} + +bool account_t::children_with_xdata() const +{ + foreach (const accounts_map::value_type& pair, accounts) + if (pair.second->has_xdata() || + pair.second->children_with_xdata()) + return true; + + return false; +} + +std::size_t account_t::children_with_flags(xdata_t::flags_t flags) const +{ + std::size_t count = 0; + bool grandchildren_visited = false; + + foreach (const accounts_map::value_type& pair, accounts) + if (pair.second->has_xflags(flags) || + pair.second->children_with_flags(flags)) + count++; + + // Although no immediately children were visited, if any progeny at all were + // visited, it counts as one. + if (count == 0 && grandchildren_visited) + count = 1; + + return count; +} + +account_t::xdata_t::details_t& +account_t::xdata_t::details_t::operator+=(const details_t& other) +{ + posts_count += other.posts_count; + posts_virtuals_count += other.posts_virtuals_count; + posts_cleared_count += other.posts_cleared_count; + posts_last_7_count += other.posts_last_7_count; + posts_last_30_count += other.posts_last_30_count; + posts_this_month_count += other.posts_this_month_count; + + if (! is_valid(earliest_post) || + (is_valid(other.earliest_post) && + other.earliest_post < earliest_post)) + earliest_post = other.earliest_post; + if (! is_valid(earliest_cleared_post) || + (is_valid(other.earliest_cleared_post) && + other.earliest_cleared_post < earliest_cleared_post)) + earliest_cleared_post = other.earliest_cleared_post; + + if (! is_valid(latest_post) || + (is_valid(other.latest_post) && + other.latest_post > latest_post)) + latest_post = other.latest_post; + if (! is_valid(latest_cleared_post) || + (is_valid(other.latest_cleared_post) && + other.latest_cleared_post > latest_cleared_post)) + latest_cleared_post = other.latest_cleared_post; + + filenames.insert(other.filenames.begin(), other.filenames.end()); + accounts_referenced.insert(other.accounts_referenced.begin(), + other.accounts_referenced.end()); + payees_referenced.insert(other.payees_referenced.begin(), + other.payees_referenced.end()); + return *this; +} + +void account_t::clear_xdata() +{ + xdata_ = none; + + foreach (accounts_map::value_type& pair, accounts) + if (! pair.second->has_flags(ACCOUNT_TEMP)) + pair.second->clear_xdata(); +} + +value_t account_t::amount(const optional& expr) const +{ + if (xdata_ && xdata_->has_flags(ACCOUNT_EXT_VISITED)) { + posts_list::const_iterator i; + if (xdata_->self_details.last_post) + i = *xdata_->self_details.last_post; + else + i = posts.begin(); + + for (; i != posts.end(); i++) { + if ((*i)->xdata().has_flags(POST_EXT_VISITED)) { + if (! (*i)->xdata().has_flags(POST_EXT_CONSIDERED)) { + (*i)->add_to_value(xdata_->self_details.total, expr); + (*i)->xdata().add_flags(POST_EXT_CONSIDERED); + } + } + xdata_->self_details.last_post = i; + } + + if (xdata_->self_details.last_reported_post) + i = *xdata_->self_details.last_reported_post; + else + i = xdata_->reported_posts.begin(); + + for (; i != xdata_->reported_posts.end(); i++) { + if ((*i)->xdata().has_flags(POST_EXT_VISITED)) { + if (! (*i)->xdata().has_flags(POST_EXT_CONSIDERED)) { + (*i)->add_to_value(xdata_->self_details.total, expr); + (*i)->xdata().add_flags(POST_EXT_CONSIDERED); + } + } + xdata_->self_details.last_reported_post = i; + } + + return xdata_->self_details.total; + } else { + return NULL_VALUE; + } +} + +value_t account_t::total(const optional& expr) const +{ + if (! (xdata_ && xdata_->family_details.calculated)) { + const_cast(*this).xdata().family_details.calculated = true; + + value_t temp; + foreach (const accounts_map::value_type& pair, accounts) { + temp = pair.second->total(expr); + if (! temp.is_null()) + add_or_set_value(xdata_->family_details.total, temp); + } + + temp = amount(expr); + if (! temp.is_null()) + add_or_set_value(xdata_->family_details.total, temp); + } + return xdata_->family_details.total; +} + +const account_t::xdata_t::details_t& +account_t::self_details(bool gather_all) const +{ + if (! (xdata_ && xdata_->self_details.gathered)) { + const_cast(*this).xdata().self_details.gathered = true; + + foreach (const post_t * post, posts) + xdata_->self_details.update(const_cast(*post), gather_all); + } + return xdata_->self_details; +} + +const account_t::xdata_t::details_t& +account_t::family_details(bool gather_all) const +{ + if (! (xdata_ && xdata_->family_details.gathered)) { + const_cast(*this).xdata().family_details.gathered = true; + + foreach (const accounts_map::value_type& pair, accounts) + xdata_->family_details += pair.second->family_details(gather_all); + + xdata_->family_details += self_details(gather_all); + } + return xdata_->family_details; +} + +void account_t::xdata_t::details_t::update(post_t& post, + bool gather_all) +{ + posts_count++; + + if (post.has_flags(POST_VIRTUAL)) + posts_virtuals_count++; + + if (gather_all && post.pos) + filenames.insert(post.pos->pathname); + + date_t date = post.date(); + + if (date.year() == CURRENT_DATE().year() && + date.month() == CURRENT_DATE().month()) + posts_this_month_count++; + + if ((CURRENT_DATE() - date).days() <= 30) + posts_last_30_count++; + if ((CURRENT_DATE() - date).days() <= 7) + posts_last_7_count++; + + if (! is_valid(earliest_post) || post.date() < earliest_post) + earliest_post = post.date(); + if (! is_valid(latest_post) || post.date() > latest_post) + latest_post = post.date(); + + if (post.checkin && (earliest_checkin.is_not_a_date_time() || + *post.checkin < earliest_checkin)) + earliest_checkin = *post.checkin; + + if (post.checkout && (latest_checkout.is_not_a_date_time() || + *post.checkout > latest_checkout)) { + latest_checkout = *post.checkout; + latest_checkout_cleared = post.state() == item_t::CLEARED; + } + + if (post.state() == item_t::CLEARED) { + posts_cleared_count++; + + if (! is_valid(earliest_cleared_post) || + post.date() < earliest_cleared_post) + earliest_cleared_post = post.date(); + if (! is_valid(latest_cleared_post) || + post.date() > latest_cleared_post) + latest_cleared_post = post.date(); + } + + if (gather_all) { + accounts_referenced.insert(post.account->fullname()); + payees_referenced.insert(post.payee()); + } +} + +void put_account(property_tree::ptree& st, const account_t& acct, + function pred) +{ + if (pred(acct)) { + std::ostringstream buf; + buf.width(sizeof(unsigned long) * 2); + buf.fill('0'); + buf << std::hex << reinterpret_cast(&acct); + + st.put(".id", buf.str()); + + st.put("name", acct.name); + st.put("fullname", acct.fullname()); + + value_t total = acct.amount(); + if (! total.is_null()) + put_value(st.put("account-amount", ""), total); + + total = acct.total(); + if (! total.is_null()) + put_value(st.put("account-total", ""), total); + + foreach (const accounts_map::value_type& pair, acct.accounts) + put_account(st.add("account", ""), *pair.second, pred); + } +} + +} // namespace ledger diff --git a/src/account.h b/src/account.h new file mode 100644 index 00000000..0abbd87a --- /dev/null +++ b/src/account.h @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup data + */ + +/** + * @file account.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _ACCOUNT_H +#define _ACCOUNT_H + +#include "scope.h" + +namespace ledger { + +class account_t; +class xact_t; +class post_t; + +typedef std::list posts_list; +typedef std::map accounts_map; +typedef std::map deferred_posts_map_t; + +class account_t : public supports_flags<>, public scope_t +{ +#define ACCOUNT_NORMAL 0x00 // no flags at all, a basic account +#define ACCOUNT_KNOWN 0x01 +#define ACCOUNT_TEMP 0x02 // account is a temporary object +#define ACCOUNT_GENERATED 0x04 // account never actually existed + +public: + account_t * parent; + string name; + optional note; + unsigned short depth; + accounts_map accounts; + posts_list posts; + optional deferred_posts; + optional value_expr; + + mutable string _fullname; +#if DOCUMENT_MODEL + mutable void * data; +#endif + + account_t(account_t * _parent = NULL, + const string& _name = "", + const optional& _note = none) + : supports_flags<>(), scope_t(), parent(_parent), + name(_name), note(_note), + depth(static_cast(parent ? parent->depth + 1 : 0)) +#if DOCUMENT_MODEL + , data(NULL) +#endif + { + TRACE_CTOR(account_t, "account_t *, const string&, const string&"); + } + account_t(const account_t& other) + : supports_flags<>(other.flags()), scope_t(), + parent(other.parent), + name(other.name), + note(other.note), + depth(other.depth), + accounts(other.accounts) +#if DOCUMENT_MODEL + , data(NULL) +#endif + { + TRACE_CTOR(account_t, "copy"); + } + virtual ~account_t(); + + virtual string description() { + return string(_("account ")) + fullname(); + } + + operator string() const { + return fullname(); + } + string fullname() const; + string partial_name(bool flat = false) const; + + void add_account(account_t * acct) { + accounts.insert(accounts_map::value_type(acct->name, acct)); + } + bool remove_account(account_t * acct) { + accounts_map::size_type n = accounts.erase(acct->name); + return n > 0; + } + + account_t * find_account(const string& name, bool auto_create = true); + account_t * find_account_re(const string& regexp); + + typedef transform_iterator, + accounts_map::iterator> + accounts_map_seconds_iterator; + + accounts_map_seconds_iterator accounts_begin() { + return make_transform_iterator + (accounts.begin(), boost::bind(&accounts_map::value_type::second, _1)); + } + accounts_map_seconds_iterator accounts_end() { + return make_transform_iterator + (accounts.end(), boost::bind(&accounts_map::value_type::second, _1)); + } + + void add_post(post_t * post); + void add_deferred_post(const string& uuid, post_t * post); + void apply_deferred_posts(); + bool remove_post(post_t * post); + + posts_list::iterator posts_begin() { + return posts.begin(); + } + posts_list::iterator posts_end() { + return posts.end(); + } + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + bool valid() const; + + friend class journal_t; + + struct xdata_t : public supports_flags<> + { +#define ACCOUNT_EXT_SORT_CALC 0x01 +#define ACCOUNT_EXT_HAS_NON_VIRTUALS 0x02 +#define ACCOUNT_EXT_HAS_UNB_VIRTUALS 0x04 +#define ACCOUNT_EXT_AUTO_VIRTUALIZE 0x08 +#define ACCOUNT_EXT_VISITED 0x10 +#define ACCOUNT_EXT_MATCHING 0x20 +#define ACCOUNT_EXT_TO_DISPLAY 0x40 +#define ACCOUNT_EXT_DISPLAYED 0x80 + + struct details_t + { + value_t total; + bool calculated; + bool gathered; + + std::size_t posts_count; + std::size_t posts_virtuals_count; + std::size_t posts_cleared_count; + std::size_t posts_last_7_count; + std::size_t posts_last_30_count; + std::size_t posts_this_month_count; + + date_t earliest_post; + date_t earliest_cleared_post; + date_t latest_post; + date_t latest_cleared_post; + + datetime_t earliest_checkin; + datetime_t latest_checkout; + bool latest_checkout_cleared; + + std::set filenames; + std::set accounts_referenced; + std::set payees_referenced; + + optional last_post; + optional last_reported_post; + + details_t() + : calculated(false), + gathered(false), + + posts_count(0), + posts_virtuals_count(0), + posts_cleared_count(0), + posts_last_7_count(0), + posts_last_30_count(0), + posts_this_month_count(0), + latest_checkout_cleared(false) + { + TRACE_CTOR(account_t::xdata_t::details_t, ""); + } + // A copy copies nothing + details_t(const details_t&) + : calculated(false), + gathered(false), + + posts_count(0), + posts_virtuals_count(0), + posts_cleared_count(0), + posts_last_7_count(0), + posts_last_30_count(0), + posts_this_month_count(0), + latest_checkout_cleared(false) + { + TRACE_CTOR(account_t::xdata_t::details_t, "copy"); + } + ~details_t() throw() { + TRACE_DTOR(account_t::xdata_t::details_t); + } + + details_t& operator+=(const details_t& other); + + void update(post_t& post, bool gather_all = false); + }; + + details_t self_details; + details_t family_details; + posts_list reported_posts; + + std::list sort_values; + + xdata_t() : supports_flags<>() + { + TRACE_CTOR(account_t::xdata_t, ""); + } + xdata_t(const xdata_t& other) + : supports_flags<>(other.flags()), + self_details(other.self_details), + family_details(other.family_details), + sort_values(other.sort_values) + { + TRACE_CTOR(account_t::xdata_t, "copy"); + } + ~xdata_t() throw() { + TRACE_DTOR(account_t::xdata_t); + } + }; + + // This variable holds optional "extended data" which is usually produced + // only during reporting, and only for the posting set being reported. + // It's a memory-saving measure to delay allocation until the last possible + // moment. + mutable optional xdata_; + + bool has_xdata() const { + return static_cast(xdata_); + } + void clear_xdata(); + xdata_t& xdata() { + if (! xdata_) + xdata_ = xdata_t(); + return *xdata_; + } + const xdata_t& xdata() const { + assert(xdata_); + return *xdata_; + } + + value_t amount(const optional& expr = none) const; + value_t total(const optional& expr = none) const; + + const xdata_t::details_t& self_details(bool gather_all = true) const; + const xdata_t::details_t& family_details(bool gather_all = true) const; + + bool has_xflags(xdata_t::flags_t flags) const { + return xdata_ && xdata_->has_flags(flags); + } + bool children_with_xdata() const; + std::size_t children_with_flags(xdata_t::flags_t flags) const; +}; + +std::ostream& operator<<(std::ostream& out, const account_t& account); + +void put_account(property_tree::ptree& pt, const account_t& acct, + function pred); + +//simple struct added to allow std::map to compare accounts in the accounts report +struct account_compare { + bool operator() (const account_t& lhs, const account_t& rhs) const { + return (lhs.fullname().compare(rhs.fullname()) < 0); + } +}; + +} // namespace ledger + +#endif // _ACCOUNT_H diff --git a/src/amount.cc b/src/amount.cc new file mode 100644 index 00000000..c6463b2b --- /dev/null +++ b/src/amount.cc @@ -0,0 +1,1324 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include +#include + +#include "amount.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +bool amount_t::stream_fullstrings = false; + +#if !defined(THREADSAFE) +// These global temporaries are pre-initialized for the sake of +// efficiency, and are reused over and over again. +static mpz_t temp; +static mpq_t tempq; +static mpfr_t tempf; +static mpfr_t tempfb; +static mpfr_t tempfnum; +static mpfr_t tempfden; +#endif + +struct amount_t::bigint_t : public supports_flags<> +{ +#define BIGINT_BULK_ALLOC 0x01 +#define BIGINT_KEEP_PREC 0x02 + + mpq_t val; + precision_t prec; + uint_least32_t refc; + +#define MP(bigint) ((bigint)->val) + + bigint_t() : prec(0), refc(1) { + mpq_init(val); + TRACE_CTOR(bigint_t, ""); + } + bigint_t(const bigint_t& other) + : supports_flags<>(static_cast + (other.flags() & ~BIGINT_BULK_ALLOC)), + prec(other.prec), refc(1) { + mpq_init(val); + mpq_set(val, other.val); + TRACE_CTOR(bigint_t, "copy"); + } + ~bigint_t() { + TRACE_DTOR(bigint_t); + assert(refc == 0); + mpq_clear(val); + } + + bool valid() const { + if (prec > 1024) { + DEBUG("ledger.validate", "amount_t::bigint_t: prec > 1024"); + return false; + } + if (flags() & ~(BIGINT_BULK_ALLOC | BIGINT_KEEP_PREC)) { + DEBUG("ledger.validate", + "amount_t::bigint_t: flags() & ~(BULK_ALLOC | KEEP_PREC)"); + return false; + } + return true; + } +}; + +bool amount_t::is_initialized = false; + +namespace { + void stream_out_mpq(std::ostream& out, + mpq_t quant, + amount_t::precision_t precision, + int zeros_prec = -1, + mpfr_rnd_t rnd = GMP_RNDN, + const optional& comm = none) + { + char * buf = NULL; + try { +#if DEBUG_ON + IF_DEBUG("amount.convert") { + char * tbuf = mpq_get_str(NULL, 10, quant); + DEBUG("amount.convert", "Rational to convert = " << tbuf); + std::free(tbuf); + } +#endif + + // Convert the rational number to a floating-point, extending the + // floating-point to a large enough size to get a precise answer. + + mp_prec_t num_prec = + static_cast(mpz_sizeinbase(mpq_numref(quant), 2)); + num_prec += amount_t::extend_by_digits*64; + if (num_prec < MPFR_PREC_MIN) + num_prec = MPFR_PREC_MIN; + DEBUG("amount.convert", "num prec = " << num_prec); + + mpfr_set_prec(tempfnum, num_prec); + mpfr_set_z(tempfnum, mpq_numref(quant), rnd); + + mp_prec_t den_prec = + static_cast(mpz_sizeinbase(mpq_denref(quant), 2)); + den_prec += amount_t::extend_by_digits*64; + if (den_prec < MPFR_PREC_MIN) + den_prec = MPFR_PREC_MIN; + DEBUG("amount.convert", "den prec = " << den_prec); + + mpfr_set_prec(tempfden, den_prec); + mpfr_set_z(tempfden, mpq_denref(quant), rnd); + + mpfr_set_prec(tempfb, num_prec + den_prec); + mpfr_div(tempfb, tempfnum, tempfden, rnd); + + if (mpfr_asprintf(&buf, "%.*RNf", precision, tempfb) < 0) + throw_(amount_error, + _("Cannot output amount to a floating-point representation")); + + DEBUG("amount.convert", "mpfr_print = " << buf + << " (precision " << precision + << ", zeros_prec " << zeros_prec << ")"); + + if (zeros_prec >= 0) { + string::size_type index = std::strlen(buf); + string::size_type point = 0; + for (string::size_type i = 0; i < index; i++) { + if (buf[i] == '.') { + point = i; + break; + } + } + if (point > 0) { + while (--index >= (point + 1 + static_cast(zeros_prec)) && + buf[index] == '0') + buf[index] = '\0'; + if (index >= (point + static_cast(zeros_prec)) && + buf[index] == '.') + buf[index] = '\0'; + } + } + + if (comm) { + int integer_digits = 0; + if (comm && comm->has_flags(COMMODITY_STYLE_THOUSANDS)) { + // Count the number of integer digits + for (const char * p = buf; *p; p++) { + if (*p == '.') + break; + else if (*p != '-') + integer_digits++; + } + } + + for (const char * p = buf; *p; p++) { + if (*p == '.') { + if (("h" == comm->symbol() || "m" == comm->symbol()) && (commodity_t::time_colon_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_TIME_COLON)))) + out << ':'; + else if (commodity_t::decimal_comma_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_DECIMAL_COMMA))) + out << ','; + else + out << *p; + assert(integer_digits <= 3); + } + else if (*p == '-') { + out << *p; + } + else { + out << *p; + + if (integer_digits > 3 && --integer_digits % 3 == 0) { + if (("h" == comm->symbol() || "m" == comm->symbol()) && (commodity_t::time_colon_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_TIME_COLON)))) + out << ':'; + else if (commodity_t::decimal_comma_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_DECIMAL_COMMA))) + out << '.'; + else + out << ','; + } + } + } + } else { + out << buf; + } + } + catch (...) { + if (buf != NULL) + mpfr_free_str(buf); + throw; + } + if (buf != NULL) + mpfr_free_str(buf); + } +} + +void amount_t::initialize() +{ + if (! is_initialized) { + mpz_init(temp); + mpq_init(tempq); + mpfr_init(tempf); + mpfr_init(tempfb); + mpfr_init(tempfnum); + mpfr_init(tempfden); + + commodity_pool_t::current_pool.reset(new commodity_pool_t); + + // Add time commodity conversions, so that timelog's may be parsed + // in terms of seconds, but reported as minutes or hours. + if (commodity_t * commodity = commodity_pool_t::current_pool->create("s")) + commodity->add_flags(COMMODITY_BUILTIN | COMMODITY_NOMARKET); +#if !NO_ASSERTS + else + assert(false); +#endif + + // Add a "percentile" commodity + if (commodity_t * commodity = commodity_pool_t::current_pool->create("%")) + commodity->add_flags(COMMODITY_BUILTIN | COMMODITY_NOMARKET); +#if !NO_ASSERTS + else + assert(false); +#endif + + is_initialized = true; + } +} + +void amount_t::shutdown() +{ + if (is_initialized) { + mpz_clear(temp); + mpq_clear(tempq); + mpfr_clear(tempf); + mpfr_clear(tempfb); + mpfr_clear(tempfnum); + mpfr_clear(tempfden); + + commodity_pool_t::current_pool.reset(); + + is_initialized = false; + } +} + +void amount_t::_copy(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (quantity != amt.quantity) { + if (quantity) + _release(); + + // Never maintain a pointer into a bulk allocation pool; such + // pointers are not guaranteed to remain. + if (amt.quantity->has_flags(BIGINT_BULK_ALLOC)) { + quantity = new bigint_t(*amt.quantity); + } else { + quantity = amt.quantity; + DEBUG("amount.refs", + quantity << " refc++, now " << (quantity->refc + 1)); + quantity->refc++; + } + } + commodity_ = amt.commodity_; + + VERIFY(valid()); +} + +void amount_t::_dup() +{ + VERIFY(valid()); + + if (quantity->refc > 1) { + bigint_t * q = new bigint_t(*quantity); + _release(); + quantity = q; + } + + VERIFY(valid()); +} + +void amount_t::_clear() +{ + if (quantity) { + _release(); + quantity = NULL; + commodity_ = NULL; + } else { + assert(! commodity_); + } +} + +void amount_t::_release() +{ + VERIFY(valid()); + + DEBUG("amount.refs", quantity << " refc--, now " << (quantity->refc - 1)); + + if (--quantity->refc == 0) { + if (quantity->has_flags(BIGINT_BULK_ALLOC)) + quantity->~bigint_t(); + else + checked_delete(quantity); + quantity = NULL; + commodity_ = NULL; + } + + VERIFY(valid()); +} + + +amount_t::amount_t(const double val) : commodity_(NULL) +{ + quantity = new bigint_t; + mpq_set_d(MP(quantity), val); + quantity->prec = extend_by_digits; // an approximation + TRACE_CTOR(amount_t, "const double"); +} + +amount_t::amount_t(const unsigned long val) : commodity_(NULL) +{ + quantity = new bigint_t; + mpq_set_ui(MP(quantity), val, 1); + TRACE_CTOR(amount_t, "const unsigned long"); +} + +amount_t::amount_t(const long val) : commodity_(NULL) +{ + quantity = new bigint_t; + mpq_set_si(MP(quantity), val, 1); + TRACE_CTOR(amount_t, "const long"); +} + + +amount_t& amount_t::operator=(const amount_t& amt) +{ + if (this != &amt) { + if (amt.quantity) + _copy(amt); + else if (quantity) + _clear(); + } + return *this; +} + + +int amount_t::compare(const amount_t& amt) const +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot compare an amount to an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot compare an uninitialized amount to an amount")); + else + throw_(amount_error, _("Cannot compare two uninitialized amounts")); + } + + if (has_commodity() && amt.has_commodity() && commodity() != amt.commodity()) { + throw_(amount_error, + _f("Cannot compare amounts with different commodities: '%1%' and '%2%'") + % commodity() % amt.commodity()); + } + + return mpq_cmp(MP(quantity), MP(amt.quantity)); +} + +bool amount_t::operator==(const amount_t& amt) const +{ + if ((quantity && ! amt.quantity) || (! quantity && amt.quantity)) + return false; + else if (! quantity && ! amt.quantity) + return true; + else if (commodity() != amt.commodity()) + return false; + + return mpq_equal(MP(quantity), MP(amt.quantity)); +} + + +amount_t& amount_t::operator+=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot add an uninitialized amount to an amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot add an amount to an uninitialized amount")); + else + throw_(amount_error, _("Cannot add two uninitialized amounts")); + } + + if (has_commodity() && amt.has_commodity() && commodity() != amt.commodity()) { + throw_(amount_error, + _f("Adding amounts with different commodities: '%1%' != '%2%'") + % commodity() % amt.commodity()); + } + + _dup(); + + mpq_add(MP(quantity), MP(quantity), MP(amt.quantity)); + + if (has_commodity() == amt.has_commodity()) + if (quantity->prec < amt.quantity->prec) + quantity->prec = amt.quantity->prec; + + return *this; +} + +amount_t& amount_t::operator-=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot subtract an amount from an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot subtract an uninitialized amount from an amount")); + else + throw_(amount_error, _("Cannot subtract two uninitialized amounts")); + } + + if (has_commodity() && amt.has_commodity() && commodity() != amt.commodity()) { + throw_(amount_error, + _f("Subtracting amounts with different commodities: '%1%' != '%2%'") + % commodity() % amt.commodity()); + } + + _dup(); + + mpq_sub(MP(quantity), MP(quantity), MP(amt.quantity)); + + if (has_commodity() == amt.has_commodity()) + if (quantity->prec < amt.quantity->prec) + quantity->prec = amt.quantity->prec; + + return *this; +} + +amount_t& amount_t::multiply(const amount_t& amt, bool ignore_commodity) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot multiply an amount by an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot multiply an uninitialized amount by an amount")); + else + throw_(amount_error, _("Cannot multiply two uninitialized amounts")); + } + + _dup(); + + mpq_mul(MP(quantity), MP(quantity), MP(amt.quantity)); + quantity->prec = + static_cast(quantity->prec + amt.quantity->prec); + + if (! has_commodity() && ! ignore_commodity) + commodity_ = amt.commodity_; + + if (has_commodity() && ! keep_precision()) { + precision_t comm_prec = commodity().precision(); + if (quantity->prec > comm_prec + extend_by_digits) + quantity->prec = static_cast(comm_prec + extend_by_digits); + } + + return *this; +} + +amount_t& amount_t::operator/=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot divide an amount by an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot divide an uninitialized amount by an amount")); + else + throw_(amount_error, _("Cannot divide two uninitialized amounts")); + } + + if (! amt) + throw_(amount_error, _("Divide by zero")); + + _dup(); + + // Increase the value's precision, to capture fractional parts after + // the divide. Round up in the last position. + + mpq_div(MP(quantity), MP(quantity), MP(amt.quantity)); + quantity->prec = + static_cast(quantity->prec + amt.quantity->prec + + extend_by_digits); + + if (! has_commodity()) + commodity_ = amt.commodity_; + + // If this amount has a commodity, and we're not dealing with plain + // numbers, or internal numbers (which keep full precision at all + // times), then round the number to within the commodity's precision + // plus six places. + + if (has_commodity() && ! keep_precision()) { + precision_t comm_prec = commodity().precision(); + if (quantity->prec > comm_prec + extend_by_digits) + quantity->prec = static_cast(comm_prec + extend_by_digits); + } + + return *this; +} + +amount_t::precision_t amount_t::precision() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine precision of an uninitialized amount")); + + return quantity->prec; +} + +bool amount_t::keep_precision() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine if precision of an uninitialized amount is kept")); + + return quantity->has_flags(BIGINT_KEEP_PREC); +} + +void amount_t::set_keep_precision(const bool keep) const +{ + if (! quantity) + throw_(amount_error, + _("Cannot set whether to keep the precision of an uninitialized amount")); + + if (keep) + quantity->add_flags(BIGINT_KEEP_PREC); + else + quantity->drop_flags(BIGINT_KEEP_PREC); +} + +amount_t::precision_t amount_t::display_precision() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine display precision of an uninitialized amount")); + + commodity_t& comm(commodity()); + + if (comm && ! keep_precision()) + return comm.precision(); + else + return comm ? std::max(quantity->prec, comm.precision()) : quantity->prec; +} + +void amount_t::in_place_negate() +{ + if (quantity) { + _dup(); + mpq_neg(MP(quantity), MP(quantity)); + } else { + throw_(amount_error, _("Cannot negate an uninitialized amount")); + } +} + +void amount_t::in_place_invert() +{ + if (! quantity) + throw_(amount_error, _("Cannot invert an uninitialized amount")); + + _dup(); + + if (sign() != 0) + mpq_inv(MP(quantity), MP(quantity)); +} + +void amount_t::in_place_round() +{ + if (! quantity) + throw_(amount_error, _("Cannot set rounding for an uninitialized amount")); + else if (! keep_precision()) + return; + + _dup(); + set_keep_precision(false); +} + +void amount_t::in_place_truncate() +{ +#if 1 + if (! quantity) + throw_(amount_error, _("Cannot truncate an uninitialized amount")); + + _dup(); + + DEBUG("amount.truncate", + "Truncating " << *this << " to precision " << display_precision()); + + std::ostringstream out; + stream_out_mpq(out, MP(quantity), display_precision()); + + scoped_array buf(new char [out.str().length() + 1]); + std::strcpy(buf.get(), out.str().c_str()); + + char * q = buf.get(); + for (char * p = q; *p != '\0'; p++, q++) { + if (*p == '.') p++; + if (p != q) *q = *p; + } + *q = '\0'; + + mpq_set_str(MP(quantity), buf.get(), 10); + + mpz_ui_pow_ui(temp, 10, display_precision()); + mpq_set_z(tempq, temp); + mpq_div(MP(quantity), MP(quantity), tempq); + + DEBUG("amount.truncate", "Truncated = " << *this); +#else + // This naive implementation is straightforward, but extremely inefficient + // as it requires parsing the commodity too, which might be fully annotated. + *this = amount_t(to_string()); +#endif +} + +void amount_t::in_place_floor() +{ + if (! quantity) + throw_(amount_error, _("Cannot compute floor on an uninitialized amount")); + + _dup(); + + mpz_fdiv_q(temp, mpq_numref(MP(quantity)), mpq_denref(MP(quantity))); + mpq_set_z(MP(quantity), temp); +} + +void amount_t::in_place_ceiling() +{ + if (! quantity) + throw_(amount_error, _("Cannot compute ceiling on an uninitialized amount")); + + _dup(); + + mpz_cdiv_q(temp, mpq_numref(MP(quantity)), mpq_denref(MP(quantity))); + mpq_set_z(MP(quantity), temp); +} + +void amount_t::in_place_roundto(int places) +{ + if (! quantity) + throw_(amount_error, _("Cannot round an uninitialized amount")); + double x=ceil(mpq_get_d(MP(quantity))*pow(10, places) - 0.49999999) / pow(10, places); + mpq_set_d(MP(quantity), x); +} + +void amount_t::in_place_unround() +{ + if (! quantity) + throw_(amount_error, _("Cannot unround an uninitialized amount")); + else if (keep_precision()) + return; + + _dup(); + + DEBUG("amount.unround", "Unrounding " << *this); + set_keep_precision(true); + DEBUG("amount.unround", "Unrounded = " << *this); +} + +void amount_t::in_place_reduce() +{ + if (! quantity) + throw_(amount_error, _("Cannot reduce an uninitialized amount")); + + while (commodity_ && commodity().smaller()) { + *this *= commodity().smaller()->number(); + commodity_ = commodity().smaller()->commodity_; + } +} + +void amount_t::in_place_unreduce() +{ + if (! quantity) + throw_(amount_error, _("Cannot unreduce an uninitialized amount")); + + amount_t tmp = *this; + commodity_t * comm = commodity_; + bool shifted = false; + + while (comm && comm->larger()) { + amount_t next_temp = tmp / comm->larger()->number(); + if (next_temp.abs() < amount_t(1L)) + break; + tmp = next_temp; + comm = comm->larger()->commodity_; + shifted = true; + } + + if (shifted) { + if (("h" == comm->symbol() || "m" == comm->symbol()) && commodity_t::time_colon_by_default) { + amount_t floored = tmp.floored(); + amount_t precision = tmp - floored; + if (precision < 0.0) { + precision += 1.0; + floored -= 1.0; + } + tmp = floored + (precision * (comm->smaller()->number() / 100.0)); + } + + *this = tmp; + commodity_ = comm; + } +} + +optional +amount_t::value(const datetime_t& moment, + const commodity_t * in_terms_of) const +{ + if (quantity) { +#if DEBUG_ON + DEBUG("commodity.price.find", + "amount_t::value of " << commodity().symbol()); + if (! moment.is_not_a_date_time()) + DEBUG("commodity.price.find", + "amount_t::value: moment = " << moment); + if (in_terms_of) + DEBUG("commodity.price.find", + "amount_t::value: in_terms_of = " << in_terms_of->symbol()); +#endif + if (has_commodity() && + (in_terms_of || ! commodity().has_flags(COMMODITY_PRIMARY))) { + optional point; + const commodity_t * comm(in_terms_of); + + if (has_annotation() && annotation().price) { + if (annotation().has_flags(ANNOTATION_PRICE_FIXATED)) { + point = price_point_t(); + point->price = *annotation().price; + DEBUG("commodity.prices.find", + "amount_t::value: fixated price = " << point->price); + } + else if (! comm) { + comm = annotation().price->commodity_ptr(); + } + } + + if (comm && commodity().referent() == comm->referent()) + return with_commodity(comm->referent()); + + if (! point) { + point = commodity().find_price(comm, moment); + + // Whether a price was found or not, check whether we should attempt + // to download a price from the Internet. This is done if (a) no + // price was found, or (b) the price is "stale" according to the + // setting of --price-exp. + if (point) + point = commodity().check_for_updated_price(point, moment, comm); + } + + if (point) { + amount_t price(point->price); + price.multiply(*this, true); + price.in_place_round(); + return price; + } + } + } else { + throw_(amount_error, + _("Cannot determine value of an uninitialized amount")); + } + return none; +} + +optional amount_t::price() const +{ + if (has_annotation() && annotation().price) { + amount_t tmp(*annotation().price); + tmp *= *this; + DEBUG("amount.price", "Returning price of " << *this << " = " << tmp); + return tmp; + } + return none; +} + + +int amount_t::sign() const +{ + if (! quantity) + throw_(amount_error, _("Cannot determine sign of an uninitialized amount")); + + return mpq_sgn(MP(quantity)); +} + +bool amount_t::is_zero() const +{ + if (! quantity) + throw_(amount_error, _("Cannot determine if an uninitialized amount is zero")); + + if (has_commodity()) { + if (keep_precision() || quantity->prec <= commodity().precision()) { + return is_realzero(); + } + else if (is_realzero()) { + return true; + } + else if (mpz_cmp(mpq_numref(MP(quantity)), + mpq_denref(MP(quantity))) > 0) { + DEBUG("amount.is_zero", "Numerator is larger than the denominator"); + return false; + } + else { + DEBUG("amount.is_zero", "We have to print the number to check for zero"); + + std::ostringstream out; + stream_out_mpq(out, MP(quantity), commodity().precision()); + + string output = out.str(); + if (! output.empty()) { + for (const char * p = output.c_str(); *p; p++) + if (*p != '0' && *p != '.' && *p != '-') + return false; + } + return true; + } + } + return is_realzero(); +} + + +double amount_t::to_double() const +{ + if (! quantity) + throw_(amount_error, _("Cannot convert an uninitialized amount to a double")); + + mpfr_set_q(tempf, MP(quantity), GMP_RNDN); + return mpfr_get_d(tempf, GMP_RNDN); +} + +long amount_t::to_long() const +{ + if (! quantity) + throw_(amount_error, _("Cannot convert an uninitialized amount to a long")); + + mpfr_set_q(tempf, MP(quantity), GMP_RNDN); + return mpfr_get_si(tempf, GMP_RNDN); +} + +bool amount_t::fits_in_long() const +{ + mpfr_set_q(tempf, MP(quantity), GMP_RNDN); + return mpfr_fits_slong_p(tempf, GMP_RNDN); +} + +commodity_t * amount_t::commodity_ptr() const +{ + return (commodity_ ? + commodity_ : commodity_pool_t::current_pool->null_commodity); +} + +bool amount_t::has_commodity() const +{ + return commodity_ && commodity_ != commodity_->pool().null_commodity; +} + +void amount_t::annotate(const annotation_t& details) +{ + commodity_t * this_base; + annotated_commodity_t * this_ann = NULL; + + if (! quantity) + throw_(amount_error, _("Cannot annotate the commodity of an uninitialized amount")); + else if (! has_commodity()) + return; // ignore attempt to annotate a "bare commodity + + if (commodity().has_annotation()) { + this_ann = &as_annotated_commodity(commodity()); + this_base = &this_ann->referent(); + } else { + this_base = &commodity(); + } + assert(this_base); + + DEBUG("amount.commodities", "Annotating commodity for amount " + << *this << std::endl << details); + + if (commodity_t * ann_comm = + this_base->pool().find_or_create(*this_base, details)) + set_commodity(*ann_comm); +#if !NO_ASSERTS + else + assert(false); +#endif + + DEBUG("amount.commodities", "Annotated amount is " << *this); +} + +bool amount_t::has_annotation() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine if an uninitialized amount's commodity is annotated")); + + assert(! has_commodity() || ! commodity().has_annotation() || + as_annotated_commodity(commodity()).details); + return has_commodity() && commodity().has_annotation(); +} + +annotation_t& amount_t::annotation() +{ + if (! quantity) + throw_(amount_error, + _("Cannot return commodity annotation details of an uninitialized amount")); + + if (! commodity().has_annotation()) + throw_(amount_error, + _("Request for annotation details from an unannotated amount")); + + annotated_commodity_t& ann_comm(as_annotated_commodity(commodity())); + return ann_comm.details; +} + +amount_t amount_t::strip_annotations(const keep_details_t& what_to_keep) const +{ + if (! quantity) + throw_(amount_error, + _("Cannot strip commodity annotations from an uninitialized amount")); + + if (! what_to_keep.keep_all(commodity())) { + amount_t t(*this); + t.set_commodity(commodity().strip_annotations(what_to_keep)); + return t; + } + return *this; +} + + +namespace { + void parse_quantity(std::istream& in, string& value) + { + char buf[256]; + char c = peek_next_nonws(in); + READ_INTO(in, buf, 255, c, + std::isdigit(c) || c == '-' || c == '.' || c == ','); + + string::size_type len = std::strlen(buf); + while (len > 0 && ! std::isdigit(buf[len - 1])) { + buf[--len] = '\0'; + in.unget(); + } + + value = buf; + } +} + +bool amount_t::parse(std::istream& in, const parse_flags_t& flags) +{ + // The possible syntax for an amount is: + // + // [-]NUM[ ]SYM [@ AMOUNT] + // SYM[ ][-]NUM [@ AMOUNT] + + string symbol; + string quant; + annotation_t details; + bool negative = false; + + commodity_t::flags_t comm_flags = COMMODITY_STYLE_DEFAULTS; + + char c = peek_next_nonws(in); + if (c == '-') { + negative = true; + in.get(c); + c = peek_next_nonws(in); + } + + char n; + if (std::isdigit(c)) { + parse_quantity(in, quant); + + if (! in.eof() && ((n = static_cast(in.peek())) != '\n')) { + if (std::isspace(n)) + comm_flags |= COMMODITY_STYLE_SEPARATED; + + commodity_t::parse_symbol(in, symbol); + + if (! symbol.empty()) + comm_flags |= COMMODITY_STYLE_SUFFIXED; + + if (! flags.has_flags(PARSE_NO_ANNOT) && + ! in.eof() && ((n = static_cast(in.peek())) != '\n')) + details.parse(in); + } + } else { + commodity_t::parse_symbol(in, symbol); + + if (! in.eof() && ((n = static_cast(in.peek())) != '\n')) { + if (std::isspace(static_cast(in.peek()))) + comm_flags |= COMMODITY_STYLE_SEPARATED; + + parse_quantity(in, quant); + + if (! flags.has_flags(PARSE_NO_ANNOT) && ! quant.empty() && + ! in.eof() && ((n = static_cast(in.peek())) != '\n')) + details.parse(in); + } + } + + if (quant.empty()) { + if (flags.has_flags(PARSE_SOFT_FAIL)) + return false; + else + throw_(amount_error, _("No quantity specified for amount")); + } + + // Allocate memory for the amount's quantity value. We have to + // monitor the allocation in a unique_ptr because this function gets + // called sometimes from amount_t's constructor; and if there is an + // exeception thrown by any of the function calls after this point, + // the destructor will never be called and the memory never freed. + + unique_ptr new_quantity; + + if (quantity) { + if (quantity->refc > 1) + _release(); + else + new_quantity.reset(quantity); + quantity = NULL; + } + + if (! new_quantity.get()) + new_quantity.reset(new bigint_t); + + // No one is holding a reference to this now. + new_quantity->refc--; + + // Create the commodity if has not already been seen, and update the + // precision if something greater was used for the quantity. + + if (symbol.empty()) { + commodity_ = NULL; + } else { + commodity_ = commodity_pool_t::current_pool->find(symbol); + if (! commodity_) + commodity_ = commodity_pool_t::current_pool->create(symbol); + assert(commodity_); + } + + // Quickly scan through and verify the correctness of the amount's use of + // punctuation. + + precision_t decimal_offset = 0; + string::size_type string_index = quant.length(); + string::size_type last_comma = string::npos; + string::size_type last_period = string::npos; + + bool no_more_commas = false; + bool no_more_periods = false; + bool no_migrate_style + = commodity().has_flags(COMMODITY_STYLE_NO_MIGRATE); + bool decimal_comma_style + = (commodity_t::decimal_comma_by_default || + commodity().has_flags(COMMODITY_STYLE_DECIMAL_COMMA)); +#if 0 + bool time_colon_style + = (commodity_t::time_colon_by_default || + commodity().has_flags(COMMODITY_STYLE_TIME_COLON)); +#endif + + new_quantity->prec = 0; + + BOOST_REVERSE_FOREACH (const char& ch, quant) { + string_index--; + + if (ch == '.') { + if (no_more_periods) + throw_(amount_error, _("Too many periods in amount")); + + if (decimal_comma_style) { + if (decimal_offset % 3 != 0) + throw_(amount_error, _("Incorrect use of thousand-mark period")); + comm_flags |= COMMODITY_STYLE_THOUSANDS; + no_more_commas = true; + } else { + if (last_comma != string::npos) { + decimal_comma_style = true; + if (decimal_offset % 3 != 0) + throw_(amount_error, _("Incorrect use of thousand-mark period")); + } else { + no_more_periods = true; + new_quantity->prec = decimal_offset; + decimal_offset = 0; + } + } + + if (last_period == string::npos) + last_period = string_index; + } + else if (ch == ',') { + if (no_more_commas) + throw_(amount_error, _("Too many commas in amount")); + + if (decimal_comma_style) { + if (last_period != string::npos) { + throw_(amount_error, _("Incorrect use of decimal comma")); + } else { + no_more_commas = true; + new_quantity->prec = decimal_offset; + decimal_offset = 0; + } + } else { + if (decimal_offset % 3 != 0) { + if (last_comma != string::npos || + last_period != string::npos) { + throw_(amount_error, _("Incorrect use of thousand-mark comma")); + } else { + decimal_comma_style = true; + no_more_commas = true; + new_quantity->prec = decimal_offset; + decimal_offset = 0; + } + } else { + comm_flags |= COMMODITY_STYLE_THOUSANDS; + no_more_periods = true; + } + } + + if (last_comma == string::npos) + last_comma = string_index; + } + else { + decimal_offset++; + } + } + + if (decimal_comma_style) + comm_flags |= COMMODITY_STYLE_DECIMAL_COMMA; + + if (flags.has_flags(PARSE_NO_MIGRATE)) { + // Can't call set_keep_precision here, because it assumes that `quantity' + // is non-NULL. + new_quantity->add_flags(BIGINT_KEEP_PREC); + } + else if (commodity_ && ! no_migrate_style) { + commodity().add_flags(comm_flags); + + if (new_quantity->prec > commodity().precision()) + commodity().set_precision(new_quantity->prec); + } + + // Now we have the final number. Remove commas and periods, if necessary. + + if (last_comma != string::npos || last_period != string::npos) { + string::size_type len = quant.length(); + scoped_array buf(new char[len + 1]); + const char * p = quant.c_str(); + char * t = buf.get(); + + while (*p) { + if (*p == ',' || *p == '.') + p++; + *t++ = *p++; + } + *t = '\0'; + + mpq_set_str(MP(new_quantity.get()), buf.get(), 10); + mpz_ui_pow_ui(temp, 10, new_quantity->prec); + mpq_set_z(tempq, temp); + mpq_div(MP(new_quantity.get()), MP(new_quantity.get()), tempq); + + IF_DEBUG("amount.parse") { + char * amt_buf = mpq_get_str(NULL, 10, MP(new_quantity.get())); + DEBUG("amount.parse", "Rational parsed = " << amt_buf); + std::free(amt_buf); + } + } else { + mpq_set_str(MP(new_quantity.get()), quant.c_str(), 10); + } + + if (negative) + mpq_neg(MP(new_quantity.get()), MP(new_quantity.get())); + + new_quantity->refc++; + quantity = new_quantity.release(); + + if (! flags.has_flags(PARSE_NO_REDUCE)) + in_place_reduce(); // will not throw an exception + + if (commodity_ && details) { + if (details.has_flags(ANNOTATION_PRICE_NOT_PER_UNIT)) { + assert(details.price); + *details.price /= this->abs(); + } + set_commodity(*commodity_pool_t::current_pool->find_or_create(*commodity_, details)); + } + + VERIFY(valid()); + + return true; +} + +void amount_t::parse_conversion(const string& larger_str, + const string& smaller_str) +{ + amount_t larger, smaller; + + larger.parse(larger_str, PARSE_NO_REDUCE); + smaller.parse(smaller_str, PARSE_NO_REDUCE); + + larger *= smaller.number(); + + if (larger.commodity()) { + larger.commodity().set_smaller(smaller); + larger.commodity().add_flags(smaller.commodity().flags() | + COMMODITY_NOMARKET); + } + if (smaller.commodity()) + smaller.commodity().set_larger(larger); +} + +void amount_t::print(std::ostream& _out, const uint_least8_t flags) const +{ + VERIFY(valid()); + + if (! quantity) { + _out << ""; + return; + } + + std::ostringstream out; + + commodity_t& comm(commodity()); + + if (! comm.has_flags(COMMODITY_STYLE_SUFFIXED)) { + comm.print(out, flags & AMOUNT_PRINT_ELIDE_COMMODITY_QUOTES); + if (comm.has_flags(COMMODITY_STYLE_SEPARATED)) + out << " "; + } + + stream_out_mpq(out, MP(quantity), display_precision(), + comm ? commodity().precision() : 0, GMP_RNDN, comm); + + if (comm.has_flags(COMMODITY_STYLE_SUFFIXED)) { + if (comm.has_flags(COMMODITY_STYLE_SEPARATED)) + out << " "; + comm.print(out, flags & AMOUNT_PRINT_ELIDE_COMMODITY_QUOTES); + } + + // If there are any annotations associated with this commodity, output them + // now. + comm.write_annotations(out, flags & AMOUNT_PRINT_NO_COMPUTED_ANNOTATIONS); + + // Things are output to a string first, so that if anyone has specified a + // width or fill for _out, it will be applied to the entire amount string, + // and not just the first part. + _out << out.str(); +} + +bool amount_t::valid() const +{ + if (quantity) { + if (! quantity->valid()) { + DEBUG("ledger.validate", "amount_t: ! quantity->valid()"); + return false; + } + + if (quantity->refc == 0) { + DEBUG("ledger.validate", "amount_t: quantity->refc == 0"); + return false; + } + } + else if (commodity_) { + DEBUG("ledger.validate", "amount_t: commodity_ != NULL"); + return false; + } + return true; +} + +void put_amount(property_tree::ptree& st, const amount_t& amt, + bool commodity_details) +{ + if (amt.has_commodity()) + put_commodity(st.put("commodity", ""), amt.commodity(), commodity_details); + + st.put("quantity", amt.quantity_string()); +} + +} // namespace ledger diff --git a/src/amount.h b/src/amount.h new file mode 100644 index 00000000..fed9a467 --- /dev/null +++ b/src/amount.h @@ -0,0 +1,790 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @defgroup math Mathematical objects + */ + +/** + * @file amount.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Basic type for handling commoditized math: amount_t + * + * An amount is the most basic numerical type in Ledger, and relies on + * commodity.h to represent commoditized amounts, which allows Ledger to + * handle mathematical expressions involving disparate commodities. + * + * Amounts can be of virtually infinite size and precision. When + * division or multiplication is performed, the precision is + * automatically expanded to include as many extra digits as necessary + * to avoid losing information. + */ +#ifndef _AMOUNT_H +#define _AMOUNT_H + +#include "utils.h" +#include "times.h" +#include "flags.h" + +namespace ledger { + +class commodity_t; +struct annotation_t; +struct keep_details_t; + +DECLARE_EXCEPTION(amount_error, std::runtime_error); + +enum parse_flags_enum_t { + PARSE_DEFAULT = 0x00, + PARSE_PARTIAL = 0x01, + PARSE_SINGLE = 0x02, + PARSE_NO_MIGRATE = 0x04, + PARSE_NO_REDUCE = 0x08, + PARSE_NO_ASSIGN = 0x10, + PARSE_NO_ANNOT = 0x20, + PARSE_OP_CONTEXT = 0x40, + PARSE_SOFT_FAIL = 0x80 +}; + +typedef basic_flags_t parse_flags_t; + +/** + * @brief Encapsulate infinite-precision commoditized amounts + * + * Used to represent commoditized infinite-precision numbers, and + * uncommoditized, plain numbers. In the commoditized case, commodities + * keep track of how they are used, and are always displayed back to the + * user after the same fashion. For uncommoditized numbers, no display + * truncation is ever done. In both cases, internal precision is always + * kept to an excessive degree. + */ +class amount_t + : public ordered_field_operators > > > +{ +public: + /** Ready the amount subsystem for use. + @note Normally called by session_t::initialize(). */ + static void initialize(); + /** Shutdown the amount subsystem and free all resources. + @note Normally called by session_t::shutdown(). */ + static void shutdown(); + + static bool is_initialized; + + /** The amount's decimal precision. */ + typedef uint_least16_t precision_t; + + /** Number of places of precision by which values are extended to + avoid losing precision during division and multiplication. */ + static const std::size_t extend_by_digits = 6U; + + /** If amounts should be streamed using to_fullstring() rather than + to_string(), so that complete precision is always displayed no matter + what the precision of an individual commodity may be. */ + static bool stream_fullstrings; + +protected: + void _copy(const amount_t& amt); + void _dup(); + void _clear(); + void _release(); + + struct bigint_t; + + bigint_t * quantity; + commodity_t * commodity_; + +public: + /** @name Constructors + @{ */ + + /** Creates a value for which is_null() is true, and which has no + value or commodity. If used in a value expression it evaluates to + zero, and its commodity equals \c commodity_t::null_commodity. */ + amount_t() : quantity(NULL), commodity_(NULL) { + TRACE_CTOR(amount_t, ""); + } + + /** Convert a double to an amount. As much precision as possible is + decoded from the binary floating point number. */ + amount_t(const double val); + + /** Convert an unsigned long to an amount. It's precision is zero. */ + amount_t(const unsigned long val); + + /** Convert a long to an amount. It's precision is zero, and the sign + is preserved. */ + amount_t(const long val); + + /** Parse a string as an (optionally commoditized) amount. If no + commodity is present, the resulting commodity is \c + commodity_t::null_commodity. The number may be of infinite + precision. */ + explicit amount_t(const string& val) : quantity(NULL) { + parse(val); + TRACE_CTOR(amount_t, "const string&"); + } + /** Parse a pointer to a C string as an (optionally commoditized) + amount. If no commodity is present, the resulting commodity is \c + commodity_t::null_commodity. The number may be of infinite + precision. */ + explicit amount_t(const char * val) : quantity(NULL) { + assert(val); + parse(val); + TRACE_CTOR(amount_t, "const char *"); + } + + /*@}*/ + + /** Create an amount whose display precision is never truncated, even + if the amount uses a commodity (which normally causes "round on + streaming" to occur). This function is mostly used by debugging + code and unit tests. This is the proper way to specify \c + $100.005, where display of the extra digit precision is required. + If a regular constructor were used, the amount would stream as \c + $100.01, even though its internal value equals \c $100.005. */ + static amount_t exact(const string& value); + + /** Release the reference count held for the underlying \c + amount_t::bigint_t object. */ + ~amount_t() { + TRACE_DTOR(amount_t); + if (quantity) + _release(); + } + + /** @name Assignment and copy + @{*/ + + /** Copy an amount object. Copies are very efficient, using a + copy-on-write model. Until the copy is changed, it refers to the + same memory used by the original via reference counting. The \c + amount_t::bigint_t class in amount.cc maintains the reference. */ + amount_t(const amount_t& amt) : quantity(NULL) { + if (amt.quantity) + _copy(amt); + else + commodity_ = NULL; + TRACE_CTOR(amount_t, "copy"); + } + /** Copy an amount object, applying the given commodity annotation + details afterward. This is equivalent to doing a normal copy + (@see amount_t(const amount_t&)) and then calling + amount_t::annotate(). */ + amount_t(const amount_t& amt, const annotation_t& details) : quantity(NULL) { + assert(amt.quantity); + _copy(amt); + annotate(details); + TRACE_CTOR(amount_t, "const amount_t&, const annotation_t&"); + } + /** Assign an amount object. This is like copying if the amount was + null beforehand, otherwise the previous value's reference is must + be freed. */ + amount_t& operator=(const amount_t& amt); + + amount_t& operator=(const double val) { + return *this = amount_t(val); + } + amount_t& operator=(const unsigned long val) { + return *this = amount_t(val); + } + amount_t& operator=(const long val) { + return *this = amount_t(val); + } + + /* Assign a string to an amount. This causes the contents of the + string to be parsed, look for a commoditized or uncommoditized + amount specifier. */ + amount_t& operator=(const string& str) { + return *this = amount_t(str); + } + amount_t& operator=(const char * str) { + assert(str); + return *this = amount_t(str); + } + + /*@}*/ + + /** @name Comparison + @{ */ + + /** Compare two amounts, returning a number less than zero if \p amt + is greater, exactly zero if they are equal, and greater than zero + if \p amt is less. This method is used to implement all of the + other comparison methods.*/ + int compare(const amount_t& amt) const; + + /** Test two amounts for equality. First the commodity pointers are + quickly tested, then the multi-precision values themselves must be + compared. */ + bool operator==(const amount_t& amt) const; + + template + bool operator==(const T& val) const { + return compare(val) == 0; + } + template + bool operator<(const T& amt) const { + return compare(amt) < 0; + } + template + bool operator>(const T& amt) const { + return compare(amt) > 0; + } + + /*@}*/ + + /** @name Binary arithmetic + */ + /*@{*/ + + amount_t& operator+=(const amount_t& amt); + amount_t& operator-=(const amount_t& amt); + amount_t& operator*=(const amount_t& amt) { + return multiply(amt); + } + amount_t& multiply(const amount_t& amt, bool ignore_commodity = false); + + /** Divide two amounts while extending the precision to preserve the + accuracy of the result. For example, if \c 10 is divided by \c 3, + the result ends up having a precision of \link + amount_t::extend_by_digits \endlink place to avoid losing internal + resolution. */ + amount_t& operator/=(const amount_t& amt); + + /*@}*/ + + /** @name Unary arithmetic + @{ */ + + /** Return an amount's internal precision. To find the precision it + should be displayed at -- assuming it was not created using + amount_t::exact() -- use the following expression instead: + @code + amount.commodity().precision() + @endcode */ + precision_t precision() const; + bool keep_precision() const; + void set_keep_precision(const bool keep = true) const; + precision_t display_precision() const; + + /** Returns the negated value of an amount. + @see operator-() + */ + amount_t negated() const { + amount_t temp(*this); + temp.in_place_negate(); + return temp; + } + void in_place_negate(); + + amount_t operator-() const { + return negated(); + } + + /** Returns the absolute value of an amount. Equivalent to: + @code + (x < * 0) ? - x : x + @endcode + */ + amount_t abs() const { + if (sign() < 0) + return negated(); + return *this; + } + + amount_t inverted() const { + amount_t temp(*this); + temp.in_place_invert(); + return temp; + } + void in_place_invert(); + + /** Yields an amount whose display precision when output is truncated + to the display precision of its commodity. This is normally the + default state of an amount, but if one has become unrounded, this + sets the "keep precision" state back to false. + @see set_keep_precision */ + amount_t rounded() const { + amount_t temp(*this); + temp.in_place_round(); + return temp; + } + void in_place_round(); + + amount_t roundto(int places) const { + amount_t temp(*this); + temp.in_place_round(); + return temp; + } + void in_place_roundto(int places); + + /** Yields an amount which has lost all of its extra precision, beyond what + the display precision of the commodity would have printed. */ + amount_t truncated() const { + amount_t temp(*this); + temp.in_place_truncate(); + return temp; + } + void in_place_truncate(); + + /** Yields an amount which has lost all of its extra precision, beyond what + the display precision of the commodity would have printed. */ + amount_t floored() const { + amount_t temp(*this); + temp.in_place_floor(); + return temp; + } + void in_place_floor(); + + /** Yields an amount which has lost all of its extra precision, beyond what + the display precision of the commodity would have printed. */ + amount_t ceilinged() const { + amount_t temp(*this); + temp.in_place_ceiling(); + return temp; + } + void in_place_ceiling(); + + /** Yields an amount whose display precision is never truncated, even + though its commodity normally displays only rounded values. */ + amount_t unrounded() const { + amount_t temp(*this); + temp.in_place_unround(); + return temp; + } + void in_place_unround(); + + /** reduces a value to its most basic commodity form, for amounts that + utilize "scaling commodities". For example, an amount of \c 1h + after reduction will be \c 3600s. + */ + amount_t reduced() const { + amount_t temp(*this); + temp.in_place_reduce(); + return temp; + } + void in_place_reduce(); + + /** unreduce(), if used with a "scaling commodity", yields the most + compact form greater than one. That is, \c 3599s will unreduce to + \c 59.98m, while \c 3601 unreduces to \c 1h. + */ + amount_t unreduced() const { + amount_t temp(*this); + temp.in_place_unreduce(); + return temp; + } + void in_place_unreduce(); + + /** Returns the historical value for an amount -- the default moment + returns the most recently known price -- based on the price history + for the given commodity (or determined automatically, if none is + provided). For example, if the amount were 10 AAPL, and + on Apr 10, 2000 each share of \c AAPL was worth \c $10, then + calling value() for that moment in time would yield the amount \c + $100.00. + */ + optional + value(const datetime_t& moment = datetime_t(), + const commodity_t * in_terms_of = NULL) const; + + optional price() const; + + /*@}*/ + + /** @name Truth tests + */ + /*@{*/ + + /** Truth tests. An amount may be truth test in several ways: + + sign() returns an integer less than, greater than, or equal to + zero depending on whether the amount is negative, zero, or + greater than zero. Note that this function tests the actual + value of the amount -- using its internal precision -- and not + the display value. To test its display value, use: + `round().sign()'. + + is_nonzero(), or operator bool, returns true if an amount's + display value is not zero. + + is_zero() returns true if an amount's display value is zero. + Thus, $0.0001 is considered zero if the current display precision + for dollars is two decimal places. + + is_realzero() returns true if an amount's actual value is zero. + Thus, $0.0001 is never considered realzero. + + is_null() returns true if an amount has no value and no + commodity. This only occurs if an uninitialized amount has never + been assigned a value. + */ + int sign() const; + + operator bool() const { + return is_nonzero(); + } + bool is_nonzero() const { + return ! is_zero(); + } + + bool is_zero() const; + bool is_realzero() const { + return sign() == 0; + } + + bool is_null() const { + if (! quantity) { + assert(! commodity_); + return true; + } + return false; + } + + /*@}*/ + + /** @name Conversion + */ + /*@{*/ + + /** Conversion methods. An amount may be converted to the same types + it can be constructed from -- with the exception of unsigned + long. Implicit conversions are not allowed in C++ (though they + are in Python), rather the following conversion methods must be + called explicitly: + + to_double([bool]) returns an amount as a double. If the optional + boolean argument is true (the default), an exception is thrown if + the conversion would lose information. + + to_long([bool]) returns an amount as a long integer. If the + optional boolean argument is true (the default), an exception is + thrown if the conversion would lose information. + + fits_in_long() returns true if to_long() would not lose + precision. + + to_string() returns an amount'ss "display value" as a string -- + after rounding the value according to the commodity's default + precision. It is equivalent to: `round().to_fullstring()'. + + to_fullstring() returns an amount's "internal value" as a string, + without any rounding. + + quantity_string() returns an amount's "display value", but + without any commodity. Note that this is different from + `number().to_string()', because in that case the commodity has + been stripped and the full, internal precision of the amount + would be displayed. + */ + double to_double() const; + long to_long() const; + bool fits_in_long() const; + + operator string() const { + return to_string(); + } + string to_string() const; + string to_fullstring() const; + string quantity_string() const; + + /*@}*/ + + /** @name Commodity methods + */ + /*@{*/ + + /** The following methods relate to an + amount's commodity: + + commodity() returns an amount's commodity. If the amount has no + commodity, the value returned is the `null_commodity'. + + has_commodity() returns true if the amount has a commodity. + + set_commodity(commodity_t) sets an amount's commodity to the + given value. Note that this merely sets the current amount to + that commodity, it does not "observe" the amount for possible + changes in the maximum display precision of the commodity, the + way that `parse' does. + + clear_commodity() sets an amount's commodity to null, such that + has_commodity() afterwards returns false. + + number() returns a commodity-less version of an amount. This is + useful for accessing just the numeric portion of an amount. + */ + commodity_t * commodity_ptr() const; + commodity_t& commodity() const { + return *commodity_ptr(); + } + + bool has_commodity() const; + void set_commodity(commodity_t& comm) { + if (! quantity) + *this = 0L; + commodity_ = &comm; + } + amount_t with_commodity(const commodity_t& comm) const { + if (commodity_ == &comm) { + return *this; + } else { + amount_t tmp(*this); + tmp.set_commodity(const_cast(comm)); + return tmp; + } + } + void clear_commodity() { + commodity_ = NULL; + } + + amount_t number() const { + if (! has_commodity()) + return *this; + + amount_t temp(*this); + temp.clear_commodity(); + return temp; + } + + /*@}*/ + + /** @name Commodity annotations + */ + /*@{*/ + + /** An amount's commodity may be annotated with special details, such as the + price it was purchased for, when it was acquired, or an arbitrary note, + identifying perhaps the lot number of an item. + + annotate_commodity(amount_t price, [datetime_t date, string tag]) + sets the annotations for the current amount's commodity. Only + the price argument is required, although it can be passed as + `none' if no price is desired. + + commodity_annotated() returns true if an amount's commodity has + any annotation details associated with it. + + annotation_details() returns all of the details of an annotated + commodity's annotations. The structure returns will evaluate as + boolean false if there are no details. + + strip_annotations() returns an amount whose commodity's annotations have + been stripped. + */ + void annotate(const annotation_t& details); + bool has_annotation() const; + + annotation_t& annotation(); + const annotation_t& annotation() const { + return const_cast(*this).annotation(); + } + + /** If the lot price is considered whenever working with commoditized + values. + + Let's say a user adds two values of the following form: + @code + 10 AAPL + 10 AAPL {$20} + @endcode + + This expression adds ten shares of Apple stock with another ten + shares that were purchased for \c $20 a share. If \c keep_price + is false, the result of this expression is an amount equal to + 20 AAPL. If \c keep_price is \c true the expression + yields an exception for adding amounts with different commodities. + In that case, a \link balance_t \endlink object must be used to + store the combined sum. */ + amount_t strip_annotations(const keep_details_t& what_to_keep) const; + + /*@}*/ + + /** @name Parsing + */ + /*@{*/ + + /** The `flags' argument of both parsing may be one or more of the + following: + + PARSE_NO_MIGRATE means to not pay attention to the way an + amount is used. Ordinarily, if an amount were $100.001, for + example, it would cause the default display precision for $ to be + "widened" to three decimal places. If PARSE_NO_MIGRATE is + used, the commodity's default display precision is not changed. + + PARSE_NO_REDUCE means not to call in_place_reduce() on the + resulting amount after it is parsed. + + These parsing methods observe the amounts they parse (unless + PARSE_NO_MIGRATE is true), and set the display details of + the corresponding commodity accordingly. This way, amounts do + not require commodities to be pre-defined in any way, but merely + displays them back to the user in the same fashion as it saw them + used. + + There is also a static convenience method called + `parse_conversion' which can be used to define a relationship + between scaling commodity values. For example, Ledger uses it to + define the relationships among various time values: + + @code + amount_t::parse_conversion("1.0m", "60s"); // a minute is 60 seconds + amount_t::parse_conversion("1.0h", "60m"); // an hour is 60 minutes + @endcode + + The method parse() is used to parse an amount from an input stream + or a string. A global operator>>() is also defined which simply + calls parse on the input stream. The parse() method has two forms: + + parse(istream, flags_t) parses an amount from the given input + stream. + + parse(string, flags_t) parses an amount from the given string. + + parse(string, flags_t) also parses an amount from a string. + */ + bool parse(std::istream& in, + const parse_flags_t& flags = PARSE_DEFAULT); + bool parse(const string& str, + const parse_flags_t& flags = PARSE_DEFAULT) { + std::istringstream stream(str); + bool result = parse(stream, flags); + return result; + } + + static void parse_conversion(const string& larger_str, + const string& smaller_str); + + /*@}*/ + + /** @name Printing + */ + /*@{*/ + + /** An amount may be output to a stream using the `print' method. There is + also a global operator<< defined which simply calls print for an amount + on the given stream. There is one form of the print method, which takes + one required argument and two arguments with default values: + + print(ostream, bool omit_commodity = false, bool full_precision = false) + prints an amounts to the given output stream, using its commodity's + default display characteristics. If `omit_commodity' is true, the + commodity will not be displayed, only the amount (although the + commodity's display precision is still used). If `full_precision' is + true, the full internal precision of the amount is displayed, regardless + of its commodity's display precision. + */ +#define AMOUNT_PRINT_NO_FLAGS 0x00 +#define AMOUNT_PRINT_RIGHT_JUSTIFY 0x01 +#define AMOUNT_PRINT_COLORIZE 0x02 +#define AMOUNT_PRINT_NO_COMPUTED_ANNOTATIONS 0x04 +#define AMOUNT_PRINT_ELIDE_COMMODITY_QUOTES 0x08 + + void print(std::ostream& out, + const uint_least8_t flags = AMOUNT_PRINT_NO_FLAGS) const; + + /*@}*/ + + /** @name Debugging + */ + /*@{*/ + + /** There are two methods defined to help with debugging: + + dump(ostream) dumps an amount to an output stream. There is + little different from print(), it simply surrounds the display + value with a marker, for example "AMOUNT($1.00)". This code is + used by other dumping code elsewhere in Ledger. + + valid() returns true if an amount is valid. This ensures that if + an amount has a commodity, it has a valid value pointer, for + example, even if that pointer simply points to a zero value. + */ + void dump(std::ostream& out) const { + out << "AMOUNT("; + print(out); + out << ")"; + } + + bool valid() const; + + /*@}*/ +}; + +inline amount_t amount_t::exact(const string& value) { + amount_t temp; + temp.parse(value, PARSE_NO_MIGRATE); + return temp; +} + +inline string amount_t::to_string() const { + std::ostringstream bufstream; + print(bufstream); + return bufstream.str(); +} + +inline string amount_t::to_fullstring() const { + std::ostringstream bufstream; + unrounded().print(bufstream); + return bufstream.str(); +} + +inline string amount_t::quantity_string() const { + std::ostringstream bufstream; + number().print(bufstream); + return bufstream.str(); +} + +inline std::ostream& operator<<(std::ostream& out, const amount_t& amt) { + if (amount_t::stream_fullstrings) + amt.unrounded().print(out); + else + amt.print(out); + return out; +} +inline std::istream& operator>>(std::istream& in, amount_t& amt) { + amt.parse(in); + return in; +} + +void put_amount(property_tree::ptree& pt, const amount_t& amt, + bool commodity_details = false); + +} // namespace ledger + +#endif // _AMOUNT_H diff --git a/src/annotate.cc b/src/annotate.cc new file mode 100644 index 00000000..c5ccdf07 --- /dev/null +++ b/src/annotate.cc @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +#include + +#include "amount.h" +#include "commodity.h" +#include "expr.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +bool annotation_t::operator<(const annotation_t& rhs) const +{ + if (! price && rhs.price) return true; + if (price && ! rhs.price) return false; + if (! date && rhs.date) return true; + if (date && ! rhs.date) return false; + if (! tag && rhs.tag) return true; + if (tag && ! rhs.tag) return false; + + if (! value_expr && rhs.value_expr) return true; + if (value_expr && ! rhs.value_expr) return false; + + if (price) { + if (price->commodity().symbol() < rhs.price->commodity().symbol()) + return true; + if (price->commodity().symbol() > rhs.price->commodity().symbol()) + return false; + + if (*price < *rhs.price) return true; + if (*price > *rhs.price) return false; + } + if (date) { + if (*date < *rhs.date) return true; + if (*date > *rhs.date) return false; + } + if (tag) { + if (*tag < *rhs.tag) return true; + if (*tag > *rhs.tag) return false; + } + if (value_expr) { + DEBUG("annotate.less", "Comparing (" << value_expr->text() + << ") < (" << rhs.value_expr->text()); + if (value_expr->text() < rhs.value_expr->text()) return true; + //if (value_expr->text() > rhs.value_expr->text()) return false; + } + + return false; +} + +void annotation_t::parse(std::istream& in) +{ + do { + std::istream::pos_type pos = in.tellg(); + if (static_cast(pos) < 0) + return; + + char buf[256]; + char c = peek_next_nonws(in); + if (c == '{') { + if (price) + throw_(amount_error, _("Commodity specifies more than one price")); + + in.get(c); + c = static_cast(in.peek()); + if (c == '{') { + in.get(c); + add_flags(ANNOTATION_PRICE_NOT_PER_UNIT); + } + + c = peek_next_nonws(in); + if (c == '=') { + in.get(c); + add_flags(ANNOTATION_PRICE_FIXATED); + } + + READ_INTO(in, buf, 255, c, c != '}'); + if (c == '}') { + in.get(c); + if (has_flags(ANNOTATION_PRICE_NOT_PER_UNIT)) { + c = static_cast(in.peek()); + if (c != '}') + throw_(amount_error, _("Commodity lot price lacks double closing brace")); + else + in.get(c); + } + } else { + throw_(amount_error, _("Commodity lot price lacks closing brace")); + } + + amount_t temp; + temp.parse(buf, PARSE_NO_MIGRATE); + + DEBUG("commodity.annotations", "Parsed annotation price: " << temp); + price = temp; + } + else if (c == '[') { + if (date) + throw_(amount_error, _("Commodity specifies more than one date")); + + in.get(c); + READ_INTO(in, buf, 255, c, c != ']'); + if (c == ']') + in.get(c); + else + throw_(amount_error, _("Commodity date lacks closing bracket")); + + date = parse_date(buf); + } + else if (c == '(') { + in.get(c); + c = static_cast(in.peek()); + if (c == '@') { + in.clear(); + in.seekg(pos, std::ios::beg); + break; + } + else if (c == '(') { + if (value_expr) + throw_(amount_error, + _("Commodity specifies more than one valuation expresion")); + + in.get(c); + READ_INTO(in, buf, 255, c, c != ')'); + if (c == ')') { + in.get(c); + c = static_cast(in.peek()); + if (c == ')') + in.get(c); + else + throw_(amount_error, + _("Commodity valuation expression lacks closing parentheses")); + } else { + throw_(amount_error, + _("Commodity valuation expression lacks closing parentheses")); + } + + value_expr = expr_t(buf); + } else { + if (tag) + throw_(amount_error, _("Commodity specifies more than one tag")); + + READ_INTO(in, buf, 255, c, c != ')'); + if (c == ')') + in.get(c); + else + throw_(amount_error, _("Commodity tag lacks closing parenthesis")); + + tag = buf; + } + } + else { + in.clear(); + in.seekg(pos, std::ios::beg); + break; + } + } while (true); + +#if DEBUG_ON + if (SHOW_DEBUG("amount.commodities") && *this) { + DEBUG("amount.commodities", + "Parsed commodity annotations: " << std::endl << *this); + } +#endif +} + +void annotation_t::print(std::ostream& out, bool keep_base, + bool no_computed_annotations) const +{ + if (price && + (! no_computed_annotations || ! has_flags(ANNOTATION_PRICE_CALCULATED))) + out << " {" + << (has_flags(ANNOTATION_PRICE_FIXATED) ? "=" : "") + << (keep_base ? *price : price->unreduced()) + << '}'; + + if (date && + (! no_computed_annotations || ! has_flags(ANNOTATION_DATE_CALCULATED))) + out << " [" << format_date(*date, FMT_PRINTED) << ']'; + + if (tag && + (! no_computed_annotations || ! has_flags(ANNOTATION_TAG_CALCULATED))) + out << " (" << *tag << ')'; + + if (value_expr && ! has_flags(ANNOTATION_VALUE_EXPR_CALCULATED)) + out << " ((" << *value_expr << "))"; +} + +void put_annotation(property_tree::ptree& st, const annotation_t& details) +{ + if (details.price) + put_amount(st.put("price", ""), *details.price); + + if (details.date) + put_date(st.put("date", ""), *details.date); + + if (details.tag) + st.put("tag", *details.tag); + + if (details.value_expr) + st.put("value_expr", details.value_expr->text()); +} + +bool keep_details_t::keep_all(const commodity_t& comm) const +{ + return (! comm.has_annotation() || + (keep_price && keep_date && keep_tag && ! only_actuals)); +} + +bool keep_details_t::keep_any(const commodity_t& comm) const +{ + return comm.has_annotation() && (keep_price || keep_date || keep_tag); +} + +bool annotated_commodity_t::operator==(const commodity_t& comm) const +{ + // If the base commodities don't match, the game's up. + if (base != comm.base) + return false; + + assert(annotated); + if (! comm.annotated) + return false; + + if (details != as_annotated_commodity(comm).details) + return false; + + return true; +} + +optional +annotated_commodity_t::find_price(const commodity_t * commodity, + const datetime_t& moment, + const datetime_t& oldest) const +{ + DEBUG("commodity.price.find", + "annotated_commodity_t::find_price(" << symbol() << ")"); + + datetime_t when; + if (! moment.is_not_a_date_time()) + when = moment; + else if (epoch) + when = *epoch; + else + when = CURRENT_TIME(); + + DEBUG("commodity.price.find", "reference time: " << when); + + const commodity_t * target = NULL; + if (commodity) + target = commodity; + + if (details.price) { + DEBUG("commodity.price.find", "price annotation: " << *details.price); + + if (details.has_flags(ANNOTATION_PRICE_FIXATED)) { + DEBUG("commodity.price.find", + "amount_t::value: fixated price = " << *details.price); + return price_point_t(when, *details.price); + } + else if (! target) { + DEBUG("commodity.price.find", "setting target commodity from price"); + target = details.price->commodity_ptr(); + } + } + +#if DEBUG_ON + if (target) + DEBUG("commodity.price.find", "target commodity: " << target->symbol()); +#endif + + if (details.value_expr) + return find_price_from_expr(const_cast(*details.value_expr), + commodity, when); + + return commodity_t::find_price(target, when, oldest); +} + +commodity_t& +annotated_commodity_t::strip_annotations(const keep_details_t& what_to_keep) +{ + DEBUG("commodity.annotated.strip", + "Reducing commodity " << *this << std::endl + << " keep price " << what_to_keep.keep_price << " " + << " keep date " << what_to_keep.keep_date << " " + << " keep tag " << what_to_keep.keep_tag); + + commodity_t * new_comm; + + bool keep_price = + ((what_to_keep.keep_price || + (details.has_flags(ANNOTATION_PRICE_FIXATED) && + has_flags(COMMODITY_SAW_ANN_PRICE_FLOAT) && + has_flags(COMMODITY_SAW_ANN_PRICE_FIXATED))) && + (! what_to_keep.only_actuals || + ! details.has_flags(ANNOTATION_PRICE_CALCULATED))); + bool keep_date = + (what_to_keep.keep_date && + (! what_to_keep.only_actuals || + ! details.has_flags(ANNOTATION_DATE_CALCULATED))); + bool keep_tag = + (what_to_keep.keep_tag && + (! what_to_keep.only_actuals || + ! details.has_flags(ANNOTATION_TAG_CALCULATED))); + + DEBUG("commodity.annotated.strip", + "Reducing commodity " << *this << std::endl + << " keep price " << keep_price << " " + << " keep date " << keep_date << " " + << " keep tag " << keep_tag); + + if ((keep_price && details.price) || + (keep_date && details.date) || + (keep_tag && details.tag)) { + new_comm = pool().find_or_create + (referent(), annotation_t(keep_price ? details.price : none, + keep_date ? details.date : none, + keep_tag ? details.tag : none)); + + // Transfer over any relevant annotation flags, as they still apply. + if (new_comm->annotated) { + annotation_t& new_details(as_annotated_commodity(*new_comm).details); + if (keep_price) + new_details.add_flags(details.flags() & + (ANNOTATION_PRICE_CALCULATED | + ANNOTATION_PRICE_FIXATED)); + if (keep_date) + new_details.add_flags(details.flags() & ANNOTATION_DATE_CALCULATED); + if (keep_tag) + new_details.add_flags(details.flags() & ANNOTATION_TAG_CALCULATED); + } + + return *new_comm; + } + + return referent(); +} + +void annotated_commodity_t::write_annotations + (std::ostream& out, bool no_computed_annotations) const +{ + details.print(out, pool().keep_base, no_computed_annotations); +} + +} // namespace ledger diff --git a/src/annotate.h b/src/annotate.h new file mode 100644 index 00000000..75d42589 --- /dev/null +++ b/src/annotate.h @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup math + */ + +/** + * @file annotate.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Types for annotating commodities + * + * Long. + */ +#ifndef _ANNOTATE_H +#define _ANNOTATE_H + +#include "expr.h" + +namespace ledger { + +struct annotation_t : public supports_flags<>, + public equality_comparable +{ +#define ANNOTATION_PRICE_CALCULATED 0x01 +#define ANNOTATION_PRICE_FIXATED 0x02 +#define ANNOTATION_PRICE_NOT_PER_UNIT 0x04 +#define ANNOTATION_DATE_CALCULATED 0x08 +#define ANNOTATION_TAG_CALCULATED 0x10 +#define ANNOTATION_VALUE_EXPR_CALCULATED 0x20 + + optional price; + optional date; + optional tag; + optional value_expr; + + explicit annotation_t(const optional& _price = none, + const optional& _date = none, + const optional& _tag = none, + const optional& _value_expr = none) + : supports_flags<>(), price(_price), date(_date), tag(_tag), + value_expr(_value_expr) { + TRACE_CTOR(annotation_t, + "optional + date_t + string + expr_t"); + } + annotation_t(const annotation_t& other) + : supports_flags<>(other.flags()), + price(other.price), date(other.date), tag(other.tag), + value_expr(other.value_expr) + { + TRACE_CTOR(annotation_t, "copy"); + } + ~annotation_t() { + TRACE_DTOR(annotation_t); + } + + operator bool() const { + return price || date || tag || value_expr; + } + + bool operator<(const annotation_t& rhs) const; + bool operator==(const annotation_t& rhs) const { + return (price == rhs.price && + date == rhs.date && + tag == rhs.tag && + (value_expr && rhs.value_expr ? + value_expr->text() == rhs.value_expr->text() : + value_expr == rhs.value_expr)); + } + + void parse(std::istream& in); + void print(std::ostream& out, bool keep_base = false, + bool no_computed_annotations = false) const; + + bool valid() const { + assert(*this); + return true; + } +}; + +void put_annotation(property_tree::ptree& pt, const annotation_t& details); + +struct keep_details_t +{ + bool keep_price; + bool keep_date; + bool keep_tag; + bool only_actuals; + + explicit keep_details_t(bool _keep_price = false, + bool _keep_date = false, + bool _keep_tag = false, + bool _only_actuals = false) + : keep_price(_keep_price), + keep_date(_keep_date), + keep_tag(_keep_tag), + only_actuals(_only_actuals) + { + TRACE_CTOR(keep_details_t, "bool, bool, bool, bool"); + } + keep_details_t(const keep_details_t& other) + : keep_price(other.keep_price), keep_date(other.keep_date), + keep_tag(other.keep_tag), only_actuals(other.only_actuals) { + TRACE_CTOR(keep_details_t, "copy"); + } + ~keep_details_t() throw() { + TRACE_DTOR(keep_details_t); + } + + bool keep_all() const { + return keep_price && keep_date && keep_tag && ! only_actuals; + } + bool keep_all(const commodity_t& comm) const; + + bool keep_any() const { + return keep_price || keep_date || keep_tag; + } + bool keep_any(const commodity_t& comm) const; +}; + +inline std::ostream& operator<<(std::ostream& out, + const annotation_t& details) { + details.print(out); + return out; +} + +class annotated_commodity_t + : public commodity_t, + public equality_comparable > +{ +protected: + friend class commodity_pool_t; + + commodity_t * ptr; + + explicit annotated_commodity_t(commodity_t * _ptr, + const annotation_t& _details) + : commodity_t(_ptr->parent_, _ptr->base), ptr(_ptr), details(_details) { + annotated = true; + qualified_symbol = _ptr->qualified_symbol; + TRACE_CTOR(annotated_commodity_t, "commodity_t *, annotation_t"); + } + +public: + annotation_t details; + + virtual ~annotated_commodity_t() { + TRACE_DTOR(annotated_commodity_t); + } + + virtual bool operator==(const commodity_t& comm) const; + virtual bool operator==(const annotated_commodity_t& comm) const { + return *this == static_cast(comm); + } + + virtual commodity_t& referent() { + return *ptr; + } + virtual const commodity_t& referent() const { + return *ptr; + } + + virtual optional value_expr() const { + if (details.value_expr) + return details.value_expr; + return commodity_t::value_expr(); + } + + optional + virtual find_price(const commodity_t * commodity = NULL, + const datetime_t& moment = datetime_t(), + const datetime_t& oldest = datetime_t()) const; + + virtual commodity_t& strip_annotations(const keep_details_t& what_to_keep); + + virtual void print(std::ostream& out, bool elide_quotes = false, + bool print_annotations = false) const { + if (print_annotations) { + std::ostringstream buf; + commodity_t::print(buf, elide_quotes); + write_annotations(buf); + out << buf.str(); + } else { + commodity_t::print(out, elide_quotes); + } + } + + virtual void write_annotations(std::ostream& out, + bool no_computed_annotations = false) const; +}; + +inline annotated_commodity_t& +as_annotated_commodity(commodity_t& commodity) { + return downcast(commodity); +} +inline const annotated_commodity_t& +as_annotated_commodity(const commodity_t& commodity) { + return downcast(commodity); +} + +} // namespace ledger + +#endif // _ANNOTATE_H diff --git a/src/balance.cc b/src/balance.cc new file mode 100644 index 00000000..5f5e6fc9 --- /dev/null +++ b/src/balance.cc @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "balance.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" +#include "unistring.h" // for justify() + +namespace ledger { + +balance_t::balance_t(const double val) +{ + amounts.insert + (amounts_map::value_type(commodity_pool_t::current_pool->null_commodity, val)); + TRACE_CTOR(balance_t, "const double"); +} + +balance_t::balance_t(const unsigned long val) +{ + amounts.insert + (amounts_map::value_type(commodity_pool_t::current_pool->null_commodity, val)); + TRACE_CTOR(balance_t, "const unsigned long"); +} + +balance_t::balance_t(const long val) +{ + amounts.insert + (amounts_map::value_type(commodity_pool_t::current_pool->null_commodity, val)); + TRACE_CTOR(balance_t, "const long"); +} + +balance_t& balance_t::operator+=(const balance_t& bal) +{ + foreach (const amounts_map::value_type& pair, bal.amounts) + *this += pair.second; + return *this; +} + +balance_t& balance_t::operator+=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot add an uninitialized amount to a balance")); + + if (amt.is_realzero()) + return *this; + + amounts_map::iterator i = amounts.find(&amt.commodity()); + if (i != amounts.end()) + i->second += amt; + else + amounts.insert(amounts_map::value_type(&amt.commodity(), amt)); + + return *this; +} + +balance_t& balance_t::operator-=(const balance_t& bal) +{ + foreach (const amounts_map::value_type& pair, bal.amounts) + *this -= pair.second; + return *this; +} + +balance_t& balance_t::operator-=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot subtract an uninitialized amount from a balance")); + + if (amt.is_realzero()) + return *this; + + amounts_map::iterator i = amounts.find(&amt.commodity()); + if (i != amounts.end()) { + i->second -= amt; + if (i->second.is_realzero()) + amounts.erase(i); + } else { + amounts.insert(amounts_map::value_type(&amt.commodity(), amt.negated())); + } + return *this; +} + +balance_t& balance_t::operator*=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot multiply a balance by an uninitialized amount")); + + if (is_realzero()) { + ; + } + else if (amt.is_realzero()) { + *this = amt; + } + else if (! amt.commodity()) { + // Multiplying by an amount with no commodity causes all the + // component amounts to be increased by the same factor. + foreach (amounts_map::value_type& pair, amounts) + pair.second *= amt; + } + else if (amounts.size() == 1) { + // Multiplying by a commoditized amount is only valid if the sole + // commodity in the balance is of the same kind as the amount's + // commodity. + if (*amounts.begin()->first == amt.commodity()) + amounts.begin()->second *= amt; + else + throw_(balance_error, + _("Cannot multiply a balance with annotated commodities by a commoditized amount")); + } + else { + assert(amounts.size() > 1); + throw_(balance_error, + _("Cannot multiply a multi-commodity balance by a commoditized amount")); + } + return *this; +} + +balance_t& balance_t::operator/=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot divide a balance by an uninitialized amount")); + + if (is_realzero()) { + ; + } + else if (amt.is_realzero()) { + throw_(balance_error, _("Divide by zero")); + } + else if (! amt.commodity()) { + // Dividing by an amount with no commodity causes all the + // component amounts to be divided by the same factor. + foreach (amounts_map::value_type& pair, amounts) + pair.second /= amt; + } + else if (amounts.size() == 1) { + // Dividing by a commoditized amount is only valid if the sole + // commodity in the balance is of the same kind as the amount's + // commodity. + if (*amounts.begin()->first == amt.commodity()) + amounts.begin()->second /= amt; + else + throw_(balance_error, + _("Cannot divide a balance with annotated commodities by a commoditized amount")); + } + else { + assert(amounts.size() > 1); + throw_(balance_error, + _("Cannot divide a multi-commodity balance by a commoditized amount")); + } + return *this; +} + +optional +balance_t::value(const datetime_t& moment, + const commodity_t * in_terms_of) const +{ + balance_t temp; + bool resolved = false; + + foreach (const amounts_map::value_type& pair, amounts) { + if (optional val = pair.second.value(moment, in_terms_of)) { + temp += *val; + resolved = true; + } else { + temp += pair.second; + } + } + return resolved ? temp : optional(); +} + +optional +balance_t::commodity_amount(const optional& commodity) const +{ + if (! commodity) { + if (amounts.size() == 1) { + return amounts.begin()->second; + } + else if (amounts.size() > 1) { + // Try stripping annotations before giving an error. + balance_t temp(strip_annotations(keep_details_t())); + if (temp.amounts.size() == 1) + return temp.commodity_amount(commodity); + + throw_(amount_error, + _f("Requested amount of a balance with multiple commodities: %1%") + % temp); + } + } + else if (amounts.size() > 0) { + amounts_map::const_iterator i = + amounts.find(const_cast(&*commodity)); + if (i != amounts.end()) + return i->second; + } + return none; +} + +balance_t +balance_t::strip_annotations(const keep_details_t& what_to_keep) const +{ + balance_t temp; + + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.strip_annotations(what_to_keep); + + return temp; +} + +void balance_t::sorted_amounts(amounts_array& sorted) const +{ + foreach (const amounts_map::value_type& pair, amounts) + sorted.push_back(&pair.second); + std::stable_sort( + sorted.begin(), sorted.end(), + [](const amount_t * left, const amount_t * right) { + return commodity_t::compare_by_commodity()(left, right) < 0; + }); +} + +void balance_t::map_sorted_amounts(function fn) const +{ + if (! amounts.empty()) { + if (amounts.size() == 1) { + const amount_t& amount((*amounts.begin()).second); + if (amount) + fn(amount); + } + else { + amounts_array sorted; + sorted_amounts(sorted); + foreach (const amount_t * amount, sorted) + fn(*amount); + } + } +} + +namespace { + struct print_amount_from_balance + { + std::ostream& out; + bool& first; + int fwidth; + int lwidth; + uint_least8_t flags; + + explicit print_amount_from_balance(std::ostream& _out, + bool& _first, + int _fwidth, int _lwidth, + uint_least8_t _flags) + : out(_out), first(_first), fwidth(_fwidth), lwidth(_lwidth), + flags(_flags) { + TRACE_CTOR(print_amount_from_balance, + "ostream&, int, int, uint_least8_t"); + } + print_amount_from_balance(const print_amount_from_balance& other) + : out(other.out), first(other.first), fwidth(other.fwidth), + lwidth(other.lwidth), flags(other.flags) { + TRACE_CTOR(print_amount_from_balance, "copy"); + } + ~print_amount_from_balance() throw() { + TRACE_DTOR(print_amount_from_balance); + } + + void operator()(const amount_t& amount) { + int width; + if (! first) { + out << std::endl; + width = lwidth; + } else { + first = false; + width = fwidth; + } + + std::ostringstream buf; + amount.print(buf, flags); + + justify(out, buf.str(), width, + flags & AMOUNT_PRINT_RIGHT_JUSTIFY, + flags & AMOUNT_PRINT_COLORIZE && amount.sign() < 0); + } + + void close() { + out.width(fwidth); + if (flags & AMOUNT_PRINT_RIGHT_JUSTIFY) + out << std::right; + else + out << std::left; + out << 0; + } + }; +} + +void balance_t::print(std::ostream& out, + const int first_width, + const int latter_width, + const uint_least8_t flags) const +{ + bool first = true; + print_amount_from_balance + amount_printer(out, first, first_width, + latter_width == 1 ? first_width : latter_width, flags); + map_sorted_amounts(amount_printer); + + if (first) + amount_printer.close(); +} + +void put_balance(property_tree::ptree& st, const balance_t& bal) +{ + foreach (const balance_t::amounts_map::value_type& pair, bal.amounts) + put_amount(st.add("amount", ""), pair.second); +} + +} // namespace ledger diff --git a/src/balance.h b/src/balance.h new file mode 100644 index 00000000..bd373cb9 --- /dev/null +++ b/src/balance.h @@ -0,0 +1,608 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup math + */ + +/** + * @file balance.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Basic type for adding multiple commodities together + * + * Unlike the amount_t class, which throws an exception if amounts of + * differing commodities are added or subtracted, the balance_t class + * is designed to allow this, tracking the amounts of each component + * commodity separately. + */ +#ifndef _BALANCE_H +#define _BALANCE_H + +#include "amount.h" + +namespace ledger { + +DECLARE_EXCEPTION(balance_error, std::runtime_error); + +/** + * @class balance_t + * + * @brief A wrapper around amount_t allowing addition of multiple commodities. + * + * The balance_t class is appopriate for keeping a running balance + * where amounts of multiple commodities may be involved. + */ +class balance_t + : public equality_comparable > > > > > > > > > > > > > +{ +public: + typedef std::unordered_map amounts_map; + typedef std::vector amounts_array; + + amounts_map amounts; + + /** + * Constructors. balance_t supports similar forms of construction + * to amount_t. + * + * balance_t() creates an empty balance to which amounts or other + * balances may be added or subtracted. + * + * balance_t(amount_t) constructs a balance whose starting value is + * equal to the given amount. + * + * balance_t(double), balance_t(unsigned long) and balance_t(long) + * will construct an amount from their arguments and then construct + * a balance whose starting value is equal to that amount. This + * initial balance will have no commodity. + * + * balance_t(string) and balance_t(const char *) both convert from a + * string representation of an amount to a balance whose initial + * value is that amount. This is the proper way to initialize a + * balance like '$100.00'. + */ + balance_t() { + TRACE_CTOR(balance_t, ""); + } + balance_t(const amount_t& amt) { + if (amt.is_null()) + throw_(balance_error, + _("Cannot initialize a balance from an uninitialized amount")); + if (! amt.is_realzero()) + amounts.insert(amounts_map::value_type(&amt.commodity(), amt)); + TRACE_CTOR(balance_t, "const amount_t&"); + } + balance_t(const double val); + balance_t(const unsigned long val); + balance_t(const long val); + + explicit balance_t(const string& val) { + amount_t temp(val); + amounts.insert(amounts_map::value_type(&temp.commodity(), temp)); + TRACE_CTOR(balance_t, "const string&"); + } + explicit balance_t(const char * val) { + amount_t temp(val); + amounts.insert(amounts_map::value_type(&temp.commodity(), temp)); + TRACE_CTOR(balance_t, "const char *"); + } + + /** + * Destructor. Destroys all of the accumulated amounts in the + * balance. + */ + ~balance_t() { + TRACE_DTOR(balance_t); + } + + /** + * Assignment and copy operators. An balance may be assigned or copied. + */ + balance_t(const balance_t& bal) : amounts(bal.amounts) { + TRACE_CTOR(balance_t, "copy"); + } + + balance_t& operator=(const balance_t& bal) { + if (this != &bal) + amounts = bal.amounts; + return *this; + } + balance_t& operator=(const amount_t& amt) { + if (amt.is_null()) + throw_(balance_error, + _("Cannot assign an uninitialized amount to a balance")); + + amounts.clear(); + if (! amt.is_realzero()) + amounts.insert(amounts_map::value_type(&amt.commodity(), amt)); + + return *this; + } + + balance_t& operator=(const string& str) { + return *this = balance_t(str); + } + balance_t& operator=(const char * str) { + return *this = balance_t(str); + } + + /** + * Comparison operators. Balances are fairly restrictive in terms + * of how they may be compared. They may be compared for equality + * or inequality, but this is all, since the concept of "less than" + * or "greater than" makes no sense when amounts of multiple + * commodities are involved. + * + * Balances may also be compared to amounts, in which case the sum + * of the balance must equal the amount exactly. + * + * If a comparison between balances is desired, the balances must + * first be rendered to value equivalent amounts using the `value' + * method, to determine a market valuation at some specific moment + * in time. + */ + bool operator==(const balance_t& bal) const { + return amounts == bal.amounts; + } + bool operator==(const amount_t& amt) const { + if (amt.is_null()) + throw_(balance_error, + _("Cannot compare a balance to an uninitialized amount")); + + if (amt.is_realzero()) + return amounts.empty(); + else + return amounts.size() == 1 && amounts.begin()->second == amt; + } + + template + bool operator==(const T& val) const { + return *this == amount_t(val); + } + + /** + * Binary arithmetic operators. Balances support addition and + * subtraction of other balances or amounts, but multiplication and + * division are restricted to uncommoditized amounts only. + */ + balance_t& operator+=(const balance_t& bal); + balance_t& operator+=(const amount_t& amt); + balance_t& operator+=(const double val) { + return *this += amount_t(val); + } + balance_t& operator+=(const unsigned long val) { + return *this += amount_t(val); + } + balance_t& operator+=(const long val) { + return *this += amount_t(val); + } + + balance_t& operator-=(const balance_t& bal); + balance_t& operator-=(const amount_t& amt); + balance_t& operator-=(const double val) { + return *this -= amount_t(val); + } + balance_t& operator-=(const unsigned long val) { + return *this -= amount_t(val); + } + balance_t& operator-=(const long val) { + return *this -= amount_t(val); + } + + balance_t& operator*=(const amount_t& amt); + balance_t& operator*=(const double val) { + return *this *= amount_t(val); + } + balance_t& operator*=(const unsigned long val) { + return *this *= amount_t(val); + } + balance_t& operator*=(const long val) { + return *this *= amount_t(val); + } + + balance_t& operator/=(const amount_t& amt); + balance_t& operator/=(const double val) { + return *this /= amount_t(val); + } + balance_t& operator/=(const unsigned long val) { + return *this /= amount_t(val); + } + balance_t& operator/=(const long val) { + return *this /= amount_t(val); + } + + /** + * Unary arithmetic operators. There are only a few unary methods + * support on balance: + * + * negate(), also unary minus (- x), returns a balance all of whose + * component amounts have been negated. In order words, it inverts + * the sign of all member amounts. + * + * abs() returns a balance where no component amount is negative. + * + * reduce() reduces the values in a balance to their most basic + * commodity forms, for amounts that utilize "scaling commodities". + * For example, a balance of 1h and 1m after reduction will be + * 3660s. + * + * unreduce(), if used with amounts that use "scaling commodities", + * yields the most compact form greater than 1.0 for each component + * amount. That is, a balance of 10m and 1799s will unreduce to + * 39.98m. + * + * value(optional) returns the total historical value for + * a balance -- the default moment returns a value based on the most + * recently known price -- based on the price history of its + * component commodities. See amount_t::value for an example. + * + * Further, for the sake of efficiency and avoiding temporary + * objects, the following methods support "in-place" variants act on + * the balance itself and return a reference to the result + * (`*this'): + * + * in_place_negate() + * in_place_reduce() + * in_place_unreduce() + */ + balance_t negated() const { + balance_t temp(*this); + temp.in_place_negate(); + return temp; + } + void in_place_negate() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_negate(); + } + balance_t operator-() const { + return negated(); + } + + balance_t abs() const { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.abs(); + return temp; + } + + balance_t rounded() const { + balance_t temp(*this); + temp.in_place_round(); + return temp; + } + void in_place_round() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_round(); + } + + balance_t roundto(int places) const { + balance_t temp(*this); + temp.in_place_roundto(places); + return temp; + } + + void in_place_roundto(int places) { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_roundto(places); + } + + balance_t truncated() const { + balance_t temp(*this); + temp.in_place_truncate(); + return temp; + } + void in_place_truncate() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_truncate(); + } + + balance_t floored() const { + balance_t temp(*this); + temp.in_place_floor(); + return temp; + } + void in_place_floor() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_floor(); + } + + balance_t ceilinged() const { + balance_t temp(*this); + temp.in_place_ceiling(); + return temp; + } + void in_place_ceiling() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_ceiling(); + } + + + balance_t unrounded() const { + balance_t temp(*this); + temp.in_place_unround(); + return temp; + } + void in_place_unround() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_unround(); + } + + balance_t reduced() const { + balance_t temp(*this); + temp.in_place_reduce(); + return temp; + } + void in_place_reduce() { + // A temporary must be used here because reduction may cause + // multiple component amounts to collapse to the same commodity. + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.reduced(); + *this = temp; + } + + balance_t unreduced() const { + balance_t temp(*this); + temp.in_place_unreduce(); + return temp; + } + void in_place_unreduce() { + // A temporary must be used here because unreduction may cause + // multiple component amounts to collapse to the same commodity. + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.unreduced(); + *this = temp; + } + + optional + value(const datetime_t& moment = datetime_t(), + const commodity_t * in_terms_of = NULL) const; + + /** + * Truth tests. An balance may be truth test in two ways: + * + * is_nonzero(), or operator bool, returns true if a balance's + * display value is not zero. + * + * is_zero() returns true if an balance's display value is zero. + * Thus, a balance containing $0.0001 is considered zero if the + * current display precision for dollars is two decimal places. + * + * is_realzero() returns true if an balance's actual value is zero. + * Thus, a balance containing $0.0001 is never considered realzero. + * + * is_empty() returns true if a balance has no amounts within it. + * This can occur after a balance has been default initialized, or + * if the exact amount it contains is subsequently subtracted from + * it. + */ + operator bool() const { + return is_nonzero(); + } + + bool is_nonzero() const { + if (is_empty()) + return false; + + foreach (const amounts_map::value_type& pair, amounts) + if (pair.second.is_nonzero()) + return true; + return false; + } + + bool is_zero() const { + if (is_empty()) + return true; + + foreach (const amounts_map::value_type& pair, amounts) + if (! pair.second.is_zero()) + return false; + return true; + } + + bool is_realzero() const { + if (is_empty()) + return true; + + foreach (const amounts_map::value_type& pair, amounts) + if (! pair.second.is_realzero()) + return false; + return true; + } + + bool is_empty() const { + return amounts.size() == 0; + } + bool single_amount() const { + return amounts.size() == 1; + } + + /** + * Conversion methods. A balance can be converted to an amount, but + * only if contains a single component amount. + */ + operator string() const { + return to_string(); + } + string to_string() const { + std::ostringstream buf; + print(buf); + return buf.str(); + } + + amount_t to_amount() const { + if (is_empty()) + throw_(balance_error, _("Cannot convert an empty balance to an amount")); + else if (amounts.size() == 1) + return amounts.begin()->second; + else + throw_(balance_error, + _("Cannot convert a balance with multiple commodities to an amount")); + return amount_t(); + } + + /** + * Commodity-related methods. Balances support two + * commodity-related methods: + * + * commodity_count() returns the number of different commodities + * stored in the balance. + * + * commodity_amount(optional) returns an (optional) + * amount for the given commodity within the balance; if no + * commodity is specified, it returns the (optional) uncommoditized + * component of the balance. If no matching element can be found, + * boost::none is returned. + */ + std::size_t commodity_count() const { + return amounts.size(); + } + + optional + commodity_amount(const optional& commodity = none) const; + + balance_t number() const { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.number(); + return temp; + } + + /** + * Annotated commodity methods. The amounts contained by a balance + * may use annotated commodities. The `strip_annotations' method + * will return a balance all of whose component amount have had + * their commodity annotations likewise stripped. See + * amount_t::strip_annotations for more details. + */ + balance_t strip_annotations(const keep_details_t& what_to_keep) const; + + /** + * Given a balance, insert a commodity-wise sort of the amounts into the + * given amounts_array. + */ + void sorted_amounts(amounts_array& sorted) const; + + /** + * Iteration primitives. `map_sorted_amounts' allows one to visit + * each amount in balance in the proper order for displaying to the + * user. Mostly used by `print' and other routinse where the sort + * order of the amounts' commodities is significant. + */ + void map_sorted_amounts(function fn) const; + + /** + * Printing methods. A balance may be output to a stream using the + * `print' method. There is also a global operator<< defined which + * simply calls print for a balance on the given stream. There is + * one form of the print method, which takes two required arguments + * and one arguments with a default value: + * + * print(ostream, int first_width, int latter_width) prints a + * balance to the given output stream, using each commodity's + * default display characteristics. The first_width parameter + * specifies the width that should be used for printing amounts + * (since they are likely to vary in width). The latter_width, if + * specified, gives the width to be used for each line after the + * first. This is useful when printing in a column which falls at + * the right-hand side of the screen. + * + * In addition to the width constraints, balances will also print + * with commodities in alphabetized order, regardless of the + * relative amounts of those commodities. There is no option to + * change this behavior. + */ + void print(std::ostream& out, + const int first_width = -1, + const int latter_width = -1, + const uint_least8_t flags = AMOUNT_PRINT_NO_FLAGS) const; + + /** + * Debugging methods. There are two methods defined to help with + * debugging: + * + * dump(ostream) dumps a balance to an output stream. There is + * little different from print(), it simply surrounds the display + * value with a marker, for example "BALANCE($1.00, DM 12.00)". + * This code is used by other dumping code elsewhere in Ledger. + * + * valid() returns true if the amounts within the balance are valid. + */ + void dump(std::ostream& out) const { + out << "BALANCE("; + bool first = true; + foreach (const amounts_map::value_type& pair, amounts) { + if (first) + first = false; + else + out << ", "; + pair.second.print(out); + } + out << ")"; + } + + bool valid() const { + foreach (const amounts_map::value_type& pair, amounts) + if (! pair.second.valid()) { + DEBUG("ledger.validate", "balance_t: ! pair.second.valid()"); + return false; + } + return true; + } +}; + +inline std::ostream& operator<<(std::ostream& out, const balance_t& bal) { + bal.print(out, 12); + return out; +} + +void put_balance(property_tree::ptree& pt, const balance_t& bal); + +} // namespace ledger + +#endif // _BALANCE_H diff --git a/src/chain.cc b/src/chain.cc new file mode 100644 index 00000000..d508dd3e --- /dev/null +++ b/src/chain.cc @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "chain.h" +#include "predicate.h" +#include "filters.h" +#include "report.h" +#include "session.h" + +namespace ledger { + +post_handler_ptr chain_pre_post_handlers(post_handler_ptr base_handler, + report_t& report) +{ + post_handler_ptr handler(base_handler); + + // anonymize_posts removes all meaningful information from xact payee's and + // account names, for the sake of creating useful bug reports. + if (report.HANDLED(anon)) + handler.reset(new anonymize_posts(handler)); + + // This filter_posts will only pass through posts matching the `predicate'. + if (report.HANDLED(limit_)) { + DEBUG("report.predicate", + "Report predicate expression = " << report.HANDLER(limit_).str()); + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + + // budget_posts takes a set of posts from a data file and uses them to + // generate "budget posts" which balance against the reported posts. + // + // forecast_posts is a lot like budget_posts, except that it adds xacts + // only for the future, and does not balance them against anything but the + // future balance. + + if (report.budget_flags != BUDGET_NO_BUDGET) { + budget_posts * budget_handler = + new budget_posts(handler, report.terminus.date(), report.budget_flags); + budget_handler->add_period_xacts(report.session.journal->period_xacts); + handler.reset(budget_handler); + + // Apply this before the budget handler, so that only matching posts are + // calculated toward the budget. The use of filter_posts above will + // further clean the results so that no automated posts that don't match + // the filter get reported. + if (report.HANDLED(limit_)) + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + else if (report.HANDLED(forecast_while_)) { + forecast_posts * forecast_handler + = new forecast_posts(handler, + predicate_t(report.HANDLER(forecast_while_).str(), + report.what_to_keep()), + report, + (report.HANDLED(forecast_years_) ? + lexical_cast + (report.HANDLER(forecast_years_).value) : 5UL)); + forecast_handler->add_period_xacts(report.session.journal->period_xacts); + handler.reset(forecast_handler); + + // See above, under budget_posts. + if (report.HANDLED(limit_)) + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + + return handler; +} + +post_handler_ptr chain_post_handlers(post_handler_ptr base_handler, + report_t& report, + bool for_accounts_report) +{ + post_handler_ptr handler(base_handler); + predicate_t display_predicate; + predicate_t only_predicate; + display_filter_posts * display_filter = NULL; + + expr_t& expr(report.HANDLER(amount_).expr); + expr.set_context(&report); + + report.HANDLER(total_).expr.set_context(&report); + report.HANDLER(display_amount_).expr.set_context(&report); + report.HANDLER(display_total_).expr.set_context(&report); + + if (! for_accounts_report) { + // Make sure only forecast postings which match are allowed through + if (report.HANDLED(forecast_while_)) { + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(forecast_while_).str(), + report.what_to_keep()), + report)); + } + + // truncate_xacts cuts off a certain number of _xacts_ from being + // displayed. It does not affect calculation. + if (report.HANDLED(head_) || report.HANDLED(tail_)) + handler.reset + (new truncate_xacts(handler, + report.HANDLED(head_) ? + lexical_cast(report.HANDLER(head_).value) : 0, + report.HANDLED(tail_) ? + lexical_cast(report.HANDLER(tail_).value) : 0)); + + // display_filter_posts adds virtual posts to the list to account + // for changes in value of commodities, which otherwise would affect + // the running total unpredictably. + display_filter = new display_filter_posts(handler, report, + report.HANDLED(revalued) && + ! report.HANDLED(no_rounding)); + handler.reset(display_filter); + + // filter_posts will only pass through posts matching the + // `display_predicate'. + if (report.HANDLED(display_)) { + display_predicate = predicate_t(report.HANDLER(display_).str(), + report.what_to_keep()); + handler.reset(new filter_posts(handler, display_predicate, report)); + } + } + + // changed_value_posts adds virtual posts to the list to account for changes + // in market value of commodities, which otherwise would affect the running + // total unpredictably. + if (report.HANDLED(revalued) && + (! for_accounts_report || report.HANDLED(unrealized))) + handler.reset(new changed_value_posts(handler, report, for_accounts_report, + report.HANDLED(unrealized), + display_filter)); + + // calc_posts computes the running total. When this appears will determine, + // for example, whether filtered posts are included or excluded from the + // running total. + handler.reset(new calc_posts(handler, expr, (! for_accounts_report || + (report.HANDLED(revalued) && + report.HANDLED(unrealized))))); + + // filter_posts will only pass through posts matching the + // `secondary_predicate'. + if (report.HANDLED(only_)) { + only_predicate = predicate_t(report.HANDLER(only_).str(), + report.what_to_keep()); + handler.reset(new filter_posts(handler, only_predicate, report)); + } + + if (! for_accounts_report) { + // sort_posts will sort all the posts it sees, based on the `sort_order' + // value expression. + if (report.HANDLED(sort_)) { + if (report.HANDLED(sort_xacts_)) + handler.reset(new sort_xacts(handler, expr_t(report.HANDLER(sort_).str()), report)); + else + handler.reset(new sort_posts(handler, report.HANDLER(sort_).str(), report)); + } + + // collapse_posts causes xacts with multiple posts to appear as xacts + // with a subtotaled post for each commodity used. + if (report.HANDLED(collapse)) + handler.reset(new collapse_posts(handler, report, expr, + display_predicate, only_predicate, + report.HANDLED(collapse_if_zero))); + + // subtotal_posts combines all the posts it receives into one subtotal + // xact, which has one post for each commodity in each account. + // + // period_posts is like subtotal_posts, but it subtotals according to time + // periods rather than totalling everything. + // + // day_of_week_posts is like period_posts, except that it reports + // all the posts that fall on each subsequent day of the week. + if (report.HANDLED(equity)) + handler.reset(new posts_as_equity(handler, report, expr)); + else if (report.HANDLED(subtotal)) + handler.reset(new subtotal_posts(handler, expr)); + } + + if (report.HANDLED(dow)) + handler.reset(new day_of_week_posts(handler, expr)); + else if (report.HANDLED(by_payee)) + handler.reset(new by_payee_posts(handler, expr)); + + // interval_posts groups posts together based on a time period, such as + // weekly or monthly. + if (report.HANDLED(period_)) + handler.reset(new interval_posts(handler, expr, + report.HANDLER(period_).str(), + report.HANDLED(exact), + report.HANDLED(empty))); + + if (report.HANDLED(date_)) + handler.reset(new transfer_details(handler, transfer_details::SET_DATE, + report.session.journal->master, + report.HANDLER(date_).str(), + report)); + + if (report.HANDLED(account_)) { + handler.reset(new transfer_details(handler, transfer_details::SET_ACCOUNT, + report.session.journal->master, + report.HANDLER(account_).str(), + report)); + } + else if (report.HANDLED(pivot_)) { + string pivot = report.HANDLER(pivot_).str(); + pivot = string("\"") + pivot + ":\" + tag(\"" + pivot + "\")"; + handler.reset(new transfer_details(handler, transfer_details::SET_ACCOUNT, + report.session.journal->master, pivot, + report)); + } + + if (report.HANDLED(payee_)) + handler.reset(new transfer_details(handler, transfer_details::SET_PAYEE, + report.session.journal->master, + report.HANDLER(payee_).str(), + report)); + + // related_posts will pass along all posts related to the post received. If + // the `related_all' handler is on, then all the xact's posts are passed; + // meaning that if one post of an xact is to be printed, all the post for + // that xact will be printed. + if (report.HANDLED(related)) + handler.reset(new related_posts(handler, report.HANDLED(related_all))); + + if (report.HANDLED(inject_)) + handler.reset(new inject_posts(handler, report.HANDLED(inject_).str(), + report.session.journal->master)); + + return handler; +} + +} // namespace ledger diff --git a/src/chain.h b/src/chain.h new file mode 100644 index 00000000..ff891238 --- /dev/null +++ b/src/chain.h @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup report + */ + +/** + * @file chain.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _CHAIN_H +#define _CHAIN_H + +#include "utils.h" + +namespace ledger { + +class post_t; +class account_t; + +template +class item_handler : public noncopyable +{ +protected: + shared_ptr handler; + +public: + item_handler() { + TRACE_CTOR(item_handler, ""); + } + item_handler(shared_ptr _handler) : handler(_handler) { + TRACE_CTOR(item_handler, "shared_ptr"); + } + virtual ~item_handler() { + TRACE_DTOR(item_handler); + } + + virtual void title(const string& str) { + if (handler) + handler->title(str); + } + + virtual void flush() { + if (handler) + handler->flush(); + } + virtual void operator()(T& item) { + if (handler) { + check_for_signal(); + (*handler)(item); + } + } + + virtual void clear() { + if (handler) + handler->clear(); + } +}; + +typedef shared_ptr > post_handler_ptr; +typedef shared_ptr > acct_handler_ptr; + +class report_t; + +post_handler_ptr +chain_pre_post_handlers(post_handler_ptr base_handler, + report_t& report); + +post_handler_ptr +chain_post_handlers(post_handler_ptr base_handler, + report_t& report, + bool for_accounts_report = false); + +inline post_handler_ptr +chain_handlers(post_handler_ptr handler, + report_t& report, + bool for_accounts_report = false) { + handler = chain_post_handlers(handler, report, for_accounts_report); + handler = chain_pre_post_handlers(handler, report); + return handler; +} + +} // namespace ledger + +#endif // _CHAIN_H diff --git a/src/commodity.cc b/src/commodity.cc new file mode 100644 index 00000000..bdf6ee54 --- /dev/null +++ b/src/commodity.cc @@ -0,0 +1,571 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +#include + +#include "amount.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" +#include "scope.h" + +namespace ledger { + +bool commodity_t::decimal_comma_by_default = false; +bool commodity_t::time_colon_by_default = false; + +void commodity_t::add_price(const datetime_t& date, const amount_t& price, + const bool reflexive) +{ + if (reflexive) { + DEBUG("history.find", "Marking " + << price.commodity().symbol() << " as a primary commodity"); + price.commodity().add_flags(COMMODITY_PRIMARY); + } else { + DEBUG("history.find", "Marking " << symbol() << " as a primary commodity"); + add_flags(COMMODITY_PRIMARY); + } + + DEBUG("history.find", "Adding price: " << symbol() + << " for " << price << " on " << date); + + pool().commodity_price_history.add_price(referent(), date, price); + + base->price_map.clear(); // a price was added, invalid the map +} + +void commodity_t::remove_price(const datetime_t& date, commodity_t& commodity) +{ + pool().commodity_price_history.remove_price(referent(), commodity, date); + + DEBUG("history.find", "Removing price: " << symbol() << " on " << date); + + base->price_map.clear(); // a price was added, invalid the map +} + +void commodity_t::map_prices(function fn, + const datetime_t& moment, + const datetime_t& _oldest, + bool bidirectionally) +{ + datetime_t when; + if (! moment.is_not_a_date_time()) + when = moment; + else if (epoch) + when = *epoch; + else + when = CURRENT_TIME(); + + pool().commodity_price_history.map_prices(fn, referent(), when, _oldest, + bidirectionally); +} + +optional +commodity_t::find_price_from_expr(expr_t& expr, const commodity_t * commodity, + const datetime_t& moment) const +{ +#if DEBUG_ON + if (SHOW_DEBUG("commodity.price.find")) { + ledger::_log_buffer << "valuation expr: "; + expr.dump(ledger::_log_buffer); + DEBUG("commodity.price.find", ""); + } +#endif + value_t result(expr.calc(*scope_t::default_scope)); + + if (is_expr(result)) { + value_t call_args; + + call_args.push_back(string_value(base_symbol())); + call_args.push_back(moment); + if (commodity) + call_args.push_back(string_value(commodity->symbol())); + + result = as_expr(result)->call(call_args, *scope_t::default_scope); + } + + return price_point_t(moment, result.to_amount()); +} + +optional +commodity_t::find_price(const commodity_t * commodity, + const datetime_t& moment, + const datetime_t& oldest) const +{ + DEBUG("commodity.price.find", "commodity_t::find_price(" << symbol() << ")"); + + const commodity_t * target = NULL; + if (commodity) + target = commodity; + else if (pool().default_commodity) + target = &*pool().default_commodity; + + if (target && this == target) + return none; + + base_t::memoized_price_entry entry(moment, oldest, + commodity ? commodity : NULL); + + DEBUG("commodity.price.find", "looking for memoized args: " + << (! moment.is_not_a_date_time() ? format_datetime(moment) : "NONE") << ", " + << (! oldest.is_not_a_date_time() ? format_datetime(oldest) : "NONE") << ", " + << (commodity ? commodity->symbol() : "NONE")); + { + base_t::memoized_price_map::iterator i = base->price_map.find(entry); + if (i != base->price_map.end()) { + DEBUG("commodity.price.find", "found! returning: " + << ((*i).second ? (*i).second->price : amount_t(0L))); + return (*i).second; + } + } + + datetime_t when; + if (! moment.is_not_a_date_time()) + when = moment; + else if (epoch) + when = *epoch; + else + when = CURRENT_TIME(); + + if (base->value_expr) + return find_price_from_expr(*base->value_expr, commodity, when); + + optional + point(target ? + pool().commodity_price_history.find_price(referent(), *target, + when, oldest) : + pool().commodity_price_history.find_price(referent(), when, oldest)); + + // Record this price point in the memoization map + if (base->price_map.size() > base_t::max_price_map_size) { + DEBUG("history.find", + "price map has grown too large, clearing it by half"); + for (std::size_t i = 0; i < base_t::max_price_map_size >> 1; i++) + base->price_map.erase(base->price_map.begin()); + } + + DEBUG("history.find", + "remembered: " << (point ? point->price : amount_t(0L))); + base->price_map.insert(base_t::memoized_price_map::value_type(entry, point)); + + return point; +} + +optional +commodity_t::check_for_updated_price(const optional& point, + const datetime_t& moment, + const commodity_t* in_terms_of) +{ + if (pool().get_quotes && ! has_flags(COMMODITY_NOMARKET)) { + bool exceeds_leeway = true; + + if (point) { + time_duration_t::sec_type seconds_diff; + if (! moment.is_not_a_date_time()) { + seconds_diff = (moment - point->when).total_seconds(); + DEBUG("commodity.download", "moment = " << moment); + DEBUG("commodity.download", "slip.moment = " << seconds_diff); + } else { + seconds_diff = (TRUE_CURRENT_TIME() - point->when).total_seconds(); + DEBUG("commodity.download", "slip.now = " << seconds_diff); + } + + DEBUG("commodity.download", "leeway = " << pool().quote_leeway); + if (seconds_diff < pool().quote_leeway) + exceeds_leeway = false; + } + + if (exceeds_leeway) { + DEBUG("commodity.download", + "attempting to download a more current quote..."); + if (optional quote = + pool().get_commodity_quote(referent(), in_terms_of)) { + if (! in_terms_of || + (quote->price.has_commodity() && + quote->price.commodity_ptr() == in_terms_of)) + return quote; + } + } + } + return point; +} + +commodity_t& commodity_t::nail_down(const expr_t& expr) +{ + annotation_t new_details; + + new_details.value_expr = expr; + new_details.add_flags(ANNOTATION_VALUE_EXPR_CALCULATED); + + return *pool().find_or_create(symbol(), new_details); +} + +commodity_t::operator bool() const +{ + return this != pool().null_commodity; +} + +namespace { + // Invalid commodity characters: + // SPACE, TAB, NEWLINE, RETURN + // 0-9 . , ; : ? ! - + * / ^ & | = + // < > { } [ ] ( ) @ + + static int invalid_chars[256] = { + /* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ + /* 00 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, + /* 10 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 20 */ 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, + /* 30 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 40 */ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 50 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, + /* 60 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 70 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, + /* 80 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 90 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* a0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* b0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* c0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* d0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* e0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* f0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + bool is_reserved_token(const char * buf) + { + switch (buf[0]) { + case 'a': + return std::strcmp(buf, "and") == 0; + case 'd': + return std::strcmp(buf, "div") == 0; + case 'e': + return std::strcmp(buf, "else") == 0; + case 'f': + return std::strcmp(buf, "false") == 0; + case 'i': + return std::strcmp(buf, "if") == 0; + case 'o': + return std::strcmp(buf, "or") == 0; + case 'n': + return std::strcmp(buf, "not") == 0; + case 't': + return std::strcmp(buf, "true") == 0; + } + return false; + } +} + +bool commodity_t::symbol_needs_quotes(const string& symbol) +{ + foreach (char ch, symbol) + if (invalid_chars[static_cast(ch)]) + return true; + + return false; +} + +void commodity_t::parse_symbol(std::istream& in, string& symbol) +{ + std::istream::pos_type pos = in.tellg(); + + char buf[256]; + char c = peek_next_nonws(in); + if (c == '"') { + in.get(c); + READ_INTO(in, buf, 255, c, c != '"'); + if (c == '"') + in.get(c); + else + throw_(amount_error, _("Quoted commodity symbol lacks closing quote")); + } else { + char * _p = buf; + while (_p - buf < 255 && in.good() && ! in.eof() && c != '\n') { + std::size_t bytes = 0; + std::ptrdiff_t size = _p - buf; + unsigned char d = static_cast(c); + + // Check for the start of a UTF-8 multi-byte encoded string + if (d >= 192 && d <= 223 && size < 254) + bytes = 2; + else if (d >= 224 && d <= 239 && size < 253) + bytes = 3; + else if (d >= 240 && d <= 247 && size < 252) + bytes = 4; + else if (d >= 248 && d <= 251 && size < 251) + bytes = 5; + else if (d >= 252 && d <= 253 && size < 250) + bytes = 6; + else if (d >= 254) // UTF-8 encoding error + break; + + if (bytes > 0) { // we're looking at a UTF-8 encoding + for (std::size_t i = 0; i < bytes; i++) { + in.get(c); + if (in.bad() || in.eof()) + throw_(amount_error, _("Invalid UTF-8 encoding for commodity name")); + *_p++ = c; + } + } + else if (invalid_chars[static_cast(c)]) { + break; + } + else { + in.get(c); + if (in.eof()) + break; + if (c == '\\') { + in.get(c); + if (in.eof()) + throw_(amount_error, _("Backslash at end of commodity name")); + } + *_p++ = c; + } + + c = static_cast(in.peek()); + } + *_p = '\0'; + + if (is_reserved_token(buf)) + buf[0] = '\0'; + } + symbol = buf; + + if (symbol.length() == 0) { + in.clear(); + in.seekg(pos, std::ios::beg); + } +} + +void commodity_t::parse_symbol(char *& p, string& symbol) +{ + if (*p == '"') { + char * q = std::strchr(p + 1, '"'); + if (! q) + throw_(amount_error, _("Quoted commodity symbol lacks closing quote")); + symbol = string(p + 1, 0, static_cast(q - p - 1)); + p = q + 2; + } else { + char * q = next_element(p); + symbol = p; + if (q) + p = q; + else + p += symbol.length(); + } + if (symbol.empty()) + throw_(amount_error, _("Failed to parse commodity")); +} + +void commodity_t::print(std::ostream& out, bool elide_quotes, bool) const +{ + string sym = symbol(); + if (elide_quotes && has_flags(COMMODITY_STYLE_SEPARATED) && + ! sym.empty() && sym[0] == '"' && + ! std::strchr(sym.c_str(), ' ')) { + string subsym(sym, 1, sym.length() - 2); + if (! all(subsym, is_digit())) + out << subsym; + else + out << sym; + } else + out << sym; +} + +bool commodity_t::valid() const +{ + if (symbol().empty() && this != pool().null_commodity) { + DEBUG("ledger.validate", + "commodity_t: symbol().empty() && this != null_commodity"); + return false; + } + + if (annotated && ! base) { + DEBUG("ledger.validate", "commodity_t: annotated && ! base"); + return false; + } + + if (precision() > 16) { + DEBUG("ledger.validate", "commodity_t: precision() > 16"); + return false; + } + + return true; +} + +int commodity_t::compare_by_commodity::operator()(const amount_t * left, + const amount_t * right) const +{ + commodity_t& leftcomm(left->commodity()); + commodity_t& rightcomm(right->commodity()); + + DEBUG("commodity.compare", " left symbol (" << leftcomm << ")"); + DEBUG("commodity.compare", "right symbol (" << rightcomm << ")"); + + int cmp = leftcomm.base_symbol().compare(rightcomm.base_symbol()); + if (cmp != 0) { + DEBUG("commodity.compare", "symbol is <"); + return cmp; + } + + if (! leftcomm.has_annotation() && rightcomm.has_annotation()) { + DEBUG("commodity.compare", "left has no annotation, right does"); + return -1; + } + else if (leftcomm.has_annotation() && ! rightcomm.has_annotation()) { + DEBUG("commodity.compare", "right has no annotation, left does"); + return 1; + } + else if (! leftcomm.has_annotation() && ! rightcomm.has_annotation()) { + DEBUG("commodity.compare", "there are no annotations, commodities match"); + return 0; + } + + annotated_commodity_t& aleftcomm(static_cast(leftcomm)); + annotated_commodity_t& arightcomm(static_cast(rightcomm)); + + if (! aleftcomm.details.price && arightcomm.details.price) { + DEBUG("commodity.compare", "left has no price, right does"); + return -1; + } + if (aleftcomm.details.price && ! arightcomm.details.price) { + DEBUG("commodity.compare", "right has no price, left does"); + return 1; + } + + if (aleftcomm.details.price && arightcomm.details.price) { + amount_t leftprice(*aleftcomm.details.price); + amount_t rightprice(*arightcomm.details.price); + + if (leftprice.commodity() != rightprice.commodity()) { + // Since we have two different amounts, there's really no way + // to establish a true sorting order; we'll just do it based + // on the numerical values. + leftprice.clear_commodity(); + rightprice.clear_commodity(); + DEBUG("commodity.compare", + "both have price, commodities don't match, recursing"); + int cmp2 = commodity_t::compare_by_commodity()(&leftprice, &rightprice); + if (cmp2 != 0) { + DEBUG("commodity.compare", "recursion found a disparity"); + return cmp2; + } + } else { + if (leftprice < rightprice) { + DEBUG("commodity.compare", "left price is less"); + return -1; + } + else if (leftprice > rightprice) { + DEBUG("commodity.compare", "left price is more"); + return 1; + } + } + } + + if (! aleftcomm.details.date && arightcomm.details.date) { + DEBUG("commodity.compare", "left has no date, right does"); + return -1; + } + if (aleftcomm.details.date && ! arightcomm.details.date) { + DEBUG("commodity.compare", "right has no date, left does"); + return 1; + } + + if (aleftcomm.details.date && arightcomm.details.date) { + gregorian::date_duration diff = + *aleftcomm.details.date - *arightcomm.details.date; + DEBUG("commodity.compare", "both have dates, comparing on difference"); + if (diff.is_negative()) { + DEBUG("commodity.compare", "dates differ"); + return -1; + } + + gregorian::date_duration diff2 = + *arightcomm.details.date - *aleftcomm.details.date; + if (diff2.is_negative()) { + DEBUG("commodity.compare", "dates differ"); + return 1; + } + } + + if (! aleftcomm.details.tag && arightcomm.details.tag) { + DEBUG("commodity.compare", "left has no tag, right does"); + return -1; + } + if (aleftcomm.details.tag && ! arightcomm.details.tag) { + DEBUG("commodity.compare", "right has no tag, left does"); + return 1; + } + + if (aleftcomm.details.tag && arightcomm.details.tag) { + DEBUG("commodity.compare", "both have tags, comparing lexically"); + if (*aleftcomm.details.tag < *arightcomm.details.tag) + return -1; + else if (*aleftcomm.details.tag > *arightcomm.details.tag) + return 1; + } + + if (! aleftcomm.details.value_expr && arightcomm.details.value_expr) { + DEBUG("commodity.compare", "left has no value expr, right does"); + return -1; + } + if (aleftcomm.details.value_expr && ! arightcomm.details.value_expr) { + DEBUG("commodity.compare", "right has no value expr, left does"); + return 1; + } + + if (aleftcomm.details.value_expr && arightcomm.details.value_expr) { + DEBUG("commodity.compare", "both have value exprs, comparing text reprs"); + return (aleftcomm.details.value_expr->text() < + arightcomm.details.value_expr->text()); + } + + DEBUG("commodity.compare", "the two are incomparable, which should never happen"); + assert(false); + return -1; +} + +void put_commodity(property_tree::ptree& st, const commodity_t& comm, + bool commodity_details) +{ + std::string flags; + if (! (comm.has_flags(COMMODITY_STYLE_SUFFIXED))) flags += 'P'; + if (comm.has_flags(COMMODITY_STYLE_SEPARATED)) flags += 'S'; + if (comm.has_flags(COMMODITY_STYLE_THOUSANDS)) flags += 'T'; + if (comm.has_flags(COMMODITY_STYLE_DECIMAL_COMMA)) flags += 'D'; + st.put(".flags", flags); + + st.put("symbol", comm.symbol()); + + if (commodity_details && comm.has_annotation()) + put_annotation(st.put("annotation", ""), as_annotated_commodity(comm).details); +} + +} // namespace ledger diff --git a/src/commodity.h b/src/commodity.h new file mode 100644 index 00000000..5060102d --- /dev/null +++ b/src/commodity.h @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup math + */ + +/** + * @file commodity.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Types for handling commodities + * + * This file contains one of the most basic types in Ledger: + * commodity_t, and its annotated cousin, annotated_commodity_t. + */ +#ifndef _COMMODITY_H +#define _COMMODITY_H + +#include "expr.h" + +namespace ledger { + +struct keep_details_t; +class commodity_pool_t; + +DECLARE_EXCEPTION(commodity_error, std::runtime_error); + +struct price_point_t +{ + datetime_t when; + amount_t price; + + price_point_t() {} + price_point_t(datetime_t _when, amount_t _price) + : when(_when), price(_price) {} + + bool operator==(const price_point_t& other) const { + return when == other.when && price == other.price; + } +}; + +class commodity_t + : public delegates_flags, + public equality_comparable1 +{ +protected: + friend class commodity_pool_t; + friend class annotated_commodity_t; + + class base_t : public noncopyable, public supports_flags + { + public: +#define COMMODITY_STYLE_DEFAULTS 0x000 +#define COMMODITY_STYLE_SUFFIXED 0x001 +#define COMMODITY_STYLE_SEPARATED 0x002 +#define COMMODITY_STYLE_DECIMAL_COMMA 0x004 +#define COMMODITY_STYLE_THOUSANDS 0x008 +#define COMMODITY_NOMARKET 0x010 +#define COMMODITY_BUILTIN 0x020 +#define COMMODITY_WALKED 0x040 +#define COMMODITY_KNOWN 0x080 +#define COMMODITY_PRIMARY 0x100 +#define COMMODITY_SAW_ANNOTATED 0x200 +#define COMMODITY_SAW_ANN_PRICE_FLOAT 0x400 +#define COMMODITY_SAW_ANN_PRICE_FIXATED 0x800 +#define COMMODITY_STYLE_TIME_COLON 0x1000 +#define COMMODITY_STYLE_NO_MIGRATE 0x2000 + + string symbol; + optional graph_index; + amount_t::precision_t precision; + optional name; + optional note; + optional smaller; + optional larger; + optional value_expr; + + typedef tuple memoized_price_entry; + typedef std::map > memoized_price_map; + + static const std::size_t max_price_map_size = 8; + mutable memoized_price_map price_map; + + public: + explicit base_t(const string& _symbol) + : supports_flags + (commodity_t::decimal_comma_by_default ? + static_cast(COMMODITY_STYLE_DECIMAL_COMMA) : + static_cast(COMMODITY_STYLE_DEFAULTS)), + symbol(_symbol), precision(0) { + TRACE_CTOR(commodity_t::base_t, "const string&"); + } + virtual ~base_t() { + TRACE_DTOR(commodity_t::base_t); + } + }; + + shared_ptr base; + + commodity_pool_t * parent_; + optional qualified_symbol; + bool annotated; + + explicit commodity_t(commodity_pool_t * _parent, + const shared_ptr& _base) + : delegates_flags(*_base.get()), base(_base), + parent_(_parent), annotated(false) { + TRACE_CTOR(commodity_t, "commodity_pool_t *, shared_ptr"); + } + +public: + static bool decimal_comma_by_default; + static bool time_colon_by_default; + + virtual ~commodity_t() { + TRACE_DTOR(commodity_t); + } + + operator bool() const; + + virtual bool operator==(const commodity_t& comm) const { + if (comm.annotated) + return comm == *this; + return base.get() == comm.base.get(); + } + bool operator==(const string& name) const { + return base_symbol() == name; + } + + static bool symbol_needs_quotes(const string& symbol); + + virtual commodity_t& referent() { + return *this; + } + virtual const commodity_t& referent() const { + return *this; + } + + bool has_annotation() const { + return annotated; + } + + virtual commodity_t& strip_annotations(const keep_details_t&) { + return *this; + } + virtual void write_annotations(std::ostream&, bool) const {} + + commodity_pool_t& pool() const { + return *parent_; + } + + string base_symbol() const { + return base->symbol; + } + string symbol() const { + return qualified_symbol ? *qualified_symbol : base_symbol(); + } + + optional graph_index() const {; + return base->graph_index; + } + void set_graph_index(const optional& arg = none) { + base->graph_index = arg; + } + + optional name() const { + return base->name; + } + void set_name(const optional& arg = none) { + base->name = arg; + } + + optional note() const { + return base->note; + } + void set_note(const optional& arg = none) { + base->note = arg; + } + + amount_t::precision_t precision() const { + return base->precision; + } + void set_precision(amount_t::precision_t arg) { + base->precision = arg; + } + + optional smaller() const { + return base->smaller; + } + void set_smaller(const optional& arg = none) { + base->smaller = arg; + } + + optional larger() const { + return base->larger; + } + void set_larger(const optional& arg = none) { + base->larger = arg; + } + + virtual optional value_expr() const { + return base->value_expr; + } + void set_value_expr(const optional& expr = none) { + base->value_expr = expr; + } + + void add_price(const datetime_t& date, const amount_t& price, + const bool reflexive = true); + void remove_price(const datetime_t& date, commodity_t& commodity); + + void map_prices(function fn, + const datetime_t& moment = datetime_t(), + const datetime_t& _oldest = datetime_t(), + bool bidirectionally = false); + + optional + find_price_from_expr(expr_t& expr, const commodity_t * commodity, + const datetime_t& moment) const; + + optional + virtual find_price(const commodity_t * commodity = NULL, + const datetime_t& moment = datetime_t(), + const datetime_t& oldest = datetime_t()) const; + + optional + check_for_updated_price(const optional& point, + const datetime_t& moment, + const commodity_t * in_terms_of); + + commodity_t& nail_down(const expr_t& expr); + + // Methods related to parsing, reading, writing, etc., the commodity + // itself. + + static void parse_symbol(std::istream& in, string& symbol); + static void parse_symbol(char *& p, string& symbol); + static string parse_symbol(std::istream& in) { + string temp; + parse_symbol(in, temp); + return temp; + } + + virtual void print(std::ostream& out, bool elide_quotes = false, + bool print_annotations = false) const; + bool valid() const; + + struct compare_by_commodity { + int operator()(const amount_t * left, const amount_t * right) const; + }; +}; + +inline std::ostream& operator<<(std::ostream& out, const commodity_t& comm) { + comm.print(out, false, true); + return out; +} + +void put_commodity(property_tree::ptree& pt, const commodity_t& comm, + bool commodity_details = false); + +//simple struct to allow std::map to compare commodities names +struct commodity_compare { + bool operator() (const commodity_t* lhs, const commodity_t* rhs) const { + return (lhs->symbol().compare(rhs->symbol()) < 0); + } +}; + +} // namespace ledger + +#endif // _COMMODITY_H diff --git a/src/compare.cc b/src/compare.cc new file mode 100644 index 00000000..63b85154 --- /dev/null +++ b/src/compare.cc @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "compare.h" +#include "op.h" +#include "scope.h" +#include "post.h" +#include "account.h" +#include "report.h" + +namespace ledger { + +void push_sort_value(std::list& sort_values, + expr_t::ptr_op_t node, scope_t& scope) +{ + if (node->kind == expr_t::op_t::O_CONS) { + while (node && node->kind == expr_t::op_t::O_CONS) { + push_sort_value(sort_values, node->left(), scope); + node = node->has_right() ? node->right() : NULL; + } + } else { + bool inverted = false; + + if (node->kind == expr_t::op_t::O_NEG) { + inverted = true; + node = node->left(); + } + + sort_values.push_back(sort_value_t()); + sort_values.back().inverted = inverted; + sort_values.back().value = expr_t(node).calc(scope).simplified(); + + if (sort_values.back().value.is_null()) + throw_(calc_error, + _("Could not determine sorting value based an expression")); + } +} + +template <> +void compare_items::find_sort_values( + std::list& sort_values, scope_t& scope) { + bind_scope_t bound_scope(report, scope); + push_sort_value(sort_values, sort_order.get_op(), bound_scope); +} + +template <> +void compare_items::find_sort_values( + std::list& sort_values, scope_t& scope) { + bind_scope_t bound_scope(report, scope); + push_sort_value(sort_values, sort_order.get_op(), bound_scope); +} + +template <> +bool compare_items::operator()(post_t * left, post_t * right) +{ + assert(left); + assert(right); + + post_t::xdata_t& lxdata(left->xdata()); + if (! lxdata.has_flags(POST_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *left); + find_sort_values(lxdata.sort_values, bound_scope); + lxdata.add_flags(POST_EXT_SORT_CALC); + } + + post_t::xdata_t& rxdata(right->xdata()); + if (! rxdata.has_flags(POST_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *right); + find_sort_values(rxdata.sort_values, bound_scope); + rxdata.add_flags(POST_EXT_SORT_CALC); + } + + return sort_value_is_less_than(lxdata.sort_values, rxdata.sort_values); +} + +template <> +bool compare_items::operator()(account_t * left, account_t * right) +{ + assert(left); + assert(right); + + account_t::xdata_t& lxdata(left->xdata()); + if (! lxdata.has_flags(ACCOUNT_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *left); + find_sort_values(lxdata.sort_values, bound_scope); + lxdata.add_flags(ACCOUNT_EXT_SORT_CALC); + } + + account_t::xdata_t& rxdata(right->xdata()); + if (! rxdata.has_flags(ACCOUNT_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *right); + find_sort_values(rxdata.sort_values, bound_scope); + rxdata.add_flags(ACCOUNT_EXT_SORT_CALC); + } + + DEBUG("value.sort", "Comparing accounts " << left->fullname() + << " <> " << right->fullname()); + + return sort_value_is_less_than(lxdata.sort_values, rxdata.sort_values); +} + +} // namespace ledger diff --git a/src/compare.h b/src/compare.h new file mode 100644 index 00000000..f1b6b9b3 --- /dev/null +++ b/src/compare.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup data + */ + +/** + * @file compare.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _COMPARE_H +#define _COMPARE_H + +#include "expr.h" + +namespace ledger { + +class post_t; +class account_t; +class report_t; + +void push_sort_value(std::list& sort_values, + expr_t::ptr_op_t node, scope_t& scope); + +template +class compare_items +{ + expr_t sort_order; + report_t& report; + + compare_items(); + +public: + compare_items(const expr_t& _sort_order, report_t& _report) : + sort_order(_sort_order), report(_report) { + TRACE_CTOR(compare_items, "const value_expr&, report_t&"); + } + compare_items(const compare_items& other) : + sort_order(other.sort_order), report(other.report) { + TRACE_CTOR(compare_items, "copy"); + } + ~compare_items() throw() { + TRACE_DTOR(compare_items); + } + + void find_sort_values(std::list& sort_values, scope_t& scope); + + bool operator()(T * left, T * right); +}; + +sort_value_t calc_sort_value(const expr_t::ptr_op_t op); + +template +bool compare_items::operator()(T * left, T * right) +{ + assert(left); assert(right); + return sort_value_is_less_than(find_sort_values(left), + find_sort_values(right)); +} + +template <> +bool compare_items::operator()(post_t * left, post_t * right); +template <> +bool compare_items::operator()(account_t * left, + account_t * right); + +} // namespace ledger + +#endif // _COMPARE_H diff --git a/src/context.h b/src/context.h new file mode 100644 index 00000000..ca7af060 --- /dev/null +++ b/src/context.h @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +/** + * @addtogroup data + */ + +/** + * @file context.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _CONTEXT_H +#define _CONTEXT_H + +#include "utils.h" +#include "times.h" + +namespace ledger { + +class journal_t; +class account_t; +class scope_t; + +class parse_context_t +{ +public: + static const std::size_t MAX_LINE = 4096; + + shared_ptr stream; + + path pathname; + path current_directory; + journal_t * journal; + account_t * master; + scope_t * scope; + char linebuf[MAX_LINE + 1]; + std::istream::pos_type line_beg_pos; + std::istream::pos_type curr_pos; + std::size_t linenum; + std::size_t errors; + std::size_t count; + std::size_t sequence; + std::string last; + + explicit parse_context_t(const path& cwd) + : current_directory(cwd), master(NULL), scope(NULL), + linenum(0), errors(0), count(0), sequence(1) {} + + explicit parse_context_t(shared_ptr _stream, + const path& cwd) + : stream(_stream), current_directory(cwd), master(NULL), + scope(NULL), linenum(0), errors(0), count(0), sequence(1) {} + + parse_context_t(const parse_context_t& context) + : stream(context.stream), + pathname(context.pathname), + current_directory(context.current_directory), + journal(context.journal), + master(context.master), + scope(context.scope), + line_beg_pos(context.line_beg_pos), + curr_pos(context.curr_pos), + linenum(context.linenum), + errors(context.errors), + count(context.count), + sequence(context.sequence) { + std::memcpy(linebuf, context.linebuf, MAX_LINE); + } + + string location() const { + return file_context(pathname, linenum); + } + + void warning(const string& what) const { + warning_func(location() + " " + what); + } + void warning(const boost::format& what) const { + warning_func(location() + " " + string(what.str())); + } +}; + +inline parse_context_t open_for_reading(const path& pathname, + const path& cwd) +{ + path filename = resolve_path(pathname); + filename = filesystem::absolute(filename, cwd); + if (! exists(filename) || is_directory(filename)) + throw_(std::runtime_error, + _f("Cannot read journal file %1%") % filename); + + path parent(filename.parent_path()); + shared_ptr stream(new ifstream(filename)); + parse_context_t context(stream, parent); + context.pathname = filename; + return context; +} + +class parse_context_stack_t +{ + std::list parsing_context; + +public: + void push() { + parsing_context.push_front(parse_context_t(filesystem::current_path())); + } + void push(shared_ptr stream, + const path& cwd = filesystem::current_path()) { + parsing_context.push_front(parse_context_t(stream, cwd)); + } + void push(const path& pathname, + const path& cwd = filesystem::current_path()) { + parsing_context.push_front(open_for_reading(pathname, cwd)); + } + + void push(const parse_context_t& context) { + parsing_context.push_front(context); + } + + void pop() { + assert(! parsing_context.empty()); + parsing_context.pop_front(); + } + + parse_context_t& get_current() { + assert(! parsing_context.empty()); + return parsing_context.front(); + } +}; + +} // namespace ledger + +#endif // _CONTEXT_H diff --git a/src/convert.cc b/src/convert.cc new file mode 100644 index 00000000..cea2a130 --- /dev/null +++ b/src/convert.cc @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "convert.h" +#include "csv.h" +#include "scope.h" +#include "iterators.h" +#include "report.h" +#include "xact.h" +#include "print.h" +#include "lookup.h" + +namespace ledger { + +value_t convert_command(call_scope_t& args) +{ + report_t& report(args.context()); + journal_t& journal(*report.session.journal.get()); + + string bucket_name; + if (report.HANDLED(account_)) + bucket_name = report.HANDLER(account_).str(); + else + bucket_name = _("Equity:Unknown"); + + account_t * bucket = journal.master->find_account(bucket_name); + account_t * unknown = journal.master->find_account(_("Expenses:Unknown")); + + // Create a flat list + xacts_list current_xacts(journal.xacts_begin(), journal.xacts_end()); + + // Read in the series of transactions from the CSV file + + print_xacts formatter(report); + path csv_file_path(args.get(0)); + + report.session.parsing_context.push(csv_file_path); + parse_context_t& context(report.session.parsing_context.get_current()); + context.journal = &journal; + context.master = bucket; + + csv_reader reader(context); + + try { + while (xact_t * xact = reader.read_xact(report.HANDLED(rich_data))) { + if (report.HANDLED(invert)) { + foreach (post_t * post, xact->posts) + post->amount.in_place_negate(); + } + + string ref = (xact->has_tag(_("UUID")) ? + xact->get_tag(_("UUID"))->to_string() : + sha1sum(reader.get_last_line())); + + checksum_map_t::const_iterator entry = journal.checksum_map.find(ref); + if (entry != journal.checksum_map.end()) { + INFO(file_context(reader.get_pathname(), + reader.get_linenum()) + << " " << "Ignoring known UUID " << ref); + checked_delete(xact); // ignore it + continue; + } + + if (report.HANDLED(rich_data) && ! xact->has_tag(_("UUID"))) + xact->set_tag(_("UUID"), string_value(ref)); + + if (xact->posts.front()->account == NULL) { + if (account_t * acct = + (report.HANDLED(auto_match) ? + lookup_probable_account(xact->payee, current_xacts.rbegin(), + current_xacts.rend(), bucket).second : + NULL)) + xact->posts.front()->account = acct; + else + xact->posts.front()->account = unknown; + } + + if (! journal.add_xact(xact)) { + checked_delete(xact); + throw_(std::runtime_error, + _("Failed to finalize derived transaction (check commodities)")); + } + else { + xact_posts_iterator xact_iter(*xact); + while (post_t * post = *xact_iter++) + formatter(*post); + } + } + formatter.flush(); + } + catch (const std::exception&) { + add_error_context(_f("While parsing file %1%") + % file_context(reader.get_pathname(), + reader.get_linenum())); + add_error_context(_("While parsing CSV line:")); + add_error_context(line_context(reader.get_last_line())); + throw; + } + + // If not, transform the payee according to regexps + + // Set the account to a default vaule, then transform the account according + // to the payee + + // Print out the final form of the transaction + + return true; +} + +} // namespace ledger diff --git a/src/convert.h b/src/convert.h new file mode 100644 index 00000000..e27b5872 --- /dev/null +++ b/src/convert.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup data + */ + +/** + * @file convert.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _CONVERT_H +#define _CONVERT_H + +#include "value.h" + +namespace ledger { + +class call_scope_t; + +value_t convert_command(call_scope_t& scope); + +} // namespace ledger + +#endif // _CONVERT_H diff --git a/src/csv.cc b/src/csv.cc new file mode 100644 index 00000000..40b8e01b --- /dev/null +++ b/src/csv.cc @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "csv.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "journal.h" +#include "pool.h" + +namespace ledger { + +string csv_reader::read_field(std::istream& in) +{ + string field; + + char c; + if (in.peek() == '"' || in.peek() == '|') { + in.get(c); + char x; + while (in.good() && ! in.eof()) { + in.get(x); + if (x == '\\') { + in.get(x); + } + else if (x == '"' && in.peek() == '"') { + in.get(x); + } + else if (x == c) { + if (x == '|') + in.unget(); + else if (in.peek() == ',') + in.get(c); + break; + } + if (x != '\0') + field += x; + } + } + else { + while (in.good() && ! in.eof()) { + in.get(c); + if (in.good()) { + if (c == ',') + break; + if (c != '\0') + field += c; + } + } + } + trim(field); + return field; +} + +char * csv_reader::next_line(std::istream& in) +{ + while (in.good() && ! in.eof() && in.peek() == '#') + in.getline(context.linebuf, parse_context_t::MAX_LINE); + + if (! in.good() || in.eof() || in.peek() == -1) + return NULL; + + in.getline(context.linebuf, parse_context_t::MAX_LINE); + + return context.linebuf; +} + +void csv_reader::read_index(std::istream& in) +{ + char * line = next_line(in); + if (! line) + return; + + std::istringstream instr(line); + + while (instr.good() && ! instr.eof()) { + string field = read_field(instr); + names.push_back(field); + + if (date_mask.match(field)) + index.push_back(FIELD_DATE); + else if (date_aux_mask.match(field)) + index.push_back(FIELD_DATE_AUX); + else if (code_mask.match(field)) + index.push_back(FIELD_CODE); + else if (payee_mask.match(field)) + index.push_back(FIELD_PAYEE); + else if (amount_mask.match(field)) + index.push_back(FIELD_AMOUNT); + else if (cost_mask.match(field)) + index.push_back(FIELD_COST); + else if (total_mask.match(field)) + index.push_back(FIELD_TOTAL); + else if (note_mask.match(field)) + index.push_back(FIELD_NOTE); + else + index.push_back(FIELD_UNKNOWN); + + DEBUG("csv.parse", "Header field: " << field); + } +} + +xact_t * csv_reader::read_xact(bool rich_data) +{ + char * line = next_line(*context.stream.get()); + if (! line || index.empty()) + return NULL; + context.linenum++; + + std::istringstream instr(line); + + unique_ptr xact(new xact_t); + unique_ptr post(new post_t); + + xact->set_state(item_t::CLEARED); + + xact->pos = position_t(); + xact->pos->pathname = context.pathname; + xact->pos->beg_pos = context.stream->tellg(); + xact->pos->beg_line = context.linenum; + xact->pos->sequence = context.sequence++; + + post->xact = xact.get(); + + post->pos = position_t(); + post->pos->pathname = context.pathname; + post->pos->beg_pos = context.stream->tellg(); + post->pos->beg_line = context.linenum; + post->pos->sequence = context.sequence++; + + post->set_state(item_t::CLEARED); + post->account = NULL; + + std::vector::size_type n = 0; + amount_t amt; + string total; + string field; + + while (instr.good() && ! instr.eof() && n < index.size()) { + field = read_field(instr); + + switch (index[n]) { + case FIELD_DATE: + xact->_date = parse_date(field); + break; + + case FIELD_DATE_AUX: + if (! field.empty()) + xact->_date_aux = parse_date(field); + break; + + case FIELD_CODE: + if (! field.empty()) + xact->code = field; + break; + + case FIELD_PAYEE: { + bool found = false; + foreach (payee_alias_mapping_t& value, context.journal->payee_alias_mappings) { + DEBUG("csv.mappings", "Looking for payee mapping: " << value.first); + if (value.first.match(field)) { + xact->payee = value.second; + found = true; + break; + } + } + if (! found) + xact->payee = field; + break; + } + + case FIELD_AMOUNT: { + std::istringstream amount_str(field); + amt.parse(amount_str, PARSE_NO_REDUCE); + if (! amt.has_commodity() && + commodity_pool_t::current_pool->default_commodity) + amt.set_commodity(*commodity_pool_t::current_pool->default_commodity); + post->amount = amt; + break; + } + + case FIELD_COST: { + std::istringstream amount_str(field); + amt.parse(amount_str, PARSE_NO_REDUCE); + if (! amt.has_commodity() && + commodity_pool_t::current_pool->default_commodity) + amt.set_commodity + (*commodity_pool_t::current_pool->default_commodity); + post->cost = amt; + break; + } + + case FIELD_TOTAL: + total = field; + break; + + case FIELD_NOTE: + if (! field.empty()) + xact->note = field; + break; + + case FIELD_UNKNOWN: + if (! names[n].empty() && ! field.empty()) + xact->set_tag(names[n], string_value(field)); + break; + } + n++; + } + + if (rich_data) { + xact->set_tag(_("Imported"), + string_value(format_date(CURRENT_DATE(), FMT_WRITTEN))); + xact->set_tag(_("CSV"), string_value(line)); + } + + // Translate the account name, if we have enough information to do so + + foreach (account_mapping_t& value, context.journal->payees_for_unknown_accounts) { + if (value.first.match(xact->payee)) { + post->account = value.second; + break; + } + } + + xact->add_post(post.release()); + + // Create the "balancing post", which refers to the account for this data + + post.reset(new post_t); + + post->xact = xact.get(); + + post->pos = position_t(); + post->pos->pathname = context.pathname; + post->pos->beg_pos = context.stream->tellg(); + post->pos->beg_line = context.linenum; + post->pos->sequence = context.sequence++; + + post->set_state(item_t::CLEARED); + post->account = context.master; + + if (! amt.is_null()) + post->amount = - amt; + + if (! total.empty()) { + std::istringstream assigned_amount_str(total); + amt.parse(assigned_amount_str, PARSE_NO_REDUCE); + if (! amt.has_commodity() && + commodity_pool_t::current_pool->default_commodity) + amt.set_commodity(*commodity_pool_t::current_pool->default_commodity); + post->assigned_amount = amt; + } + + xact->add_post(post.release()); + + return xact.release(); +} + +} // namespace ledger diff --git a/src/csv.h b/src/csv.h new file mode 100644 index 00000000..274ecc7e --- /dev/null +++ b/src/csv.h @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup data + */ + +/** + * @file csv.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _CSV_H +#define _CSV_H + +#include "value.h" +#include "context.h" + +namespace ledger { + +class xact_t; +class journal_t; +class account_t; + +class csv_reader +{ + parse_context_t context; + + enum headers_t { + FIELD_DATE = 0, + FIELD_DATE_AUX, + FIELD_CODE, + FIELD_PAYEE, + FIELD_AMOUNT, + FIELD_COST, + FIELD_TOTAL, + FIELD_NOTE, + + FIELD_UNKNOWN + }; + + mask_t date_mask; + mask_t date_aux_mask; + mask_t code_mask; + mask_t payee_mask; + mask_t amount_mask; + mask_t cost_mask; + mask_t total_mask; + mask_t note_mask; + + std::vector index; + std::vector names; + +public: + csv_reader(parse_context_t& _context) + : context(_context), + date_mask("date"), + date_aux_mask("posted( ?date)?"), + code_mask("code"), + payee_mask("(payee|desc(ription)?|title)"), + amount_mask("amount"), + cost_mask("cost"), + total_mask("total"), + note_mask("note") { + read_index(*context.stream.get()); + TRACE_CTOR(csv_reader, "parse_context_t&"); + } + ~csv_reader() { + TRACE_DTOR(csv_reader); + } + + void read_index(std::istream& in); + string read_field(std::istream& in); + char * next_line(std::istream& in); + + xact_t * read_xact(bool rich_data); + + const char * get_last_line() const { + return context.linebuf; + } + path get_pathname() const { + return context.pathname; + } + std::size_t get_linenum() const { + return context.linenum; + } +}; + +} // namespace ledger + +#endif // _CSV_H diff --git a/src/draft.cc b/src/draft.cc new file mode 100644 index 00000000..12d7faf7 --- /dev/null +++ b/src/draft.cc @@ -0,0 +1,532 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "draft.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "journal.h" +#include "session.h" +#include "report.h" +#include "lookup.h" +#include "print.h" + +namespace ledger { + +void draft_t::xact_template_t::dump(std::ostream& out) const +{ + if (date) + out << _("Date: ") << *date << std::endl; + else + out << _("Date: ") << std::endl; + + if (code) + out << _("Code: ") << *code << std::endl; + if (note) + out << _("Note: ") << *note << std::endl; + + if (payee_mask.empty()) + out << _("Payee mask: INVALID (template expression will cause an error)") + << std::endl; + else + out << _("Payee mask: ") << payee_mask << std::endl; + + if (posts.empty()) { + out << std::endl + << _("") + << std::endl; + } else { + foreach (const post_template_t& post, posts) { + out << std::endl + << _f("[Posting \"%1%\"]") % (post.from ? _("from") : _("to")) + << std::endl; + + if (post.account_mask) + out << _(" Account mask: ") << *post.account_mask << std::endl; + else if (post.from) + out << _(" Account mask: ") << std::endl; + else + out << _(" Account mask: ") << std::endl; + + if (post.amount) + out << _(" Amount: ") << *post.amount << std::endl; + + if (post.cost) + out << _(" Cost: ") << *post.cost_operator + << " " << *post.cost << std::endl; + } + } +} + +void draft_t::parse_args(const value_t& args) +{ + regex date_mask(_("([0-9]+(?:[-/.][0-9]+)?(?:[-/.][0-9]+))?")); + smatch what; + bool check_for_date = true; + + tmpl = xact_template_t(); + + optional weekday; + xact_template_t::post_template_t * post = NULL; + value_t::sequence_t::const_iterator begin = args.begin(); + value_t::sequence_t::const_iterator end = args.end(); + + for (; begin != end; begin++) { + if (check_for_date && + regex_match((*begin).to_string(), what, date_mask)) { + tmpl->date = parse_date(what[0]); + check_for_date = false; + } + else if (check_for_date && + bool(weekday = string_to_day_of_week(what[0]))) { +#if defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 7 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + short dow = static_cast(*weekday); +#if defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 7 +#pragma GCC diagnostic pop +#endif + date_t date = CURRENT_DATE() - date_duration(1); + while (date.day_of_week() != dow) + date -= date_duration(1); + tmpl->date = date; + check_for_date = false; + } + else { + string arg = (*begin).to_string(); + + if (arg == "at") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->payee_mask = (*++begin).to_string(); + } + else if (arg == "to" || arg == "from") { + if (! post || post->account_mask) { + tmpl->posts.push_back(xact_template_t::post_template_t()); + post = &tmpl->posts.back(); + } + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + post->account_mask = mask_t((*++begin).to_string()); + post->from = arg == "from"; + } + else if (arg == "on") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->date = parse_date((*++begin).to_string()); + check_for_date = false; + } + else if (arg == "code") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->code = (*++begin).to_string(); + } + else if (arg == "note") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->note = (*++begin).to_string(); + } + else if (arg == "rest") { + ; // just ignore this argument + } + else if (arg == "@" || arg == "@@") { + amount_t cost; + post->cost_operator = arg; + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + arg = (*++begin).to_string(); + if (! cost.parse(arg, PARSE_SOFT_FAIL | PARSE_NO_MIGRATE)) + throw std::runtime_error(_("Invalid xact command arguments")); + post->cost = cost; + } + else { + // Without a preposition, it is either: + // + // A payee, if we have not seen one + // An account or an amount, if we have + // An account if an amount has just been seen + // An amount if an account has just been seen + + if (tmpl->payee_mask.empty()) { + tmpl->payee_mask = arg; + } else { + amount_t amt; + optional account; + + if (! amt.parse(arg, PARSE_SOFT_FAIL | PARSE_NO_MIGRATE)) + account = mask_t(arg); + + if (! post || + (account && post->account_mask) || + (! account && post->amount)) { + tmpl->posts.push_back(xact_template_t::post_template_t()); + post = &tmpl->posts.back(); + } + + if (account) { + post->account_mask = account; + } else { + post->amount = amt; + post = NULL; // an amount concludes this posting + } + } + } + } + } + + if (! tmpl->posts.empty()) { + bool has_only_from = true; + bool has_only_to = true; + + // A single account at the end of the line is the "from" account + if (tmpl->posts.size() > 1 && + tmpl->posts.back().account_mask && ! tmpl->posts.back().amount) + tmpl->posts.back().from = true; + + foreach (xact_template_t::post_template_t& post_tmpl, tmpl->posts) { + if (post_tmpl.from) + has_only_to = false; + else + has_only_from = false; + } + + if (has_only_from) { + tmpl->posts.push_front(xact_template_t::post_template_t()); + } + else if (has_only_to) { + tmpl->posts.push_back(xact_template_t::post_template_t()); + tmpl->posts.back().from = true; + } + } +} + +xact_t * draft_t::insert(journal_t& journal) +{ + if (! tmpl) + return NULL; + + if (tmpl->payee_mask.empty()) + throw std::runtime_error(_("'xact' command requires at least a payee")); + + xact_t * matching = NULL; + unique_ptr added(new xact_t); + + if (xact_t * xact = + lookup_probable_account(tmpl->payee_mask.str(), journal.xacts.rbegin(), + journal.xacts.rend()).first) { + DEBUG("draft.xact", "Found payee by lookup: transaction on line " + << xact->pos->beg_line); + matching = xact; + } else { + for (xacts_list::reverse_iterator j = journal.xacts.rbegin(); + j != journal.xacts.rend(); + j++) { + if (tmpl->payee_mask.match((*j)->payee)) { + matching = *j; + DEBUG("draft.xact", + "Found payee match: transaction on line " << (*j)->pos->beg_line); + break; + } + } + } + + if (! tmpl->date) { + added->_date = CURRENT_DATE(); + DEBUG("draft.xact", "Setting date to current date"); + } else { + added->_date = tmpl->date; + DEBUG("draft.xact", "Setting date to template date: " << *tmpl->date); + } + + added->set_state(item_t::UNCLEARED); + + if (matching) { + added->payee = matching->payee; + //added->code = matching->code; + //added->note = matching->note; + +#if DEBUG_ON + DEBUG("draft.xact", "Setting payee from match: " << added->payee); + //if (added->code) + // DEBUG("draft.xact", "Setting code from match: " << *added->code); + //if (added->note) + // DEBUG("draft.xact", "Setting note from match: " << *added->note); +#endif + } else { + added->payee = tmpl->payee_mask.str(); + DEBUG("draft.xact", "Setting payee from template: " << added->payee); + } + + if (tmpl->code) { + added->code = tmpl->code; + DEBUG("draft.xact", "Now setting code from template: " << *added->code); + } + if (tmpl->note) { + added->note = tmpl->note; + DEBUG("draft.xact", "Now setting note from template: " << *added->note); + } + + if (tmpl->posts.empty()) { + if (matching) { + DEBUG("draft.xact", "Template had no postings, copying from match"); + + foreach (post_t * post, matching->posts) { + added->add_post(new post_t(*post)); + added->posts.back()->set_state(item_t::UNCLEARED); + } + } else { + throw_(std::runtime_error, + _f("No accounts, and no past transaction matching '%1%'") + % tmpl->payee_mask); + } + } else { + DEBUG("draft.xact", "Template had postings"); + + bool any_post_has_amount = false; + foreach (xact_template_t::post_template_t& post, tmpl->posts) { + if (post.amount) { + DEBUG("draft.xact", " and at least one has an amount specified"); + any_post_has_amount = true; + break; + } + } + + foreach (xact_template_t::post_template_t& post, tmpl->posts) { + unique_ptr new_post; + + commodity_t * found_commodity = NULL; + + if (matching) { + if (post.account_mask) { + DEBUG("draft.xact", + "Looking for matching posting based on account mask"); + + foreach (post_t * x, matching->posts) { + if (post.account_mask->match(x->account->fullname())) { + new_post.reset(new post_t(*x)); + DEBUG("draft.xact", + "Founding posting from line " << x->pos->beg_line); + break; + } + } + } else { + if (post.from) { + for (posts_list::reverse_iterator j = matching->posts.rbegin(); + j != matching->posts.rend(); + j++) { + if ((*j)->must_balance()) { + new_post.reset(new post_t(**j)); + DEBUG("draft.xact", + "Copied last real posting from matching"); + break; + } + } + } else { + for (posts_list::iterator j = matching->posts.begin(); + j != matching->posts.end(); + j++) { + if ((*j)->must_balance()) { + new_post.reset(new post_t(**j)); + DEBUG("draft.xact", + "Copied first real posting from matching"); + break; + } + } + } + } + } + + if (! new_post.get()) { + new_post.reset(new post_t); + DEBUG("draft.xact", "New posting was NULL, creating a blank one"); + } + + if (! new_post->account) { + DEBUG("draft.xact", "New posting still needs an account"); + + if (post.account_mask) { + DEBUG("draft.xact", "The template has an account mask"); + + account_t * acct = NULL; + if (! acct) { + acct = journal.find_account_re(post.account_mask->str()); +#if DEBUG_ON + if (acct) + DEBUG("draft.xact", "Found account as a regular expression"); +#endif + } + if (! acct) { + acct = journal.find_account(post.account_mask->str()); +#if DEBUG_ON + if (acct) + DEBUG("draft.xact", "Found (or created) account by name"); +#endif + } + + // Find out the default commodity to use by looking at the last + // commodity used in that account + for (xacts_list::reverse_iterator j = journal.xacts.rbegin(); + j != journal.xacts.rend(); + j++) { + foreach (post_t * x, (*j)->posts) { + if (x->account == acct && ! x->amount.is_null()) { + new_post.reset(new post_t(*x)); + DEBUG("draft.xact", + "Found account in journal postings, setting new posting"); + break; + } + } + } + + new_post->account = acct; + DEBUG("draft.xact", + "Set new posting's account to: " << acct->fullname()); + } else { + if (post.from) { + new_post->account = journal.find_account(_("Liabilities:Unknown")); + DEBUG("draft.xact", + "Set new posting's account to: Liabilities:Unknown"); + } else { + new_post->account = journal.find_account(_("Expenses:Unknown")); + DEBUG("draft.xact", + "Set new posting's account to: Expenses:Unknown"); + } + } + } + assert(new_post->account); + + if (new_post.get() && ! new_post->amount.is_null()) { + found_commodity = &new_post->amount.commodity(); + + if (any_post_has_amount) { + new_post->amount = amount_t(); + DEBUG("draft.xact", "New posting has an amount, but we cleared it"); + } else { + any_post_has_amount = true; + DEBUG("draft.xact", "New posting has an amount, and we're using it"); + } + } + + if (post.amount) { + new_post->amount = *post.amount; + DEBUG("draft.xact", "Copied over posting amount"); + + if (post.from) { + new_post->amount.in_place_negate(); + DEBUG("draft.xact", "Negated new posting amount"); + } + } + + if (post.cost) { + if (post.cost->sign() < 0) + throw parse_error(_("A posting's cost may not be negative")); + + post.cost->in_place_unround(); + + if (*post.cost_operator == "@") { + // For the sole case where the cost might be uncommoditized, + // guarantee that the commodity of the cost after multiplication + // is the same as it was before. + commodity_t& cost_commodity(post.cost->commodity()); + *post.cost *= new_post->amount; + post.cost->set_commodity(cost_commodity); + } + else if (new_post->amount.sign() < 0) { + new_post->cost->in_place_negate(); + } + + new_post->cost = *post.cost; + DEBUG("draft.xact", "Copied over posting cost"); + } + + if (found_commodity && + ! new_post->amount.is_null() && + ! new_post->amount.has_commodity()) { + new_post->amount.set_commodity(*found_commodity); + DEBUG("draft.xact", "Set posting amount commodity to: " + << new_post->amount.commodity()); + + new_post->amount = new_post->amount.rounded(); + DEBUG("draft.xact", + "Rounded posting amount to: " << new_post->amount); + } + + added->add_post(new_post.release()); + added->posts.back()->account->add_post(added->posts.back()); + added->posts.back()->set_state(item_t::UNCLEARED); + + DEBUG("draft.xact", "Added new posting to derived entry"); + } + } + + if (! journal.add_xact(added.get())) + throw_(std::runtime_error, + _("Failed to finalize derived transaction (check commodities)")); + + return added.release(); +} + +value_t template_command(call_scope_t& args) +{ + report_t& report(find_scope(args)); + std::ostream& out(report.output_stream); + + out << _("--- Input arguments ---") << std::endl; + args.value().dump(out); + out << std::endl << std::endl; + + draft_t draft(args.value()); + out << _("--- Transaction template ---") << std::endl; + draft.dump(out); + + return true; +} + +value_t xact_command(call_scope_t& args) +{ + report_t& report(find_scope(args)); + draft_t draft(args.value()); + + unique_ptr new_xact(draft.insert(*report.session.journal.get())); + if (new_xact.get()) { + // Only consider actual postings for the "xact" command + report.HANDLER(limit_).on("#xact", "actual"); + + report.xact_report(post_handler_ptr(new print_xacts(report)), *new_xact.get()); + } + + return true; +} + +} // namespace ledger diff --git a/src/draft.h b/src/draft.h new file mode 100644 index 00000000..44c3bf3d --- /dev/null +++ b/src/draft.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup expr + */ + +/** + * @file draft.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _DRAFT_H +#define _DRAFT_H + +#include "exprbase.h" +#include "value.h" + +namespace ledger { + +class journal_t; +class xact_t; + +class draft_t : public expr_base_t +{ + typedef expr_base_t base_type; + + struct xact_template_t + { + optional date; + optional code; + optional note; + mask_t payee_mask; + + struct post_template_t { + bool from; + optional account_mask; + optional amount; + optional cost_operator; + optional cost; + + post_template_t() : from(false) { + TRACE_CTOR(post_template_t, ""); + } + ~post_template_t() throw() { + TRACE_DTOR(post_template_t); + } + }; + + std::list posts; + + xact_template_t() { + TRACE_CTOR(xact_template_t, ""); + } + xact_template_t(const xact_template_t& other) + : date(other.date), + code(other.code), + note(other.note), + payee_mask(other.payee_mask), + posts(other.posts) + { + TRACE_CTOR(xact_template_t, "copy"); + } + ~xact_template_t() throw() { + TRACE_DTOR(xact_template_t); + } + + void dump(std::ostream& out) const; + }; + + optional tmpl; + +public: + draft_t(const value_t& args) : base_type() { + if (! args.empty()) + parse_args(args); + TRACE_CTOR(draft_t, "value_t"); + } + virtual ~draft_t() throw() { + TRACE_DTOR(draft_t); + } + + void parse_args(const value_t& args); + + virtual result_type real_calc(scope_t&) { + assert(false); + return true; + } + + xact_t * insert(journal_t& journal); + + virtual void dump(std::ostream& out) const { + if (tmpl) + tmpl->dump(out); + } +}; + +value_t xact_command(call_scope_t& args); +value_t template_command(call_scope_t& args); + +} // namespace ledger + +#endif // _DRAFT_H diff --git a/src/emacs.cc b/src/emacs.cc new file mode 100644 index 00000000..52389316 --- /dev/null +++ b/src/emacs.cc @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include +#include +#include "emacs.h" +#include "xact.h" +#include "post.h" +#include "account.h" + +namespace ledger { + + void format_emacs_posts::write_xact(xact_t& xact) + { + if (xact.pos) + out << "\"" << xact.pos->pathname.string() << "\" " + << xact.pos->beg_line << " "; + else + out << "\"\" " << -1 << " "; + + tm when = gregorian::to_tm(xact.date()); + std::time_t date = std::mktime(&when); + + out << "(" << (date / 65536) << " " << (date % 65536) << " 0) "; + + if (xact.code) + out << "\"" << *xact.code << "\" "; + else + out << "nil "; + + if (xact.payee.empty()) + out << "nil"; + else + out << "\"" << xact.payee << "\""; + + out << "\n"; + } + + void format_emacs_posts::operator()(post_t& post) + { + if (! post.has_xdata() || + ! post.xdata().has_flags(POST_EXT_DISPLAYED)) { + if (! last_xact) { + out << "(("; + write_xact(*post.xact); + } + else if (post.xact != last_xact) { + out << ")\n ("; + write_xact(*post.xact); + } + else { + out << "\n"; + } + + if (post.pos) + out << " (" << post.pos->beg_line << " "; + else + out << " (" << -1 << " "; + + out << "\"" << post.reported_account()->fullname() << "\" \"" + << post.amount << "\""; + + switch (post.state()) { + case item_t::UNCLEARED: + out << " nil"; + break; + case item_t::CLEARED: + out << " t"; + break; + case item_t::PENDING: + out << " pending"; + break; + } + + if (post.cost) + out << " \"" << *post.cost << "\""; + if (post.note) + out << " \"" << escape_string(*post.note) << "\""; + out << ")"; + + last_xact = post.xact; + + post.xdata().add_flags(POST_EXT_DISPLAYED); + } + } + + string format_emacs_posts::escape_string(string raw){ + replace_all(raw, "\\", "\\\\"); + replace_all(raw, "\"", "\\\""); + return raw; + } + +} // namespace ledger diff --git a/src/emacs.h b/src/emacs.h new file mode 100644 index 00000000..6e428613 --- /dev/null +++ b/src/emacs.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup report + */ + +/** + * @file emacs.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _EMACS_H +#define _EMACS_H + +#include "chain.h" + +namespace ledger { + +class xact_t; + +class format_emacs_posts : public item_handler +{ + format_emacs_posts(); + +protected: + std::ostream& out; + xact_t * last_xact; + +public: + format_emacs_posts(std::ostream& _out) + : out(_out), last_xact(NULL) { + TRACE_CTOR(format_emacs_posts, "std::ostream&"); + } + ~format_emacs_posts() { + TRACE_DTOR(format_emacs_posts); + } + + virtual void write_xact(xact_t& xact); + virtual void flush() { + if (last_xact) + out << "))\n"; + out.flush(); + } + virtual void operator()(post_t& post); + virtual string escape_string(string raw); +}; + +} // namespace ledger + +#endif // _REPORT_H diff --git a/src/error.cc b/src/error.cc new file mode 100644 index 00000000..1ab92840 --- /dev/null +++ b/src/error.cc @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +#include + +#include "utils.h" + +namespace ledger { + +std::ostringstream _ctxt_buffer; +std::ostringstream _desc_buffer; + +string error_context() +{ + string context = _ctxt_buffer.str(); + _ctxt_buffer.clear(); + _ctxt_buffer.str(""); + return context; +} + +string file_context(const path& file, const std::size_t line) +{ + std::ostringstream buf; + buf << '"' << file.string() << "\", line " << line << ":"; + return buf.str(); +} + +string line_context(const string& line, + const string::size_type pos, + const string::size_type end_pos) +{ + std::ostringstream buf; + buf << " " << line << "\n"; + + if (pos != 0) { + buf << " "; + if (end_pos == 0) { + for (string::size_type i = 0; i < pos; i += 1) + buf << " "; + buf << "^"; + } else { + for (string::size_type i = 0; i < end_pos; i += 1) { + if (i >= pos) + buf << "^"; + else + buf << " "; + } + } + } + return buf.str(); +} + +string source_context(const path& file, + const std::istream::pos_type pos, + const std::istream::pos_type end_pos, + const string& prefix) +{ + const std::streamoff len = end_pos - pos; + if (! len || file.empty()) + return _(""); + + assert(len > 0); + assert(len < 8192); + + std::ostringstream out; + + ifstream in(file); + in.seekg(pos, std::ios::beg); + + scoped_array buf(new char[static_cast(len) + 1]); + in.read(buf.get(), static_cast(len)); + assert(in.gcount() == static_cast(len)); + buf[static_cast(len)] = '\0'; + + bool first = true; + for (char * p = std::strtok(buf.get(), "\n"); + p; + p = std::strtok(NULL, "\n")) { + if (first) + first = false; + else + out << '\n'; + out << prefix << p; + } + + return out.str(); +} + +} // namespace ledger diff --git a/src/error.h b/src/error.h new file mode 100644 index 00000000..3bdcde98 --- /dev/null +++ b/src/error.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +/** + * @addtogroup util + */ + +/** + * @file error.h + * @author John Wiegley + * + * @ingroup util + */ +#ifndef _ERROR_H +#define _ERROR_H + +namespace ledger { + +extern std::ostringstream _desc_buffer; + +template +[[ noreturn ]] inline void throw_func(const string& message) { + _desc_buffer.clear(); + _desc_buffer.str(""); + throw T(message); +} + +#define throw_(cls, msg) \ + ((_desc_buffer << (msg)), \ + throw_func(_desc_buffer.str())) + +inline void warning_func(const string& message) { + std::cerr << "Warning: " << message << std::endl; + _desc_buffer.clear(); + _desc_buffer.str(""); +} + +#define warning_(msg) \ + ((_desc_buffer << (msg)), \ + warning_func(_desc_buffer.str())) + +extern std::ostringstream _ctxt_buffer; + +#define add_error_context(msg) \ + ((long(_ctxt_buffer.tellp()) == 0) ? \ + (_ctxt_buffer << (msg)) : \ + (_ctxt_buffer << std::endl << (msg))) + +string error_context(); + +string file_context(const path& file, std::size_t line); +string line_context(const string& line, + const string::size_type pos = 0, + const string::size_type end_pos = 0); + +string source_context(const path& file, + const std::istream::pos_type pos, + const std::istream::pos_type end_pos, + const string& prefix = ""); + +#define DECLARE_EXCEPTION(name, kind) \ + class name : public kind { \ + public: \ + explicit name(const string& why) throw() : kind(why) {} \ + virtual ~name() throw() {} \ + } + +struct error_count { + std::size_t count; + std::string message; + explicit error_count(std::size_t _count, std::string _msg) : count(_count), message(_msg) {} + const char * what() const { return message.c_str(); } +}; + +} // namespace ledger + +#endif // _ERROR_H diff --git a/src/expr.cc b/src/expr.cc new file mode 100644 index 00000000..c8945d3d --- /dev/null +++ b/src/expr.cc @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +#include + +#include "expr.h" +#include "parser.h" +#include "scope.h" + +namespace ledger { + +expr_t::expr_t() : base_type() +{ + TRACE_CTOR(expr_t, ""); +} + +expr_t::expr_t(const expr_t& other) : base_type(other), ptr(other.ptr) +{ + TRACE_CTOR(expr_t, "copy"); +} +expr_t::expr_t(ptr_op_t _ptr, scope_t * _context) + : base_type(_context), ptr(_ptr) +{ + TRACE_CTOR(expr_t, "const ptr_op_t&, scope_t *"); +} + +expr_t::expr_t(const string& _str, const parse_flags_t& flags) + : base_type() +{ + if (! _str.empty()) + parse(_str, flags); + TRACE_CTOR(expr_t, "string, parse_flags_t"); +} + +expr_t::expr_t(std::istream& in, const parse_flags_t& flags) + : base_type() +{ + parse(in, flags); + TRACE_CTOR(expr_t, "std::istream&, parse_flags_t"); +} + +expr_t::~expr_t() { + TRACE_DTOR(expr_t); +} + +expr_t& expr_t::operator=(const expr_t& _expr) +{ + if (this != &_expr) { + base_type::operator=(_expr); + ptr = _expr.ptr; + } + return *this; +} + +expr_t::operator bool() const throw() +{ + return ptr.get() != NULL; +} + +expr_t::ptr_op_t expr_t::get_op() throw() +{ + return ptr; +} + +void expr_t::parse(std::istream& in, const parse_flags_t& flags, + const optional& original_string) +{ + parser_t parser; + std::istream::pos_type start_pos = in.tellg(); + ptr = parser.parse(in, flags, original_string); + std::istream::pos_type end_pos = in.tellg(); + + if (original_string) { + set_text(*original_string); + } + else if (end_pos > start_pos) { + in.clear(); + in.seekg(start_pos, std::ios::beg); + scoped_array buf + (new char[static_cast(end_pos - start_pos) + 1]); + int len = static_cast(end_pos) - static_cast(start_pos); + in.read(buf.get(), len); + buf[len] = '\0'; + set_text(buf.get()); + } + else { + set_text(""); + } +} + +void expr_t::compile(scope_t& scope) +{ + if (! compiled && ptr) { + ptr = ptr->compile(scope); + base_type::compile(scope); + } +} + +value_t expr_t::real_calc(scope_t& scope) +{ + if (ptr) { + ptr_op_t locus; + try { + return ptr->calc(scope, &locus); + } + catch (const std::exception&) { + if (locus) { + string current_context = error_context(); + + add_error_context(_("While evaluating value expression:")); + add_error_context(op_context(ptr, locus)); + + if (SHOW_INFO()) { + add_error_context(_("The value expression tree was:")); + std::ostringstream buf; + ptr->dump(buf, 0); + + std::istringstream in(buf.str()); + std::ostringstream out; + char linebuf[1024]; + bool first = true; + while (in.good() && ! in.eof()) { + in.getline(linebuf, 1023); + std::streamsize len = in.gcount(); + if (len > 0) { + if (first) + first = false; + else + out << '\n'; + out << " " << linebuf; + } + } + add_error_context(out.str()); + } + + if (! current_context.empty()) + add_error_context(current_context); + } + throw; + } + } + return NULL_VALUE; +} + +bool expr_t::is_constant() const +{ + assert(compiled); + return ptr && ptr->is_value(); +} + +bool expr_t::is_function() const +{ + assert(compiled); + return ptr && ptr->is_function(); +} + +value_t& expr_t::constant_value() +{ + assert(is_constant()); + return ptr->as_value_lval(); +} + +const value_t& expr_t::constant_value() const +{ + assert(is_constant()); + return ptr->as_value(); +} + +expr_t::func_t& expr_t::get_function() +{ + assert(is_function()); + return ptr->as_function_lval(); +} + +string expr_t::context_to_str() const +{ + return ptr ? op_context(ptr) : _(""); +} + +void expr_t::print(std::ostream& out) const +{ + if (ptr) + ptr->print(out); +} + +void expr_t::dump(std::ostream& out) const +{ + if (ptr) ptr->dump(out, 0); +} + +bool merged_expr_t::check_for_single_identifier(const string& expr) +{ + bool single_identifier = true; + for (const char * p = expr.c_str(); *p; ++p) + if (! std::isalnum(*p) || *p == '_') { + single_identifier = false; + break; + } + + if (single_identifier) { + set_base_expr(expr); + exprs.clear(); + return true; + } else { + return false; + } +} + +void merged_expr_t::compile(scope_t& scope) +{ + if (exprs.empty()) { + parse(base_expr); + } else { + std::ostringstream buf; + + buf << "__tmp_" << term << "=(" << term << "=(" << base_expr << ")"; + foreach (const string& expr, exprs) { + if (merge_operator == ";") + buf << merge_operator << term << "=" << expr; + else + buf << merge_operator << "(" << expr << ")"; + } + buf << ";" << term << ");__tmp_" << term; + + DEBUG("expr.merged.compile", "Compiled expr: " << buf.str()); + parse(buf.str()); + } + + expr_t::compile(scope); +} + +expr_t::ptr_op_t as_expr(const value_t& val) +{ + VERIFY(val.is_any()); + return val.as_any(); +} + +void set_expr(value_t& val, expr_t::ptr_op_t op) +{ + val.set_any(op); +} + +value_t expr_value(expr_t::ptr_op_t op) +{ + value_t temp; + temp.set_any(op); + return temp; +} + +value_t source_command(call_scope_t& args) +{ + std::istream * in = NULL; + scoped_ptr stream; + string pathname; + + if (args.has(0)) { + pathname = args.get(0); + stream.reset(new ifstream(path(pathname))); + in = stream.get(); + } else { + pathname = ""; + in = &std::cin; + } + + symbol_scope_t file_locals(args); + std::size_t linenum = 0; + char buf[4096]; + std::istream::pos_type pos; + + while (in->good() && ! in->eof()) { + pos = in->tellg(); + in->getline(buf, 4095); + linenum++; + + char * p = skip_ws(buf); + if (*p && *p != ';') { + try { + expr_t(p).calc(file_locals); + } + catch (const std::exception&) { + add_error_context(_f("While parsing value expression on line %1%:") + % linenum); + add_error_context(source_context(pathname, pos, in->tellg(), "> ")); + } + } + } + + return true; +} + +} // namespace ledger diff --git a/src/expr.h b/src/expr.h new file mode 100644 index 00000000..b3806bc1 --- /dev/null +++ b/src/expr.h @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup expr + */ + +/** + * @file expr.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _EXPR_H +#define _EXPR_H + +#include "exprbase.h" +#include "value.h" + +namespace ledger { + +class expr_t : public expr_base_t +{ + class parser_t; + typedef expr_base_t base_type; + +public: + struct token_t; + class op_t; + typedef intrusive_ptr ptr_op_t; + typedef intrusive_ptr const_ptr_op_t; + + enum check_expr_kind_t { + EXPR_GENERAL, + EXPR_ASSERTION, + EXPR_CHECK + }; + + typedef std::pair check_expr_pair; + typedef std::list check_expr_list; + +protected: + ptr_op_t ptr; + +public: + expr_t(); + expr_t(const expr_t& other); + expr_t(ptr_op_t _ptr, scope_t * _context = NULL); + + expr_t(const string& _str, const parse_flags_t& flags = PARSE_DEFAULT); + expr_t(std::istream& in, const parse_flags_t& flags = PARSE_DEFAULT); + + virtual ~expr_t(); + + expr_t& operator=(const expr_t& _expr); + + virtual operator bool() const throw(); + + ptr_op_t get_op() throw(); + + void parse(const string& str, const parse_flags_t& flags = PARSE_DEFAULT) { + std::istringstream stream(str); + return parse(stream, flags, str); + } + + virtual void parse(std::istream& in, + const parse_flags_t& flags = PARSE_DEFAULT, + const optional& original_string = none); + virtual void compile(scope_t& scope); + virtual value_t real_calc(scope_t& scope); + + bool is_constant() const; + value_t& constant_value(); + const value_t& constant_value() const; + bool is_function() const; + func_t& get_function(); + + virtual string context_to_str() const; + virtual void print(std::ostream& out) const; + virtual void dump(std::ostream& out) const; +}; + +/** + * Dealing with expr pointers tucked into value objects. + */ +inline bool is_expr(const value_t& val) { + return val.is_any() && val.as_any().type() == typeid(expr_t::ptr_op_t); +} + +expr_t::ptr_op_t as_expr(const value_t& val); +void set_expr(value_t& val, expr_t::ptr_op_t op); +value_t expr_value(expr_t::ptr_op_t op); + +// A merged expression allows one to set an expression term, "foo", and +// a base expression, "bar", and then merge in later expressions that +// utilize foo. For example: +// +// foo: bar +// merge: foo * 10 +// merge: foo + 20 +// +// When this expression is finally compiled, the base and merged +// elements are written into this: +// +// __tmp=(foo=bar; foo=foo*10; foo=foo+20);__tmp +// +// This allows users to select flags like -O, -B or -I at any time, and +// also combine flags such as -V and -A. + +class merged_expr_t : public expr_t +{ +public: + string term; + string base_expr; + string merge_operator; + + std::list exprs; + + merged_expr_t(const string& _term, const string& expr, + const string& merge_op = ";") + : expr_t(), term(_term), base_expr(expr), merge_operator(merge_op) { + TRACE_CTOR(merged_expr_t, "string, string, string"); + } + virtual ~merged_expr_t() { + TRACE_DTOR(merged_expr_t); + } + + void set_term(const string& _term) { + term = _term; + } + void set_base_expr(const string& expr) { + base_expr = expr; + } + void set_merge_operator(const string& merge_op) { + merge_operator = merge_op; + } + + bool check_for_single_identifier(const string& expr); + + void prepend(const string& expr) { + if (! check_for_single_identifier(expr)) + exprs.push_front(expr); + } + void append(const string& expr) { + if (! check_for_single_identifier(expr)) + exprs.push_back(expr); + } + void remove(const string& expr) { + exprs.remove(expr); + } + + virtual void compile(scope_t& scope); +}; + +class call_scope_t; +value_t source_command(call_scope_t& scope); + +} // namespace ledger + +#endif // _EXPR_H diff --git a/src/exprbase.h b/src/exprbase.h new file mode 100644 index 00000000..9c28dca3 --- /dev/null +++ b/src/exprbase.h @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup expr + */ + +/** + * @file exprbase.h + * @author John Wiegley + * + * @ingroup expr + * + * This class provides basic behavior for all the domain specific expression + * languages used in Leger: + * + * | Typename | Description | result_type | Derives | + * |-------------+----------------------------+-----------------+-------------| + * | expr_t | Value expressions | value_t | | + * | predicate_t | Special form of expr_t | bool | expr_t | + * | query_t | Report queries | bool | predicate_t | + * | period_t | Time periods and durations | date_interval_t | | + * | draft_t | Partially filled xacts | xact_t * | | + * | format_t | Format strings | string | | + */ +#ifndef _EXPRBASE_H +#define _EXPRBASE_H + +#include "utils.h" +#include "amount.h" + +namespace ledger { + +DECLARE_EXCEPTION(parse_error, std::runtime_error); +DECLARE_EXCEPTION(compile_error, std::runtime_error); +DECLARE_EXCEPTION(calc_error, std::runtime_error); +DECLARE_EXCEPTION(usage_error, std::runtime_error); + +class scope_t; +class call_scope_t; + +template +class expr_base_t +{ +public: + typedef ResultType result_type; + + typedef function func_t; + +protected: + scope_t * context; + string str; + bool compiled; + + virtual result_type real_calc(scope_t& scope) = 0; + +public: + expr_base_t(const expr_base_t& other) + : context(other.context), str(other.str), compiled(false) { + TRACE_CTOR(expr_base_t, "copy"); + } + expr_base_t(scope_t * _context = NULL) + : context(_context), compiled(false) + { + TRACE_CTOR(expr_base_t, "scope_t *"); + } + virtual ~expr_base_t() { + TRACE_DTOR(expr_base_t); + } + + expr_base_t& operator=(const expr_base_t& _expr) { + if (this != &_expr) { + str = _expr.str; + context = _expr.context; + compiled = _expr.compiled; + } + return *this; + } + expr_base_t& operator=(const string& _expr) { + parse(_expr); + return *this; + } + + virtual operator bool() const throw() { + return ! str.empty(); + } + + virtual string text() const throw() { + return str; + } + void set_text(const string& txt) { + str = txt; + compiled = false; + } + + void parse(const string& expr_str, + const parse_flags_t& flags = PARSE_DEFAULT) { + std::istringstream stream(expr_str); + return parse(stream, flags, expr_str); + } + virtual void parse(std::istream&, + const parse_flags_t& = PARSE_DEFAULT, + const optional& original_string = none) { + set_text(original_string ? *original_string : ""); + } + + virtual void mark_uncompiled() { + compiled = false; + } + + void recompile(scope_t& scope) { + compiled = false; + compile(scope); + } + + virtual void compile(scope_t& scope) { + if (! compiled) { + // Derived classes need to do something here. + context = &scope; + compiled = true; + } + } + + result_type operator()(scope_t& scope) { + return calc(scope); + } + + result_type calc(scope_t& scope) + { + if (! compiled) { +#if DEBUG_ON + if (SHOW_DEBUG("expr.compile")) { + DEBUG("expr.compile", "Before compilation:"); + dump(*_log_stream); + } +#endif // DEBUG_ON + + DEBUG("expr.compile", "Compiling: " << str); + compile(scope); + +#if DEBUG_ON + if (SHOW_DEBUG("expr.compile")) { + DEBUG("expr.compile", "After compilation:"); + dump(*_log_stream); + } +#endif // DEBUG_ON + } + + DEBUG("expr.calc", "Calculating: " << str); + return real_calc(scope); + } + + result_type calc() { + assert(context); + return calc(*context); + } + + scope_t * get_context() { + return context; + } + void set_context(scope_t * scope) { + context = scope; + } + + virtual string context_to_str() const { + return empty_string; + } + + string print_to_str() const { + std::ostringstream out; + print(out); + return out.str(); + } + string dump_to_str() const { + std::ostringstream out; + dump(out); + return out.str(); + } + string preview_to_str(scope_t&) const { + std::ostringstream out; + preview(out); + return out.str(); + } + + virtual void print(std::ostream&) const {} + virtual void dump(std::ostream&) const {} + + result_type preview(std::ostream& out, scope_t& scope) const { + out << _("--- Input expression ---") << std::endl; + out << text() << std::endl; + + out << std::endl << _("--- Text as parsed ---") << std::endl; + print(out); + out << std::endl; + + out << std::endl << _("--- Expression tree ---") << std::endl; + dump(out); + + out << std::endl << _("--- Compiled tree ---") << std::endl; + compile(scope); + dump(out); + + out << std::endl << _("--- Result value ---") << std::endl; + return calc(); + } +}; + +template +std::ostream& operator<<(std::ostream& out, + const expr_base_t& expr) { + expr.print(out); + return out; +} + +} // namespace ledger + +#endif // _EXPRBASE_H diff --git a/src/filters.cc b/src/filters.cc new file mode 100644 index 00000000..8daf6700 --- /dev/null +++ b/src/filters.cc @@ -0,0 +1,1539 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "filters.h" +#include "iterators.h" +#include "journal.h" +#include "report.h" +#include "compare.h" +#include "pool.h" + +namespace ledger { + +void post_splitter::print_title(const value_t& val) +{ + if (! report.HANDLED(no_titles)) { + std::ostringstream buf; + val.print(buf); + post_chain->title(buf.str()); + } +} + +void post_splitter::flush() +{ + foreach (value_to_posts_map::value_type& pair, posts_map) { + preflush_func(pair.first); + + foreach (post_t * post, pair.second) + (*post_chain)(*post); + + post_chain->flush(); + post_chain->clear(); + + if (postflush_func) + (*postflush_func)(pair.first); + } +} + +void post_splitter::operator()(post_t& post) +{ + bind_scope_t bound_scope(report, post); + value_t result(group_by_expr.calc(bound_scope)); + + if (! result.is_null()) { + value_to_posts_map::iterator i = posts_map.find(result); + if (i != posts_map.end()) { + (*i).second.push_back(&post); + } else { + std::pair inserted + = posts_map.insert(value_to_posts_map::value_type(result, posts_list())); + assert(inserted.second); + (*inserted.first).second.push_back(&post); + } + } +} + +void truncate_xacts::flush() +{ + if (! posts.size()) + return; + + xact_t * xact = (*posts.begin())->xact; + + int l = 0; + foreach (post_t * post, posts) + if (xact != post->xact) { + l++; + xact = post->xact; + } + l++; + + xact = (*posts.begin())->xact; + + int i = 0; + foreach (post_t * post, posts) { + if (xact != post->xact) { + xact = post->xact; + i++; + } + + bool print = false; + if (head_count) { + if (head_count > 0 && i < head_count) + print = true; + else if (head_count < 0 && i >= - head_count) + print = true; + } + + if (! print && tail_count) { + if (tail_count > 0 && l - i <= tail_count) + print = true; + else if (tail_count < 0 && l - i > - tail_count) + print = true; + } + + if (print) + item_handler::operator()(*post); + } + posts.clear(); + + item_handler::flush(); +} + +void truncate_xacts::operator()(post_t& post) +{ + if (completed) + return; + + if (last_xact != post.xact) { + if (last_xact) + xacts_seen++; + last_xact = post.xact; + } + + if (tail_count == 0 && head_count > 0 && + static_cast(xacts_seen) >= head_count) { + flush(); + completed = true; + return; + } + + posts.push_back(&post); +} + +void sort_posts::post_accumulated_posts() +{ + std::stable_sort(posts.begin(), posts.end(), + compare_items(sort_order, report)); + + foreach (post_t * post, posts) { + post->xdata().drop_flags(POST_EXT_SORT_CALC); + item_handler::operator()(*post); + } + + posts.clear(); +} + +namespace { + void split_string(const string& str, const char ch, + std::list& strings) + { + const char * b = str.c_str(); + for (const char * p = b; *p; p++) { + if (*p == ch) { + strings.push_back(string(b, static_cast(p - b))); + b = p + 1; + } + } + strings.push_back(string(b)); + } + + account_t * create_temp_account_from_path(std::list& account_names, + temporaries_t& temps, + account_t * master) + { + account_t * new_account = NULL; + foreach (const string& name, account_names) { + if (new_account) { + new_account = new_account->find_account(name); + } else { + new_account = master->find_account(name, false); + if (! new_account) + new_account = &temps.create_account(name, master); + } + } + + assert(new_account != NULL); + return new_account; + } +} + +void anonymize_posts::render_commodity(amount_t& amt) +{ + commodity_t& comm(amt.commodity()); + + std::size_t id; + bool newly_added = false; + + commodity_index_map::iterator i = comms.find(&comm); + if (i == comms.end()) { + id = next_comm_id++; + newly_added = true; + comms.insert(commodity_index_map::value_type(&comm, id)); + } else { + id = (*i).second; + } + + std::ostringstream buf; + do { + buf << static_cast('A' + (id % 26)); + id /= 26; + } + while (id > 0); + + if (amt.has_annotation()) + amt.set_commodity + (*commodity_pool_t::current_pool->find_or_create(buf.str(), + amt.annotation())); + else + amt.set_commodity + (*commodity_pool_t::current_pool->find_or_create(buf.str())); + + if (newly_added) { + amt.commodity().set_flags(comm.flags()); + amt.commodity().set_precision(comm.precision()); + } +} + +void anonymize_posts::operator()(post_t& post) +{ + boost::uuids::detail::sha1 sha; + unsigned int message_digest[5]; + bool copy_xact_details = false; + + if (last_xact != post.xact) { + temps.copy_xact(*post.xact); + last_xact = post.xact; + copy_xact_details = true; + } + xact_t& xact = temps.last_xact(); + xact.code = none; + + if (copy_xact_details) { + xact.copy_details(*post.xact); + + std::ostringstream buf; + buf << reinterpret_cast(post.xact->payee.c_str()) + << integer_gen() << post.xact->payee.c_str(); + + sha.reset(); + sha.process_bytes(buf.str().c_str(), buf.str().length()); + sha.get_digest(message_digest); + + xact.payee = to_hex(message_digest); + xact.note = none; + } else { + xact.journal = post.xact->journal; + } + + std::list account_names; + + for (account_t * acct = post.account; + acct; + acct = acct->parent) { + std::ostringstream buf; + buf << integer_gen() << acct << acct->fullname(); + + sha.reset(); + sha.process_bytes(buf.str().c_str(), buf.str().length()); + sha.get_digest(message_digest); + + account_names.push_front(to_hex(message_digest)); + } + + account_t * new_account = + create_temp_account_from_path(account_names, temps, xact.journal->master); + post_t& temp = temps.copy_post(post, xact, new_account); + temp.note = none; + temp.add_flags(POST_ANONYMIZED); + + render_commodity(temp.amount); + if (temp.amount.has_annotation()) { + temp.amount.annotation().tag = none; + if (temp.amount.annotation().price) + render_commodity(*temp.amount.annotation().price); + } + + if (temp.cost) + render_commodity(*temp.cost); + if (temp.assigned_amount) + render_commodity(*temp.assigned_amount); + + (*handler)(temp); +} + +void calc_posts::operator()(post_t& post) +{ + post_t::xdata_t& xdata(post.xdata()); + + if (last_post) { + assert(last_post->has_xdata()); + if (calc_running_total) + xdata.total = last_post->xdata().total; + xdata.count = last_post->xdata().count + 1; + } else { + xdata.count = 1; + } + + post.add_to_value(xdata.visited_value, amount_expr); + xdata.add_flags(POST_EXT_VISITED); + + account_t * acct = post.reported_account(); + acct->xdata().add_flags(ACCOUNT_EXT_VISITED); + + if (calc_running_total) + add_or_set_value(xdata.total, xdata.visited_value); + + item_handler::operator()(post); + + last_post = &post; +} + +namespace { + void handle_value(const value_t& value, + account_t * account, + xact_t * xact, + temporaries_t& temps, + post_handler_ptr handler, + const date_t& date = date_t(), + const bool act_date_p = true, + const value_t& total = value_t(), + const bool direct_amount = false, + const bool mark_visited = false, + const bool bidir_link = true) + { + post_t& post = temps.create_post(*xact, account, bidir_link); + post.add_flags(ITEM_GENERATED); + + // If the account for this post is all virtual, then report the post as + // such. This allows subtotal reports to show "(Account)" for accounts + // that contain only virtual posts. + if (account && account->has_xdata() && + account->xdata().has_flags(ACCOUNT_EXT_AUTO_VIRTUALIZE)) { + if (! account->xdata().has_flags(ACCOUNT_EXT_HAS_NON_VIRTUALS)) { + post.add_flags(POST_VIRTUAL); + if (! account->xdata().has_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS)) + post.add_flags(POST_MUST_BALANCE); + } + } + + post_t::xdata_t& xdata(post.xdata()); + + if (is_valid(date)) { + if (act_date_p) + xdata.date = date; + else + xdata.value_date = date; + } + + value_t temp(value); + + switch (value.type()) { + case value_t::BOOLEAN: + case value_t::INTEGER: + temp.in_place_cast(value_t::AMOUNT); + // fall through... + + case value_t::AMOUNT: + post.amount = temp.as_amount(); + break; + + case value_t::BALANCE: + case value_t::SEQUENCE: + xdata.compound_value = temp; + xdata.add_flags(POST_EXT_COMPOUND); + break; + + case value_t::DATETIME: + case value_t::DATE: + default: + assert(false); + break; + } + + if (! total.is_null()) + xdata.total = total; + + if (direct_amount) + xdata.add_flags(POST_EXT_DIRECT_AMT); + + DEBUG("filters.changed_value.rounding", "post.amount = " << post.amount); + + (*handler)(post); + + if (mark_visited) { + post.xdata().add_flags(POST_EXT_VISITED); + post.account->xdata().add_flags(ACCOUNT_EXT_VISITED); + } + } +} + +void collapse_posts::report_subtotal() +{ + if (! count) + return; + + std::size_t displayed_count = 0; + foreach (post_t * post, component_posts) { + bind_scope_t bound_scope(report, *post); + if (only_predicate(bound_scope) && display_predicate(bound_scope)) + displayed_count++; + } + + if (displayed_count == 1) { + item_handler::operator()(*last_post); + } + else if (only_collapse_if_zero && ! subtotal.is_zero()) { + foreach (post_t * post, component_posts) + item_handler::operator()(*post); + } + else { + date_t earliest_date; + date_t latest_date; + + foreach (post_t * post, component_posts) { + date_t date = post->date(); + date_t value_date = post->value_date(); + if (! is_valid(earliest_date) || date < earliest_date) + earliest_date = date; + if (! is_valid(latest_date) || value_date > latest_date) + latest_date = value_date; + } + + xact_t& xact = temps.create_xact(); + xact.payee = last_xact->payee; + xact._date = (is_valid(earliest_date) ? + earliest_date : last_xact->_date); + + DEBUG("filters.collapse", "Pseudo-xact date = " << *xact._date); + DEBUG("filters.collapse", "earliest date = " << earliest_date); + DEBUG("filters.collapse", "latest date = " << latest_date); + + handle_value(/* value= */ subtotal, + /* account= */ totals_account, + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ latest_date, + /* act_date_p= */ false); + } + + component_posts.clear(); + + last_xact = NULL; + last_post = NULL; + subtotal = 0L; + count = 0; +} + +void collapse_posts::operator()(post_t& post) +{ + // If we've reached a new xact, report on the subtotal + // accumulated thus far. + + if (last_xact != post.xact && count > 0) + report_subtotal(); + + post.add_to_value(subtotal, amount_expr); + + component_posts.push_back(&post); + + last_xact = post.xact; + last_post = &post; + count++; +} + +void related_posts::flush() +{ + if (posts.size() > 0) { + foreach (post_t * post, posts) { + assert(post->xact); + foreach (post_t * r_post, post->xact->posts) { + post_t::xdata_t& xdata(r_post->xdata()); + if (! xdata.has_flags(POST_EXT_HANDLED) && + (! xdata.has_flags(POST_EXT_RECEIVED) ? + ! r_post->has_flags(ITEM_GENERATED | POST_VIRTUAL) : + also_matching)) { + xdata.add_flags(POST_EXT_HANDLED); + item_handler::operator()(*r_post); + } + } + } + } + + item_handler::flush(); +} + +display_filter_posts::display_filter_posts(post_handler_ptr handler, + report_t& _report, + bool _show_rounding) + : item_handler(handler), report(_report), + display_amount_expr(report.HANDLER(display_amount_).expr), + display_total_expr(report.HANDLER(display_total_).expr), + show_rounding(_show_rounding) +{ + create_accounts(); + TRACE_CTOR(display_filter_posts, "post_handler_ptr, report_t&, bool"); +} + +bool display_filter_posts::output_rounding(post_t& post) +{ + bind_scope_t bound_scope(report, post); + value_t new_display_total; + + if (show_rounding) { + new_display_total = (display_total_expr.calc(bound_scope) + .strip_annotations(report.what_to_keep())); + + DEBUG("filters.changed_value.rounding", + "rounding.new_display_total = " << new_display_total); + } + + // Allow the posting to be displayed if: + // 1. Its display_amount would display as non-zero, or + // 2. The --empty option was specified, or + // 3. a) The account of the posting is , and + // b) the revalued option is specified, and + // c) the --no-rounding option is not specified. + + if (post.account == revalued_account) { + if (show_rounding) + last_display_total = new_display_total; + return true; + } + + if (value_t repriced_amount = (display_amount_expr.calc(bound_scope) + .strip_annotations(report.what_to_keep()))) { + if (! last_display_total.is_null()) { + DEBUG("filters.changed_value.rounding", + "rounding.repriced_amount = " << repriced_amount); + + value_t precise_display_total(new_display_total.truncated() - + repriced_amount.truncated()); + + DEBUG("filters.changed_value.rounding", + "rounding.precise_display_total = " << precise_display_total); + DEBUG("filters.changed_value.rounding", + "rounding.last_display_total = " << last_display_total); + + if (value_t diff = precise_display_total - last_display_total) { + DEBUG("filters.changed_value.rounding", + "rounding.diff = " << diff); + + handle_value(/* value= */ diff, + /* account= */ rounding_account, + /* xact= */ post.xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ date_t(), + /* act_date_p= */ true, + /* total= */ precise_display_total, + /* direct_amount= */ true, + /* mark_visited= */ false, + /* bidir_link= */ false); + } + } + if (show_rounding) + last_display_total = new_display_total; + return true; + } else { + return report.HANDLED(empty); + } +} + +void display_filter_posts::operator()(post_t& post) +{ + if (output_rounding(post)) + item_handler::operator()(post); +} + +changed_value_posts::changed_value_posts + (post_handler_ptr handler, + report_t& _report, + bool _for_accounts_report, + bool _show_unrealized, + display_filter_posts * _display_filter) + : item_handler(handler), report(_report), + total_expr(report.HANDLED(revalued_total_) ? + report.HANDLER(revalued_total_).expr : + report.HANDLER(display_total_).expr), + display_total_expr(report.HANDLER(display_total_).expr), + changed_values_only(report.HANDLED(revalued_only)), + historical_prices_only(report.HANDLED(historical)), + for_accounts_report(_for_accounts_report), + show_unrealized(_show_unrealized), last_post(NULL), + display_filter(_display_filter) +{ + string gains_equity_account_name; + if (report.HANDLED(unrealized_gains_)) + gains_equity_account_name = report.HANDLER(unrealized_gains_).str(); + else + gains_equity_account_name = _("Equity:Unrealized Gains"); + gains_equity_account = + report.session.journal->master->find_account(gains_equity_account_name); + gains_equity_account->add_flags(ACCOUNT_GENERATED); + + string losses_equity_account_name; + if (report.HANDLED(unrealized_losses_)) + losses_equity_account_name = report.HANDLER(unrealized_losses_).str(); + else + losses_equity_account_name = _("Equity:Unrealized Losses"); + losses_equity_account = + report.session.journal->master->find_account(losses_equity_account_name); + losses_equity_account->add_flags(ACCOUNT_GENERATED); + + create_accounts(); + + TRACE_CTOR(changed_value_posts, + "post_handler_ptr, report_t&, bool, bool, display_filter_posts *"); +} + +void changed_value_posts::flush() +{ + if (last_post && last_post->date() <= report.terminus.date()) { + if (! historical_prices_only) { + if (! for_accounts_report) + output_intermediate_prices(*last_post, report.terminus.date()); + output_revaluation(*last_post, report.terminus.date()); + } + last_post = NULL; + } + item_handler::flush(); +} + +void changed_value_posts::output_revaluation(post_t& post, const date_t& date) +{ + if (is_valid(date)) + post.xdata().date = date; + + try { + bind_scope_t bound_scope(report, post); + repriced_total = total_expr.calc(bound_scope); + } + catch (...) { + post.xdata().date = date_t(); + throw; + } + post.xdata().date = date_t(); + + DEBUG("filters.changed_value", + "output_revaluation(last_total) = " << last_total); + DEBUG("filters.changed_value", + "output_revaluation(repriced_total) = " << repriced_total); + + if (! last_total.is_null()) { + if (value_t diff = repriced_total - last_total) { + DEBUG("filters.changed_value", "output_revaluation(strip(diff)) = " + << diff.strip_annotations(report.what_to_keep())); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Commodities revalued"); + xact._date = is_valid(date) ? date : post.value_date(); + + if (! for_accounts_report) { + handle_value + (/* value= */ diff, + /* account= */ revalued_account, + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ *xact._date, + /* act_date_p= */ true, + /* total= */ repriced_total); + } + else if (show_unrealized) { + handle_value + (/* value= */ - diff, + /* account= */ (diff < 0L ? + losses_equity_account : + gains_equity_account), + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ *xact._date, + /* act_date_p= */ true, + /* total= */ value_t(), + /* direct_amount= */ false, + /* mark_visited= */ true); + } + } + } +} + +namespace { + struct insert_prices_in_map { + price_map_t& all_prices; + + insert_prices_in_map(price_map_t& _all_prices) + : all_prices(_all_prices) {} + + void operator()(const datetime_t& date, const amount_t& price) { + all_prices.insert(price_map_t::value_type(date, price)); + } + }; +} + +void changed_value_posts::output_intermediate_prices(post_t& post, + const date_t& current) +{ + // To fix BZ#199, examine the balance of last_post and determine whether the + // price of that amount changed after its date and before the new post's + // date. If so, generate an output_revaluation for that price change. + // Mostly this is only going to occur if the user has a series of pricing + // entries, since a posting-based revaluation would be seen here as a post. + + value_t display_total(last_total); + + if (display_total.type() == value_t::SEQUENCE) { + xact_t& xact(temps.create_xact()); + + xact.payee = _("Commodities revalued"); + xact._date = is_valid(current) ? current : post.value_date(); + + post_t& temp(temps.copy_post(post, xact)); + temp.add_flags(ITEM_GENERATED); + + post_t::xdata_t& xdata(temp.xdata()); + if (is_valid(current)) + xdata.date = current; + + DEBUG("filters.revalued", "intermediate last_total = " << last_total); + + switch (last_total.type()) { + case value_t::BOOLEAN: + case value_t::INTEGER: + last_total.in_place_cast(value_t::AMOUNT); + // fall through... + + case value_t::AMOUNT: + temp.amount = last_total.as_amount(); + break; + + case value_t::BALANCE: + case value_t::SEQUENCE: + xdata.compound_value = last_total; + xdata.add_flags(POST_EXT_COMPOUND); + break; + + case value_t::DATETIME: + case value_t::DATE: + default: + assert(false); + break; + } + + bind_scope_t inner_scope(report, temp); + display_total = display_total_expr.calc(inner_scope); + + DEBUG("filters.revalued", "intermediate display_total = " << display_total); + } + + switch (display_total.type()) { + case value_t::VOID: + case value_t::INTEGER: + case value_t::SEQUENCE: + break; + + case value_t::AMOUNT: + display_total.in_place_cast(value_t::BALANCE); + // fall through... + + case value_t::BALANCE: { + price_map_t all_prices; + + foreach (const balance_t::amounts_map::value_type& amt_comm, + display_total.as_balance().amounts) + amt_comm.first->map_prices(insert_prices_in_map(all_prices), + datetime_t(current), + datetime_t(post.value_date()), true); + + // Choose the last price from each day as the price to use + typedef std::map date_map; + date_map pricing_dates; + + BOOST_REVERSE_FOREACH(const price_map_t::value_type& price, all_prices) { + // This insert will fail if a later price has already been inserted + // for that date. + DEBUG("filters.revalued", + "re-inserting " << price.second << " at " << price.first.date()); + pricing_dates.insert(date_map::value_type(price.first.date(), true)); + } + + // Go through the time-sorted prices list, outputting a revaluation for + // each price difference. + foreach (const date_map::value_type& price, pricing_dates) { + output_revaluation(post, price.first); + last_total = repriced_total; + } + break; + } + default: + assert(false); + break; + } +} + +void changed_value_posts::operator()(post_t& post) +{ + if (last_post) { + if (! for_accounts_report && ! historical_prices_only) + output_intermediate_prices(*last_post, post.value_date()); + output_revaluation(*last_post, post.value_date()); + } + + if (changed_values_only) + post.xdata().add_flags(POST_EXT_DISPLAYED); + + item_handler::operator()(post); + + bind_scope_t bound_scope(report, post); + last_total = total_expr.calc(bound_scope); + last_post = &post; +} + +void subtotal_posts::report_subtotal(const char * spec_fmt, + const optional& interval) +{ + if (component_posts.empty()) + return; + + optional range_start = interval ? interval->start : none; + optional range_finish = interval ? interval->inclusive_end() : none; + + if (! range_start || ! range_finish) { + foreach (post_t * post, component_posts) { + date_t date = post->date(); + date_t value_date = post->value_date(); +#if defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 7 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + if (! range_start || date < *range_start) + range_start = date; + if (! range_finish || value_date > *range_finish) + range_finish = value_date; +#if defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 7 +#pragma GCC diagnostic pop +#endif + } + } + component_posts.clear(); + + std::ostringstream out_date; + if (spec_fmt) { + out_date << format_date(*range_finish, FMT_CUSTOM, spec_fmt); + } + else if (date_format) { + out_date << "- " << format_date(*range_finish, FMT_CUSTOM, + date_format->c_str()); + } + else { + out_date << "- " << format_date(*range_finish); + } + + xact_t& xact = temps.create_xact(); + xact.payee = out_date.str(); + xact._date = *range_start; + + foreach (values_map::value_type& pair, values) + handle_value(/* value= */ pair.second.value, + /* account= */ pair.second.account, + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ *range_finish, + /* act_date_p= */ false); + + values.clear(); +} + +void subtotal_posts::operator()(post_t& post) +{ + component_posts.push_back(&post); + + account_t * acct = post.reported_account(); + assert(acct); + +#if 0 + // jww (2012-04-06): The problem with doing this early is that + // fn_display_amount will recalculate this again. For example, if you + // use --invert, it will invert both here and in the display amount, + // effectively negating it. + bind_scope_t bound_scope(*amount_expr.get_context(), post); + value_t amount(amount_expr.calc(bound_scope)); +#else + value_t amount(post.amount); +#endif + + post.xdata().compound_value = amount; + post.xdata().add_flags(POST_EXT_COMPOUND); + + values_map::iterator i = values.find(acct->fullname()); + if (i == values.end()) { +#if DEBUG_ON + std::pair result = +#endif + values.insert(values_pair + (acct->fullname(), + acct_value_t(acct, amount, post.has_flags(POST_VIRTUAL), + post.has_flags(POST_MUST_BALANCE)))); +#if DEBUG_ON + assert(result.second); +#endif + } else { + if (post.has_flags(POST_VIRTUAL) != (*i).second.is_virtual) + throw_(std::logic_error, + _("'equity' cannot accept virtual and " + "non-virtual postings to the same account")); + + add_or_set_value((*i).second.value, amount); + } + + // If the account for this post is all virtual, mark it as + // such, so that `handle_value' can show "(Account)" for accounts + // that contain only virtual posts. + + post.reported_account()->xdata().add_flags(ACCOUNT_EXT_AUTO_VIRTUALIZE); + + if (! post.has_flags(POST_VIRTUAL)) + post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_NON_VIRTUALS); + else if (! post.has_flags(POST_MUST_BALANCE)) + post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS); +} + +void interval_posts::report_subtotal(const date_interval_t& ival) +{ + if (exact_periods) + subtotal_posts::report_subtotal(); + else + subtotal_posts::report_subtotal(NULL, ival); +} + +namespace { + struct sort_posts_by_date { + bool operator()(post_t * left, post_t * right) const { + return left->date() < right->date(); + } + }; +} + +void interval_posts::operator()(post_t& post) +{ + // If there is a duration (such as weekly), we must generate the + // report in two passes. Otherwise, we only have to check whether the + // post falls within the reporting period. + + if (interval.duration) { + all_posts.push_back(&post); + } + else if (interval.find_period(post.date())) { + item_handler::operator()(post); + } +} + +void interval_posts::flush() +{ + if (! interval.duration) { + item_handler::flush(); + return; + } + + // Sort all the postings we saw by date ascending + std::stable_sort(all_posts.begin(), all_posts.end(), + sort_posts_by_date()); + + // only if the interval has no start use the earliest post + if (!(interval.begin() && interval.find_period(*interval.begin()))) + // Determine the beginning interval by using the earliest post + if (all_posts.size() > 0 && all_posts.front() + && !interval.find_period(all_posts.front()->date())) + throw_(std::logic_error, _("Failed to find period for interval report")); + + // Walk the interval forward reporting all posts within each one + // before moving on, until we reach the end of all_posts + bool saw_posts = false; + for (std::deque::iterator i = all_posts.begin(); + i != all_posts.end(); ) { + post_t * post(*i); + + DEBUG("filters.interval", + "Considering post " << post->date() << " = " << post->amount); +#if DEBUG_ON + DEBUG("filters.interval", "interval is:"); + debug_interval(interval); +#endif + assert(! interval.finish || post->date() < *interval.finish); + + if (interval.within_period(post->date())) { + DEBUG("filters.interval", "Calling subtotal_posts::operator()"); + subtotal_posts::operator()(*post); + ++i; + saw_posts = true; + } else { + if (saw_posts) { + DEBUG("filters.interval", + "Calling subtotal_posts::report_subtotal()"); + report_subtotal(interval); + saw_posts = false; + } + else if (generate_empty_posts) { + // Generate a null posting, so the intervening periods can be + // seen when -E is used, or if the calculated amount ends up + // being non-zero + xact_t& null_xact = temps.create_xact(); + null_xact._date = interval.inclusive_end(); + + post_t& null_post = temps.create_post(null_xact, empty_account); + null_post.add_flags(POST_CALCULATED); + null_post.amount = 0L; + + subtotal_posts::operator()(null_post); + report_subtotal(interval); + } + + DEBUG("filters.interval", "Advancing interval"); + ++interval; + } + } + + // If the last postings weren't reported, do so now. + if (saw_posts) { + DEBUG("filters.interval", + "Calling subtotal_posts::report_subtotal() at end"); + report_subtotal(interval); + } + + // Tell our parent class to flush + subtotal_posts::flush(); +} + +namespace { + struct create_post_from_amount + { + post_handler_ptr handler; + xact_t& xact; + account_t& balance_account; + temporaries_t& temps; + + explicit create_post_from_amount(post_handler_ptr _handler, + xact_t& _xact, + account_t& _balance_account, + temporaries_t& _temps) + : handler(_handler), xact(_xact), + balance_account(_balance_account), temps(_temps) { + TRACE_CTOR(create_post_from_amount, + "post_handler_ptr, xact_t&, account_t&, temporaries_t&"); + } + create_post_from_amount(const create_post_from_amount& other) + : handler(other.handler), xact(other.xact), + balance_account(other.balance_account), temps(other.temps) { + TRACE_CTOR(create_post_from_amount, "copy"); + } + ~create_post_from_amount() throw() { + TRACE_DTOR(create_post_from_amount); + } + + void operator()(const amount_t& amount) { + post_t& balance_post = temps.create_post(xact, &balance_account); + balance_post.amount = - amount; + (*handler)(balance_post); + } + }; +} + +void posts_as_equity::report_subtotal() +{ + date_t finish; + foreach (post_t * post, component_posts) { + date_t date = post->date(); + if (! is_valid(finish) || date > finish) + finish = date; + } + component_posts.clear(); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Opening Balances"); + xact._date = finish; + + value_t total = 0L; + foreach (values_map::value_type& pair, values) { + value_t value(pair.second.value.strip_annotations(report.what_to_keep())); + if (! value.is_zero()) { + if (value.is_balance()) { + value.as_balance_lval().map_sorted_amounts + ([&](const amount_t& amt) { + if (! amt.is_zero()) + handle_value(/* value= */ amt, + /* account= */ pair.second.account, + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ finish, + /* act_date_p= */ false); + }); + } else { + handle_value(/* value= */ value.to_amount(), + /* account= */ pair.second.account, + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ finish, + /* act_date_p= */ false); + } + } + + if (! pair.second.is_virtual || pair.second.must_balance) + total += value; + } + values.clear(); + + // This last part isn't really needed, since an Equity:Opening + // Balances posting with a null amount will automatically balance with + // all the other postings generated. But it does make the full + // balancing amount clearer to the user. + if (! total.is_zero()) { + create_post_from_amount post_creator(handler, xact, + *balance_account, temps); + if (total.is_balance()) + total.as_balance_lval().map_sorted_amounts(post_creator); + else + post_creator(total.to_amount()); + } +} + +void by_payee_posts::flush() +{ + foreach (payee_subtotals_map::value_type& pair, payee_subtotals) + pair.second->report_subtotal(pair.first.c_str()); + + item_handler::flush(); + + payee_subtotals.clear(); +} + +void by_payee_posts::operator()(post_t& post) +{ + payee_subtotals_map::iterator i = payee_subtotals.find(post.payee()); + if (i == payee_subtotals.end()) { + payee_subtotals_pair + temp(post.payee(), + shared_ptr(new subtotal_posts(handler, amount_expr))); + std::pair result + = payee_subtotals.insert(temp); + + assert(result.second); + if (! result.second) + return; + i = result.first; + } + + (*(*i).second)(post); +} + +void transfer_details::operator()(post_t& post) +{ + xact_t& xact = temps.copy_xact(*post.xact); + xact._date = post.date(); + + post_t& temp = temps.copy_post(post, xact); + temp.set_state(post.state()); + + bind_scope_t bound_scope(scope, temp); + value_t substitute(expr.calc(bound_scope)); + + if (! substitute.is_null()) { + switch (which_element) { + case SET_DATE: + temp._date = substitute.to_date(); + break; + + case SET_ACCOUNT: { + string account_name = substitute.to_string(); + if (! account_name.empty() && + account_name[account_name.length() - 1] != ':') { + account_t * prev_account = temp.account; + temp.account->remove_post(&temp); + + account_name += ':'; + account_name += prev_account->fullname(); + + std::list account_names; + split_string(account_name, ':', account_names); + temp.account = create_temp_account_from_path(account_names, temps, + xact.journal->master); + temp.account->add_post(&temp); + + temp.account->add_flags(prev_account->flags()); + if (prev_account->has_xdata()) + temp.account->xdata().add_flags(prev_account->xdata().flags()); + } + break; + } + + case SET_PAYEE: + xact.payee = substitute.to_string(); + break; + } + } + + item_handler::operator()(temp); +} + +void day_of_week_posts::flush() +{ + for (int i = 0; i < 7; i++) { + foreach (post_t * post, days_of_the_week[i]) + subtotal_posts::operator()(*post); + subtotal_posts::report_subtotal("%As"); + days_of_the_week[i].clear(); + } + + subtotal_posts::flush(); +} + +void generate_posts::add_period_xacts(period_xacts_list& period_xacts) +{ + foreach (period_xact_t * xact, period_xacts) + foreach (post_t * post, xact->posts) + add_post(xact->period, *post); +} + +void generate_posts::add_post(const date_interval_t& period, post_t& post) +{ + pending_posts.push_back(pending_posts_pair(period, &post)); +} + +void budget_posts::report_budget_items(const date_t& date) +{ + { // Cleanup pending items that finished before date + // We have to keep them until the last day they apply because operator() needs them to see if a + // posting is budgeted or not + std::list posts_to_erase; + for (pending_posts_list::iterator i = pending_posts.begin(); i != pending_posts.end(); i++) { + pending_posts_list::value_type& pair(*i); + if (pair.first.finish && ! pair.first.start && pair.first.finish < date) { + posts_to_erase.push_back(i); + } + } + foreach (pending_posts_list::iterator& i, posts_to_erase) + pending_posts.erase(i); + } + + if (pending_posts.size() == 0) + return; + + bool reported; + do { + reported = false; + for (pending_posts_list::iterator i = pending_posts.begin(); + i != pending_posts.end(); + i++) { + pending_posts_list::value_type& pair(*i); + + if (pair.first.finish && ! pair.first.start) + continue; // skip expired posts + + optional begin = pair.first.start; + if (! begin) { + optional range_begin; + if (pair.first.range) + range_begin = pair.first.range->begin(); + + DEBUG("budget.generate", "Finding period for pending post"); + if (! pair.first.find_period(range_begin ? *range_begin : date)) + continue; + if (! pair.first.start) + throw_(std::logic_error, + _("Failed to find period for periodic transaction")); + begin = pair.first.start; + } + +#if DEBUG_ON + DEBUG("budget.generate", "begin = " << *begin); + DEBUG("budget.generate", "date = " << date); + if (pair.first.finish) + DEBUG("budget.generate", "pair.first.finish = " << *pair.first.finish); +#endif + + if (*begin <= date && + (! pair.first.finish || *begin < *pair.first.finish)) { + post_t& post = *pair.second; + + ++pair.first; + DEBUG("budget.generate", "Reporting budget for " + << post.reported_account()->fullname()); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Budget transaction"); + xact._date = begin; + + post_t& temp = temps.copy_post(post, xact); + temp.amount.in_place_negate(); + + if (flags & BUDGET_WRAP_VALUES) { + value_t seq; + seq.push_back(0L); + seq.push_back(temp.amount); + + temp.xdata().compound_value = seq; + temp.xdata().add_flags(POST_EXT_COMPOUND); + } + + item_handler::operator()(temp); + + reported = true; + } + } + } while (reported); +} + +void budget_posts::operator()(post_t& post) +{ + bool post_in_budget = false; + + foreach (pending_posts_list::value_type& pair, pending_posts) { + for (account_t * acct = post.reported_account(); + acct; + acct = acct->parent) { + if (acct == (*pair.second).reported_account()) { + post_in_budget = true; + // Report the post as if it had occurred in the parent account. + if (post.reported_account() != acct) + post.set_reported_account(acct); + goto handle; + } + } + } + + handle: + if (post_in_budget && flags & BUDGET_BUDGETED) { + report_budget_items(post.date()); + item_handler::operator()(post); + } + else if (! post_in_budget && flags & BUDGET_UNBUDGETED) { + item_handler::operator()(post); + } +} + +void budget_posts::flush() +{ + if (flags & BUDGET_BUDGETED) + report_budget_items(terminus); + + item_handler::flush(); +} + +void forecast_posts::add_post(const date_interval_t& period, post_t& post) +{ + date_interval_t i(period); + if (! i.start && ! i.find_period(CURRENT_DATE())) + return; + + generate_posts::add_post(i, post); + + // Advance the period's interval until it is at or beyond the current + // date. + while (*i.start < CURRENT_DATE()) + ++i; +} + +void forecast_posts::flush() +{ + posts_list passed; + date_t last = CURRENT_DATE(); + + // If there are period transactions to apply in a continuing series until + // the forecast condition is met, generate those transactions now. Note + // that no matter what, we abandon forecasting beyond the next 5 years. + // + // It works like this: + // + // Earlier, in forecast_posts::add_period_xacts, we cut up all the periodic + // transactions into their components postings, so that we have N "periodic + // postings". For example, if the user had this: + // + // ~ daily + // Expenses:Food $10 + // Expenses:Auto:Gas $20 + // ~ monthly + // Expenses:Food $100 + // Expenses:Auto:Gas $200 + // + // We now have 4 periodic postings in `pending_posts'. + // + // Each periodic postings gets its own copy of its parent transaction's + // period, which is modified as we go. This is found in the second member + // of the pending_posts_list for each posting. + // + // The algorithm below works by iterating through the N periodic postings + // over and over, until each of them mets the termination critera for the + // forecast and is removed from the set. + + while (pending_posts.size() > 0) { + // At each step through the loop, we find the first periodic posting whose + // period contains the earliest starting date. + pending_posts_list::iterator least = pending_posts.begin(); + for (pending_posts_list::iterator i = ++pending_posts.begin(); + i != pending_posts.end(); + i++) { + assert((*i).first.start); + assert((*least).first.start); + if (*(*i).first.start < *(*least).first.start) + least = i; + } + +#if !NO_ASSERTS + if ((*least).first.finish) + assert(*(*least).first.start < *(*least).first.finish); +#endif + + // If the next date in the series for this periodic posting is more than 5 + // years beyond the last valid post we generated, drop it from further + // consideration. + date_t& next(*(*least).first.next); + assert(next > *(*least).first.start); + + if (static_cast((next - last).days()) > + static_cast(365U) * forecast_years) { + DEBUG("filters.forecast", + "Forecast transaction exceeds " << forecast_years + << " years beyond today"); + pending_posts.erase(least); + continue; + } + + // `post' refers to the posting defined in the period transaction. We + // make a copy of it within a temporary transaction with the payee + // "Forecast transaction". + post_t& post = *(*least).second; + xact_t& xact = temps.create_xact(); + xact.payee = _("Forecast transaction"); + xact._date = next; + post_t& temp = temps.copy_post(post, xact); + + // Submit the generated posting + DEBUG("filters.forecast", + "Forecast transaction: " << temp.date() + << " " << temp.account->fullname() + << " " << temp.amount); + item_handler::operator()(temp); + + // If the generated posting matches the user's report query, check whether + // it also fails to match the continuation condition for the forecast. If + // it does, drop this periodic posting from consideration. + if (temp.has_xdata() && temp.xdata().has_flags(POST_EXT_MATCHES)) { + DEBUG("filters.forecast", " matches report query"); + bind_scope_t bound_scope(context, temp); + if (! pred(bound_scope)) { + DEBUG("filters.forecast", " fails to match continuation criteria"); + pending_posts.erase(least); + continue; + } + } + + // Increment the 'least', but remove it from pending_posts if it + // exceeds its own boundaries. + ++(*least).first; + if (! (*least).first.start) { + pending_posts.erase(least); + continue; + } + } + + item_handler::flush(); +} + +inject_posts::inject_posts(post_handler_ptr handler, + const string& tag_list, + account_t * master) + : item_handler(handler) +{ + scoped_array buf(new char[tag_list.length() + 1]); + std::strcpy(buf.get(), tag_list.c_str()); + + for (char * q = std::strtok(buf.get(), ","); + q; + q = std::strtok(NULL, ",")) { + std::list account_names; + split_string(q, ':', account_names); + + account_t * account = + create_temp_account_from_path(account_names, temps, master); + account->add_flags(ACCOUNT_GENERATED); + + tags_list.push_back + (tags_list_pair(q, tag_mapping_pair(account, tag_injected_set()))); + } + + TRACE_CTOR(inject_posts, "post_handler_ptr, string, account_t *"); +} + +void inject_posts::operator()(post_t& post) +{ + foreach (tags_list_pair& pair, tags_list) { + optional tag_value = post.get_tag(pair.first, false); + // When checking if the transaction has the tag, only inject once + // per transaction. + if (! tag_value && + pair.second.second.find(post.xact) == pair.second.second.end() && + (tag_value = post.xact->get_tag(pair.first))) + pair.second.second.insert(post.xact); + + if (tag_value) { + xact_t& xact = temps.copy_xact(*post.xact); + xact._date = post.date(); + xact.add_flags(ITEM_GENERATED); + post_t& temp = temps.copy_post(post, xact); + + temp.account = pair.second.first; + temp.amount = tag_value->to_amount(); + temp.add_flags(ITEM_GENERATED); + + item_handler::operator()(temp); + } + } + + item_handler::operator()(post); +} + +} // namespace ledger diff --git a/src/filters.h b/src/filters.h new file mode 100644 index 00000000..cf053a24 --- /dev/null +++ b/src/filters.h @@ -0,0 +1,1076 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup report + */ + +/** + * @file filters.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _FILTERS_H +#define _FILTERS_H + +#include "chain.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "temps.h" + +namespace ledger { + +////////////////////////////////////////////////////////////////////// +// +// Posting collector +// + +class post_splitter : public item_handler +{ +public: + typedef std::map value_to_posts_map; + typedef function custom_flusher_t; + +protected: + value_to_posts_map posts_map; + post_handler_ptr post_chain; + report_t& report; + expr_t& group_by_expr; + custom_flusher_t preflush_func; + optional postflush_func; + +public: + post_splitter(post_handler_ptr _post_chain, + report_t& _report, + expr_t& _group_by_expr) + : post_chain(_post_chain), report(_report), + group_by_expr(_group_by_expr) { + preflush_func = bind(&post_splitter::print_title, this, _1); + TRACE_CTOR(post_splitter, "scope_t&, post_handler_ptr, expr_t"); + } + virtual ~post_splitter() { + TRACE_DTOR(post_splitter); + } + + void set_preflush_func(custom_flusher_t functor) { + preflush_func = functor; + } + void set_postflush_func(custom_flusher_t functor) { + postflush_func = functor; + } + + virtual void print_title(const value_t& val); + + virtual void flush(); + virtual void operator()(post_t& post); + + virtual void clear() { + posts_map.clear(); + post_chain->clear(); + item_handler::clear(); + } +}; + +////////////////////////////////////////////////////////////////////// +// +// Posting filters +// + +class ignore_posts : public item_handler +{ +public: + virtual void operator()(post_t&) {} +}; + +class collect_posts : public item_handler +{ +public: + std::vector posts; + + collect_posts() : item_handler() { + TRACE_CTOR(collect_posts, ""); + } + virtual ~collect_posts() { + TRACE_DTOR(collect_posts); + } + + std::size_t length() const { + return posts.size(); + } + + std::vector::iterator begin() { + return posts.begin(); + } + std::vector::iterator end() { + return posts.end(); + } + + virtual void flush() {} + virtual void operator()(post_t& post) { + posts.push_back(&post); + } + + virtual void clear() { + posts.clear(); + item_handler::clear(); + } +}; + +template +class pass_down_posts : public item_handler +{ + pass_down_posts(); + +public: + pass_down_posts(post_handler_ptr handler, Iterator& iter) + : item_handler(handler) { + while (post_t * post = *iter) { + try { + item_handler::operator()(*post); + } + catch (const std::exception&) { + add_error_context(item_context(*post, _("While handling posting"))); + throw; + } + iter.increment(); + } + + item_handler::flush(); + + TRACE_CTOR(pass_down_posts, "post_handler_ptr, posts_iterator"); + } + + virtual ~pass_down_posts() { + TRACE_DTOR(pass_down_posts); + } +}; + +class push_to_posts_list : public item_handler +{ + push_to_posts_list(); + +public: + posts_list& posts; + + push_to_posts_list(posts_list& _posts) : posts(_posts) { + TRACE_CTOR(push_to_posts_list, "posts_list&"); + } + virtual ~push_to_posts_list() { + TRACE_DTOR(push_to_posts_list); + } + + virtual void operator()(post_t& post) { + posts.push_back(&post); + } +}; + +class truncate_xacts : public item_handler +{ + int head_count; + int tail_count; + bool completed; + + posts_list posts; + std::size_t xacts_seen; + xact_t * last_xact; + + truncate_xacts(); + +public: + truncate_xacts(post_handler_ptr handler, + int _head_count, int _tail_count) + : item_handler(handler), + head_count(_head_count), tail_count(_tail_count), + completed(false), xacts_seen(0), last_xact(NULL) { + TRACE_CTOR(truncate_xacts, "post_handler_ptr, int, int"); + } + virtual ~truncate_xacts() { + TRACE_DTOR(truncate_xacts); + } + + virtual void flush(); + virtual void operator()(post_t& post); + + virtual void clear() { + completed = false; + posts.clear(); + xacts_seen = 0; + last_xact = NULL; + + item_handler::clear(); + } +}; + +class sort_posts : public item_handler +{ + typedef std::deque posts_deque; + + posts_deque posts; + expr_t sort_order; + report_t& report; + + sort_posts(); + +public: + sort_posts(post_handler_ptr handler, const expr_t& _sort_order, + report_t& _report) + : item_handler(handler), sort_order(_sort_order), report(_report) { + TRACE_CTOR(sort_posts, "post_handler_ptr, const value_expr&, report_t&"); + } + sort_posts(post_handler_ptr handler, const string& _sort_order, + report_t& _report) + : item_handler(handler), sort_order(_sort_order), report(_report) { + TRACE_CTOR(sort_posts, "post_handler_ptr, const string&, report_t&"); + } + virtual ~sort_posts() { + TRACE_DTOR(sort_posts); + } + + virtual void post_accumulated_posts(); + + virtual void flush() { + post_accumulated_posts(); + item_handler::flush(); + } + + virtual void operator()(post_t& post) { + posts.push_back(&post); + } + + virtual void clear() { + posts.clear(); + sort_order.mark_uncompiled(); + + item_handler::clear(); + } +}; + +class sort_xacts : public item_handler +{ + sort_posts sorter; + xact_t * last_xact; + + sort_xacts(); + +public: + sort_xacts(post_handler_ptr handler, const expr_t& _sort_order, + report_t& _report) + : sorter(handler, _sort_order, _report) { + TRACE_CTOR(sort_xacts, + "post_handler_ptr, const value_expr&, report_t&"); + } + sort_xacts(post_handler_ptr handler, const string& _sort_order, + report_t& _report) + : sorter(handler, _sort_order, _report) { + TRACE_CTOR(sort_xacts, + "post_handler_ptr, const string&, report_t&"); + } + virtual ~sort_xacts() { + TRACE_DTOR(sort_xacts); + } + + virtual void flush() { + sorter.flush(); + item_handler::flush(); + } + + virtual void operator()(post_t& post) { + if (last_xact && post.xact != last_xact) + sorter.post_accumulated_posts(); + + sorter(post); + + last_xact = post.xact; + } + + virtual void clear() { + sorter.clear(); + last_xact = NULL; + + item_handler::clear(); + } +}; + +class filter_posts : public item_handler +{ + predicate_t pred; + scope_t& context; + + filter_posts(); + +public: + filter_posts(post_handler_ptr handler, + const predicate_t& predicate, + scope_t& _context) + : item_handler(handler), pred(predicate), context(_context) { + TRACE_CTOR(filter_posts, "post_handler_ptr, predicate_t, scope_t&"); + } + virtual ~filter_posts() { + TRACE_DTOR(filter_posts); + } + + virtual void operator()(post_t& post) { + bind_scope_t bound_scope(context, post); + if (pred(bound_scope)) { + post.xdata().add_flags(POST_EXT_MATCHES); + (*handler)(post); + } + } + + virtual void clear() { + pred.mark_uncompiled(); + item_handler::clear(); + } +}; + +class anonymize_posts : public item_handler +{ + typedef std::map commodity_index_map; + typedef variate_generator > int_generator_t; + + temporaries_t temps; + commodity_index_map comms; + std::size_t next_comm_id; + xact_t * last_xact; + mt19937 rnd_gen; + uniform_int<> integer_range; + int_generator_t integer_gen; + + anonymize_posts(); + +public: + anonymize_posts(post_handler_ptr handler) + : item_handler(handler), next_comm_id(0), last_xact(NULL), + rnd_gen(static_cast(static_cast(std::time(0)))), + integer_range(1, 2000000000L), + integer_gen(rnd_gen, integer_range) { + TRACE_CTOR(anonymize_posts, "post_handler_ptr"); + } + virtual ~anonymize_posts() { + TRACE_DTOR(anonymize_posts); + handler.reset(); + } + + void render_commodity(amount_t& amt); + + virtual void operator()(post_t& post); + + virtual void clear() { + temps.clear(); + comms.clear(); + last_xact = NULL; + + item_handler::clear(); + } +}; + +class calc_posts : public item_handler +{ + post_t * last_post; + expr_t& amount_expr; + bool calc_running_total; + + calc_posts(); + +public: + calc_posts(post_handler_ptr handler, + expr_t& _amount_expr, + bool _calc_running_total = false) + : item_handler(handler), last_post(NULL), + amount_expr(_amount_expr), calc_running_total(_calc_running_total) { + TRACE_CTOR(calc_posts, "post_handler_ptr, expr_t&, bool"); + } + virtual ~calc_posts() { + TRACE_DTOR(calc_posts); + } + + virtual void operator()(post_t& post); + + virtual void clear() { + last_post = NULL; + amount_expr.mark_uncompiled(); + + item_handler::clear(); + } +}; + +class collapse_posts : public item_handler +{ + expr_t& amount_expr; + predicate_t display_predicate; + predicate_t only_predicate; + value_t subtotal; + std::size_t count; + xact_t * last_xact; + post_t * last_post; + temporaries_t temps; + account_t * totals_account; + bool only_collapse_if_zero; + std::list component_posts; + report_t& report; + + collapse_posts(); + +public: + collapse_posts(post_handler_ptr handler, + report_t& _report, + expr_t& _amount_expr, + predicate_t _display_predicate, + predicate_t _only_predicate, + bool _only_collapse_if_zero = false) + : item_handler(handler), amount_expr(_amount_expr), + display_predicate(_display_predicate), + only_predicate(_only_predicate), count(0), + last_xact(NULL), last_post(NULL), + only_collapse_if_zero(_only_collapse_if_zero), report(_report) { + create_accounts(); + TRACE_CTOR(collapse_posts, "post_handler_ptr, ..."); + } + virtual ~collapse_posts() { + TRACE_DTOR(collapse_posts); + handler.reset(); + } + + void create_accounts() { + totals_account = &temps.create_account(_("")); + } + + virtual void flush() { + report_subtotal(); + item_handler::flush(); + } + + void report_subtotal(); + + virtual void operator()(post_t& post); + + virtual void clear() { + amount_expr.mark_uncompiled(); + display_predicate.mark_uncompiled(); + only_predicate.mark_uncompiled(); + + subtotal = value_t(); + count = 0; + last_xact = NULL; + last_post = NULL; + + temps.clear(); + create_accounts(); + component_posts.clear(); + + item_handler::clear(); + } +}; + +class related_posts : public item_handler +{ + posts_list posts; + bool also_matching; + + related_posts(); + +public: + related_posts(post_handler_ptr handler, + const bool _also_matching = false) + : item_handler(handler), + also_matching(_also_matching) { + TRACE_CTOR(related_posts, "post_handler_ptr, const bool"); + } + virtual ~related_posts() throw() { + TRACE_DTOR(related_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post) { + post.xdata().add_flags(POST_EXT_RECEIVED); + posts.push_back(&post); + } + + virtual void clear() { + posts.clear(); + item_handler::clear(); + } +}; + +class display_filter_posts : public item_handler +{ + // This filter requires that calc_posts be used at some point + // later in the chain. + + report_t& report; + expr_t& display_amount_expr; + expr_t& display_total_expr; + bool show_rounding; + value_t last_display_total; + temporaries_t temps; + account_t * rounding_account; + + display_filter_posts(); + +public: + account_t * revalued_account; + + display_filter_posts(post_handler_ptr handler, + report_t& _report, + bool _show_rounding); + + virtual ~display_filter_posts() { + TRACE_DTOR(display_filter_posts); + handler.reset(); + } + + void create_accounts() { + rounding_account = &temps.create_account(_("")); + revalued_account = &temps.create_account(_("")); + } + + bool output_rounding(post_t& post); + + virtual void operator()(post_t& post); + + virtual void clear() { + display_amount_expr.mark_uncompiled(); + display_total_expr.mark_uncompiled(); + + last_display_total = value_t(); + + temps.clear(); + item_handler::clear(); + + create_accounts(); + } +}; + +class changed_value_posts : public item_handler +{ + // This filter requires that calc_posts be used at some point + // later in the chain. + + report_t& report; + expr_t& total_expr; + expr_t& display_total_expr; + bool changed_values_only; + bool historical_prices_only; + bool for_accounts_report; + bool show_unrealized; + post_t * last_post; + value_t last_total; + value_t repriced_total; + temporaries_t temps; + account_t * revalued_account; + account_t * gains_equity_account; + account_t * losses_equity_account; + + display_filter_posts * display_filter; + + changed_value_posts(); + +public: + changed_value_posts(post_handler_ptr handler, + report_t& _report, + bool _for_accounts_report, + bool _show_unrealized, + display_filter_posts * _display_filter); + + virtual ~changed_value_posts() { + TRACE_DTOR(changed_value_posts); + temps.clear(); + handler.reset(); + } + + void create_accounts() { + revalued_account = (display_filter ? display_filter->revalued_account : + &temps.create_account(_(""))); + } + + virtual void flush(); + + void output_revaluation(post_t& post, const date_t& current); + void output_intermediate_prices(post_t& post, const date_t& current); + + virtual void operator()(post_t& post); + + virtual void clear() { + total_expr.mark_uncompiled(); + display_total_expr.mark_uncompiled(); + + last_post = NULL; + last_total = value_t(); + + temps.clear(); + item_handler::clear(); + + create_accounts(); + } +}; + +class subtotal_posts : public item_handler +{ + subtotal_posts(); + +protected: + class acct_value_t + { + acct_value_t(); + + public: + account_t * account; + value_t value; + bool is_virtual; + bool must_balance; + + acct_value_t(account_t * a, bool _is_virtual = false, + bool _must_balance = false) + : account(a), is_virtual(_is_virtual), must_balance(_must_balance) { + TRACE_CTOR(acct_value_t, "account_t *, bool, bool"); + } + acct_value_t(account_t * a, value_t& v, bool _is_virtual = false, + bool _must_balance = false) + : account(a), value(v), is_virtual(_is_virtual), + must_balance(_must_balance) { + TRACE_CTOR(acct_value_t, "account_t *, value_t&, bool, bool"); + } + acct_value_t(const acct_value_t& av) + : account(av.account), value(av.value), is_virtual(av.is_virtual), + must_balance(av.must_balance) { + TRACE_CTOR(acct_value_t, "copy"); + } + ~acct_value_t() throw() { + TRACE_DTOR(acct_value_t); + } + }; + + typedef std::map values_map; + typedef std::pair values_pair; + +protected: + expr_t& amount_expr; + values_map values; + optional date_format; + temporaries_t temps; + std::deque component_posts; + +public: + subtotal_posts(post_handler_ptr handler, expr_t& _amount_expr, + const optional& _date_format = none) + : item_handler(handler), amount_expr(_amount_expr), + date_format(_date_format) { + TRACE_CTOR(subtotal_posts, + "post_handler_ptr, expr_t&, const optional&"); + } + virtual ~subtotal_posts() { + TRACE_DTOR(subtotal_posts); + handler.reset(); + } + + void report_subtotal(const char * spec_fmt = NULL, + const optional& interval = none); + + virtual void flush() { + if (values.size() > 0) + report_subtotal(); + item_handler::flush(); + } + virtual void operator()(post_t& post); + + virtual void clear() { + amount_expr.mark_uncompiled(); + values.clear(); + temps.clear(); + component_posts.clear(); + + item_handler::clear(); + } +}; + +class interval_posts : public subtotal_posts +{ + date_interval_t start_interval; + date_interval_t interval; + account_t * empty_account; + bool exact_periods; + bool generate_empty_posts; + + std::deque all_posts; + + interval_posts(); + +public: + + interval_posts(post_handler_ptr _handler, + expr_t& amount_expr, + const date_interval_t& _interval, + bool _exact_periods = false, + bool _generate_empty_posts = false) + : subtotal_posts(_handler, amount_expr), start_interval(_interval), + interval(start_interval), exact_periods(_exact_periods), + generate_empty_posts(_generate_empty_posts) { + create_accounts(); + TRACE_CTOR(interval_posts, + "post_handler_ptr, expr_t&, date_interval_t, bool, bool"); + } + virtual ~interval_posts() throw() { + TRACE_DTOR(interval_posts); + } + + void create_accounts() { + empty_account = &temps.create_account(_("")); + } + + void report_subtotal(const date_interval_t& ival); + +#if DEBUG_ON + void debug_interval(const date_interval_t& ival) { + if (ival.start) + DEBUG("filters.interval", "start = " << *ival.start); + else + DEBUG("filters.interval", "no start"); + + if (ival.finish) + DEBUG("filters.interval", "finish = " << *ival.finish); + else + DEBUG("filters.interval", "no finish"); + } +#endif + + virtual void operator()(post_t& post); + virtual void flush(); + + virtual void clear() { + interval = start_interval; + + subtotal_posts::clear(); + create_accounts(); + } +}; + +class posts_as_equity : public subtotal_posts +{ + report_t& report; + post_t * last_post; + account_t * equity_account; + account_t * balance_account; + + posts_as_equity(); + +public: + posts_as_equity(post_handler_ptr _handler, report_t& _report, + expr_t& amount_expr) + : subtotal_posts(_handler, amount_expr), report(_report) { + create_accounts(); + TRACE_CTOR(posts_as_equity, "post_handler_ptr, expr_t&"); + } + virtual ~posts_as_equity() throw() { + TRACE_DTOR(posts_as_equity); + } + + void create_accounts() { + equity_account = &temps.create_account(_("Equity")); + balance_account = equity_account->find_account(_("Opening Balances")); + } + + void report_subtotal(); + + virtual void flush() { + report_subtotal(); + subtotal_posts::flush(); + } + + virtual void clear() { + last_post = NULL; + subtotal_posts::clear(); + create_accounts(); + } +}; + +class by_payee_posts : public item_handler +{ + typedef std::map > payee_subtotals_map; + typedef std::pair > payee_subtotals_pair; + + expr_t& amount_expr; + payee_subtotals_map payee_subtotals; + + by_payee_posts(); + + public: + by_payee_posts(post_handler_ptr handler, expr_t& _amount_expr) + : item_handler(handler), amount_expr(_amount_expr) { + TRACE_CTOR(by_payee_posts, "post_handler_ptr, expr_t&"); + } + virtual ~by_payee_posts() { + TRACE_DTOR(by_payee_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post); + + virtual void clear() { + amount_expr.mark_uncompiled(); + payee_subtotals.clear(); + + item_handler::clear(); + } +}; + +class transfer_details : public item_handler +{ + account_t * master; + expr_t expr; + scope_t& scope; + temporaries_t temps; + + transfer_details(); + +public: + enum element_t { + SET_DATE, + SET_ACCOUNT, + SET_PAYEE + } which_element; + + transfer_details(post_handler_ptr handler, + element_t _which_element, + account_t * _master, + const expr_t& _expr, + scope_t& _scope) + : item_handler(handler), master(_master), + expr(_expr), scope(_scope), which_element(_which_element) { + TRACE_CTOR(transfer_details, + "post_handler_ptr, element_t, account_t *, expr_t, scope_t&"); + } + virtual ~transfer_details() { + TRACE_DTOR(transfer_details); + handler.reset(); + } + + virtual void operator()(post_t& post); + + virtual void clear() { + expr.mark_uncompiled(); + temps.clear(); + + item_handler::clear(); + } +}; + +class day_of_week_posts : public subtotal_posts +{ + posts_list days_of_the_week[7]; + + day_of_week_posts(); + +public: + day_of_week_posts(post_handler_ptr handler, expr_t& amount_expr) + : subtotal_posts(handler, amount_expr) { + TRACE_CTOR(day_of_week_posts, "post_handler_ptr, bool"); + } + virtual ~day_of_week_posts() throw() { + TRACE_DTOR(day_of_week_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post) { + days_of_the_week[post.date().day_of_week()].push_back(&post); + } + + virtual void clear() { + for (int i = 0; i < 7; i++) + days_of_the_week[i].clear(); + + subtotal_posts::clear(); + } +}; + +class generate_posts : public item_handler +{ + generate_posts(); + +protected: + typedef std::pair pending_posts_pair; + typedef std::list pending_posts_list; + + pending_posts_list pending_posts; + temporaries_t temps; + +public: + generate_posts(post_handler_ptr handler) + : item_handler(handler) { + TRACE_CTOR(generate_posts, "post_handler_ptr"); + } + + virtual ~generate_posts() { + TRACE_DTOR(generate_posts); + handler.reset(); + } + + void add_period_xacts(period_xacts_list& period_xacts); + + virtual void add_post(const date_interval_t& period, post_t& post); + + virtual void clear() { + pending_posts.clear(); + temps.clear(); + + item_handler::clear(); + } +}; + +class budget_posts : public generate_posts +{ +#define BUDGET_NO_BUDGET 0x00 +#define BUDGET_BUDGETED 0x01 +#define BUDGET_UNBUDGETED 0x02 +#define BUDGET_WRAP_VALUES 0x04 + + uint_least8_t flags; + date_t terminus; + + budget_posts(); + +public: + budget_posts(post_handler_ptr handler, + date_t _terminus, + uint_least8_t _flags = BUDGET_BUDGETED) + : generate_posts(handler), flags(_flags), terminus(_terminus) { + TRACE_CTOR(budget_posts, "post_handler_ptr, date_t, uint_least8_t"); + } + virtual ~budget_posts() throw() { + TRACE_DTOR(budget_posts); + } + + void report_budget_items(const date_t& date); + + virtual void flush(); + virtual void operator()(post_t& post); +}; + +class forecast_posts : public generate_posts +{ + predicate_t pred; + scope_t& context; + const std::size_t forecast_years; + + public: + forecast_posts(post_handler_ptr handler, + const predicate_t& predicate, + scope_t& _context, + const std::size_t _forecast_years) + : generate_posts(handler), pred(predicate), context(_context), + forecast_years(_forecast_years) { + TRACE_CTOR(forecast_posts, + "post_handler_ptr, predicate_t, scope_t&, std::size_t"); + } + virtual ~forecast_posts() throw() { + TRACE_DTOR(forecast_posts); + } + + virtual void add_post(const date_interval_t& period, post_t& post); + virtual void flush(); + + virtual void clear() { + pred.mark_uncompiled(); + generate_posts::clear(); + } +}; + +class inject_posts : public item_handler +{ + typedef std::set tag_injected_set; + typedef std::pair tag_mapping_pair; + typedef std::pair tags_list_pair; + + std::list tags_list; + temporaries_t temps; + + public: + inject_posts(post_handler_ptr handler, const string& tag_list, + account_t * master); + + virtual ~inject_posts() throw() { + TRACE_DTOR(inject_posts); + handler.reset(); + } + + virtual void operator()(post_t& post); +}; + +////////////////////////////////////////////////////////////////////// +// +// Account filters +// + +template +class pass_down_accounts : public item_handler +{ + pass_down_accounts(); + + optional pred; + optional context; + +public: + pass_down_accounts(acct_handler_ptr handler, + Iterator& iter, + const optional& _pred = none, + const optional& _context = none) + : item_handler(handler), pred(_pred), context(_context) { + TRACE_CTOR(pass_down_accounts, "acct_handler_ptr, accounts_iterator, ..."); + + while (account_t * account = *iter++) { + if (! pred) { + item_handler::operator()(*account); + } else { + bind_scope_t bound_scope(*context, *account); + if ((*pred)(bound_scope)) + item_handler::operator()(*account); + } + } + + item_handler::flush(); + } + + virtual ~pass_down_accounts() { + TRACE_DTOR(pass_down_accounts); + } + + virtual void clear() { + if (pred) + pred->mark_uncompiled(); + + item_handler::clear(); + } +}; + +} // namespace ledger + +#endif // _FILTERS_H diff --git a/src/flags.h b/src/flags.h new file mode 100644 index 00000000..42e2719a --- /dev/null +++ b/src/flags.h @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup util + */ + +/** + * @file flags.h + * @author John Wiegley + * + * @ingroup util + */ +#ifndef _FLAGS_H +#define _FLAGS_H + + +template +class supports_flags +{ +public: + typedef T flags_t; + +protected: + flags_t _flags; + +public: + supports_flags() : _flags(static_cast(0)) { + TRACE_CTOR(supports_flags, ""); + } + supports_flags(const supports_flags& arg) : _flags(arg._flags) { + TRACE_CTOR(supports_flags, "copy"); + } + supports_flags(const flags_t& arg) : _flags(arg) { + TRACE_CTOR(supports_flags, "const flags_t&"); + } + ~supports_flags() throw() { + TRACE_DTOR(supports_flags); + } + + supports_flags& operator=(const supports_flags& other) { + _flags = other._flags; + return *this; + } + + flags_t flags() const { + return _flags; + } + bool has_flags(const flags_t arg) const { + return _flags & arg; + } + + void set_flags(const flags_t arg) { + _flags = arg; + } + void clear_flags() { + _flags = static_cast(0); + } + void add_flags(const flags_t arg) { + _flags = static_cast(static_cast(_flags) | static_cast(arg)); + } + void drop_flags(const flags_t arg) { + _flags = static_cast(static_cast(_flags) & static_cast(~arg)); + } +}; + +template +class basic_flags_t : public supports_flags +{ +public: + basic_flags_t() { + TRACE_CTOR(basic_flags_t, ""); + } + basic_flags_t(const T& bits) { + TRACE_CTOR(basic_flags_t, "const T&"); + supports_flags::set_flags(bits); + } + basic_flags_t(const U& bits) { + TRACE_CTOR(basic_flags_t, "const U&"); + supports_flags::set_flags(static_cast(bits)); + } + ~basic_flags_t() throw() { + TRACE_DTOR(basic_flags_t); + } + + basic_flags_t(const basic_flags_t& other) + : supports_flags(other) { + TRACE_CTOR(basic_flags_t, "copy"); + } + basic_flags_t& operator=(const basic_flags_t& other) { + set_flags(other.flags()); + return *this; + } + basic_flags_t& operator=(const T& bits) { + set_flags(bits); + return *this; + } + + operator T() const { + return supports_flags::flags(); + } + operator U() const { + return supports_flags::flags(); + } + + basic_flags_t plus_flags(const T& arg) const { + basic_flags_t temp(*this); + temp.add_flags(arg); + return temp; + } + basic_flags_t minus_flags(const T& arg) const { + basic_flags_t temp(*this); + temp.drop_flags(arg); + return temp; + } +}; + +template +class delegates_flags : public boost::noncopyable +{ +public: + typedef T flags_t; + +protected: + supports_flags& _flags; + +public: + delegates_flags() : _flags() { + TRACE_CTOR(delegates_flags, ""); + } + delegates_flags(supports_flags& arg) : _flags(arg) { + TRACE_CTOR(delegates_flags, "const supports_flags&"); + } + ~delegates_flags() throw() { + TRACE_DTOR(delegates_flags); + } + + flags_t flags() const { + return _flags.flags(); + } + bool has_flags(const flags_t arg) const { + return _flags.has_flags(arg); + } + + void set_flags(const flags_t arg) { + _flags.set_flags(arg); + } + void clear_flags() { + _flags.clear_flags(); + } + void add_flags(const flags_t arg) { + _flags.add_flags(arg); + } + void drop_flags(const flags_t arg) { + _flags.drop_flags(arg); + } +}; + +#endif // _FLAGS_H diff --git a/src/format.cc b/src/format.cc new file mode 100644 index 00000000..5b9baa21 --- /dev/null +++ b/src/format.cc @@ -0,0 +1,698 @@ +/* + * Copyright (c) 2003-2019, John Wiegley. 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. + */ + +#include + +#include "format.h" +#include "scope.h" +#include "pstream.h" + +namespace ledger { + +format_t::elision_style_t format_t::default_style = TRUNCATE_TRAILING; +bool format_t::default_style_changed = false; + +void format_t::element_t::dump(std::ostream& out) const +{ + out << "Element: "; + + switch (type) { + case STRING: out << " STRING"; break; + case EXPR: out << " EXPR"; break; + } + + out << " flags: 0x" << std::hex << int(flags()); + out << " min: "; + out << std::right; + out.width(2); + out << std::dec << int(min_width); + out << " max: "; + out << std::right; + out.width(2); + out << std::dec << int(max_width); + + switch (type) { + case STRING: + out << " str: '" << boost::get(data) << "'" << std::endl; + break; + case EXPR: + out << " expr: " << boost::get(data) << std::endl; + break; + } +} + +namespace { + struct format_mapping_t { + char letter; + const char * expr; + } single_letter_mappings[] = { + { 'd', "aux_date ? format_date(date) + \"=\" + format_date(aux_date) : format_date(date)" }, + { 'D', "date" }, + { 'S', "filename" }, + { 'B', "beg_pos" }, + { 'b', "beg_line" }, + { 'E', "end_pos" }, + { 'e', "end_line" }, + { 'X', "\"* \" if cleared" }, + { 'Y', "\"* \" if xact.cleared" }, + { 'C', "\"(\" + code + \") \" if code" }, + { 'P', "payee" }, + { 'a', "account" }, + { 'A', "account" }, + { 't', "justify(scrub(display_amount), $min, $max, $left, color)" }, + { 'T', "justify(scrub(display_total), $min, $max, $left, color)" }, + { 'N', "note" }, + }; + + expr_t parse_single_expression(const char *& p, bool single_expr = true) + { + string temp(p); + ptristream str(const_cast(p)); + expr_t expr; + expr.parse(str, single_expr ? PARSE_SINGLE : PARSE_PARTIAL, temp); + if (str.eof()) { + expr.set_text(p); + p += std::strlen(p); + } else { + assert(str.good()); + std::istream::pos_type pos = str.tellg(); + expr.set_text(string(p, p + long(pos))); + p += long(pos) - 1; + + // Don't gobble up any whitespace + const char * base = p; + while (p >= base && std::isspace(*p)) + p--; + } + return expr; + } + + inline expr_t::ptr_op_t ident_node(const string& ident) + { + expr_t::ptr_op_t node(new expr_t::op_t(expr_t::op_t::IDENT)); + node->set_ident(ident); + return node; + } +} + +format_t::element_t * format_t::parse_elements(const string& fmt, + const optional& tmpl) +{ + unique_ptr result; + + element_t * current = NULL; + + static char buf[65535]; + char * q = buf; + + for (const char * p = fmt.c_str(); *p; p++) { + if (*p != '%' && *p != '\\') { + *q++ = *p; + continue; + } + + if (! result.get()) { + result.reset(new element_t); + current = result.get(); + } else { + current->next.reset(new element_t); + current = current->next.get(); + } + + if (q != buf) { + current->type = element_t::STRING; + current->data = string(buf, q); + q = buf; + + current->next.reset(new element_t); + current = current->next.get(); + } + + if (*p == '\\') { + p++; + current->type = element_t::STRING; + switch (*p) { + case 'b': current->data = string("\b"); break; + case 'f': current->data = string("\f"); break; + case 'n': current->data = string("\n"); break; + case 'r': current->data = string("\r"); break; + case 't': current->data = string("\t"); break; + case 'v': current->data = string("\v"); break; + case '\\': current->data = string("\\"); break; + default: current->data = string(1, *p); break; + } + continue; + } + + ++p; + while (*p == '-') { + switch (*p) { + case '-': + current->add_flags(ELEMENT_ALIGN_LEFT); + break; + } + ++p; + } + + std::size_t num = 0; + while (*p && std::isdigit(*p)) { + num *= 10; + num += static_cast(*p++ - '0'); + } + current->min_width = num; + + if (*p == '.') { + ++p; + num = 0; + while (*p && std::isdigit(*p)) { + num *= 10; + num += static_cast(*p++ - '0'); + } + current->max_width = num; + if (current->min_width == 0) + current->min_width = current->max_width; + } + + if (std::isalpha(*p)) { + bool found = false; + for (std::size_t i = 0; i < (sizeof(single_letter_mappings) / + sizeof(format_mapping_t)); i++) { + if (*p == single_letter_mappings[i].letter) { + std::ostringstream expr; + for (const char * ptr = single_letter_mappings[i].expr; *ptr;) { + if (*ptr == '$') { + const char * beg = ++ptr; + while (*ptr && std::isalpha(*ptr)) + ++ptr; + string::size_type klen = static_cast(ptr - beg); + string keyword(beg, 0, klen); + if (keyword == "min") + expr << (current->min_width > 0 ? + static_cast(current->min_width) : -1); + else if (keyword == "max") + expr << (current->max_width > 0 ? + static_cast(current->max_width) : -1); + else if (keyword == "left") + expr << (current->has_flags(ELEMENT_ALIGN_LEFT) ? "false" : "true"); +#if DEBUG_ON + else + assert("Unrecognized format substitution keyword" == NULL); +#endif + } else { + expr << *ptr++; + } + } + current->type = element_t::EXPR; + current->data = expr_t(expr.str()); + found = true; + break; + } + } + if (! found) + throw_(format_error, _f("Unrecognized formatting character: %1%") % *p); + } else { + switch (*p) { + case '%': + current->type = element_t::STRING; + current->data = string("%"); + break; + + case '$': { + if (! tmpl) + throw_(format_error, _("Prior field reference, but no template")); + + p++; + if (*p == '0' || (! std::isdigit(*p) && + *p != 'A' && *p != 'B' && *p != 'C' && + *p != 'D' && *p != 'E' && *p != 'F')) + throw_(format_error, _("%$ field reference must be a digit from 1-9")); + + int index = std::isdigit(*p) ? *p - '0' : (*p - 'A' + 10); + element_t * tmpl_elem = tmpl->elements.get(); + + for (int i = 1; i < index && tmpl_elem; i++) { + tmpl_elem = tmpl_elem->next.get(); + while (tmpl_elem && tmpl_elem->type != element_t::EXPR) + tmpl_elem = tmpl_elem->next.get(); + } + + if (! tmpl_elem) + throw_(format_error, _("%$ reference to a non-existent prior field")); + + *current = *tmpl_elem; + break; + } + + case '(': + case '{': { + bool format_amount = *p == '{'; + + current->type = element_t::EXPR; + current->data = parse_single_expression(p); + + // Wrap the subexpression in calls to justify and scrub + if (! format_amount) + break; + + expr_t::ptr_op_t op = boost::get(current->data).get_op(); + + expr_t::ptr_op_t call2_node(new expr_t::op_t(expr_t::op_t::O_CALL)); + { + call2_node->set_left(ident_node("justify")); + + { + expr_t::ptr_op_t args3_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + { + { + expr_t::ptr_op_t call1_node(new expr_t::op_t(expr_t::op_t::O_CALL)); + { + call1_node->set_left(ident_node("scrub")); + call1_node->set_right(op->kind == expr_t::op_t::O_CONS ? op->left() : op); + } + + args3_node->set_left(call1_node); + } + + expr_t::ptr_op_t args2_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + { + { + expr_t::ptr_op_t arg1_node(new expr_t::op_t(expr_t::op_t::VALUE)); + arg1_node->set_value(current->min_width > 0 ? + long(current->min_width) : -1); + + args2_node->set_left(arg1_node); + } + + { + expr_t::ptr_op_t args1_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + { + { + expr_t::ptr_op_t arg2_node(new expr_t::op_t(expr_t::op_t::VALUE)); + arg2_node->set_value(current->max_width > 0 ? + long(current->max_width) : -1); + + args1_node->set_left(arg2_node); + } + + { + expr_t::ptr_op_t arg3_node(new expr_t::op_t(expr_t::op_t::VALUE)); + arg3_node->set_value(! current->has_flags(ELEMENT_ALIGN_LEFT)); + + args1_node->set_right(arg3_node); + } + } + + args2_node->set_right(args1_node); + } + + args3_node->set_right(args2_node); + } + } + + call2_node->set_right(args3_node); + } + } + + current->min_width = 0; + current->max_width = 0; + + string prev_expr = boost::get(current->data).text(); + + expr_t::ptr_op_t colorize_op; + if (op->kind == expr_t::op_t::O_CONS) + colorize_op = op->has_right() ? op->right() : NULL; + + if (colorize_op) { + expr_t::ptr_op_t call3_node(new expr_t::op_t(expr_t::op_t::O_CALL)); + { + call3_node->set_left(ident_node("ansify_if")); + + { + expr_t::ptr_op_t args4_node(new expr_t::op_t(expr_t::op_t::O_CONS)); + { + args4_node->set_left(call2_node); // from above + args4_node->set_right(colorize_op); + } + + call3_node->set_right(args4_node); + } + } + + current->data = expr_t(call3_node); + } else { + current->data = expr_t(call2_node); + } + + boost::get(current->data).set_text(prev_expr); + break; + } + + default: + throw_(format_error, _f("Unrecognized formatting character: %1%") % *p); + } + } + } + + if (q != buf) { + if (! result.get()) { + result.reset(new element_t); + current = result.get(); + } else { + current->next.reset(new element_t); + current = current->next.get(); + } + current->type = element_t::STRING; + current->data = string(buf, q); + } + + return result.release(); +} + +string format_t::real_calc(scope_t& scope) +{ + std::ostringstream out_str; + + for (element_t * elem = elements.get(); elem; elem = elem->next.get()) { + std::ostringstream out; + string name; + + if (elem->has_flags(ELEMENT_ALIGN_LEFT)) + out << std::left; + else + out << std::right; + + switch (elem->type) { + case element_t::STRING: + if (elem->min_width > 0) + out.width(static_cast(elem->min_width)); + out << boost::get(elem->data); + break; + + case element_t::EXPR: { + expr_t& expr(boost::get(elem->data)); + try { + expr.compile(scope); + + value_t value; + if (expr.is_function()) { + call_scope_t args(scope); + args.push_back(long(elem->max_width)); + value = expr.get_function()(args); + } else { + value = expr.calc(scope); + } + DEBUG("format.expr", "value = (" << value << ")"); + + if (elem->min_width > 0) + value.print(out, static_cast(elem->min_width), -1, + ! elem->has_flags(ELEMENT_ALIGN_LEFT)); + else + out << value.to_string(); + } + catch (const calc_error&) { + string current_context = error_context(); + + add_error_context(_("While calculating format expression:")); + add_error_context(expr.context_to_str()); + + if (! current_context.empty()) + add_error_context(current_context); + throw; + } + break; + } + } + + if (elem->max_width > 0 || elem->min_width > 0) { + unistring temp(out.str()); + string result; + + if (elem->max_width > 0 && elem->max_width < temp.length()) { + result = truncate(temp, elem->max_width); + } else { + result = temp.extract(); + if (elem->min_width > temp.length()) + for (std::size_t i = 0; i < elem->min_width - temp.length(); i++) + result += " "; + } + out_str << result; + } else { + out_str << out.str(); + } + } + + return out_str.str(); +} + +string format_t::truncate(const unistring& ustr, + const std::size_t width, + const std::size_t account_abbrev_length) +{ + assert(width < 4095); + + const std::size_t len = ustr.length(); + if (width == 0 || len <= width) + return ustr.extract(); + + std::ostringstream buf; + + elision_style_t style = default_style; + if (account_abbrev_length > 0 && ! default_style_changed) + style = ABBREVIATE; + + switch (style) { + case TRUNCATE_LEADING: + // This method truncates at the beginning. + buf << ".." << ustr.extract(len - (width - 2), width - 2); + break; + + case TRUNCATE_MIDDLE: + // This method truncates in the middle. + buf << ustr.extract(0, (width - 2) / 2) + << ".." + << ustr.extract(len - ((width - 2) / 2 + (width - 2) % 2), + (width - 2) / 2 + (width - 2) % 2); + break; + + case ABBREVIATE: + if (account_abbrev_length > 0) { + // The algorithm here is complex, but aims to preserve the most + // information in the most useful places. + // + // Consider: You have an account name like + // 'Assets:Banking:Check:Register'. This account name, which is + // 29 characters long, must be shortened to fit in 20. How would + // you shorten it? + // + // The approach taken below is to compute the difference, or 9 + // characters, and then distribute this difference semi-evenly + // among first three segments of the account name, by taking + // characters until the difference is gone. Further, earlier + // segments will give up more of their share of letters than later + // segments, since the later segments usually contain more useful + // information. + + // First, chop up the Unicode string into individual segments. + std::list parts; + string::size_type beg = 0; + string strcopy(ustr.extract()); + for (string::size_type pos = strcopy.find(':'); + pos != string::npos; + beg = pos + 1, pos = strcopy.find(':', beg)) + parts.push_back(string(strcopy, beg, pos - beg)); + parts.push_back(string(strcopy, beg)); + + DEBUG("format.abbrev", "Account name: " << strcopy); + DEBUG("format.abbrev", + "Must fit a " << len << " char string in " << width << " chars"); + + // Figure out the lengths of all the parts. The last part is + // always displayed in full, while the former parts are + // distributed, with the latter parts being longer than the + // former, but with none shorter than account_abbrev_length. + std::list lens; +#if DEBUG_ON + int index = 0; +#endif + for (std::list::iterator i = parts.begin(); + i != parts.end(); + i++) { + std::size_t l = unistring(*i).length(); + DEBUG("format.abbrev", + "Segment " << ++index << " is " << l << " chars wide"); + lens.push_back(l); + } + + // Determine the "overflow", or how many chars in excess we are. + + std::size_t overflow = len - width; + DEBUG("format.abbrev", + "There are " << overflow << " chars of overflow"); + + // Walk through the first n-1 segments, and start subtracting + // letters to decrease the overflow. This is done in multiple + // passes until the overflow is gone, or we cannot reduce any + // further. The calculation to find the amount to remove is: + // + // overflow * (((len(segment) + counter) * iteration) / + // (len(string) - len(last_segment) - counter)) + // + // Where: + // overflow - the amount that needs to be removed + // counter - starts at n-1 for the first segment, then + // decreases by one until it reaches 0 for the + // last segment (which is never shortened). + // This value is used to weight the shrinkage + // so that earlier segments shrink faster. + // iteration - starts at 1, increase by 1 for every + // iteration of the loop + // + // In the example above, we have this account name: + // + // Assets:Banking:Check:Register + // + // Therefore, the amount to be removed from Assets is calculated as: + // + // 9 * (((6 + 3) * 1) / (29 - 8 - 3)) = ceil(4.5) = 5 + // + // However, since removing 5 chars would make the length of the + // segment shorter than the default minimum of 2, we can only + // remove 4 chars from Assets to reduce the overflow. And on it + // goes. + // + // The final result will be: As:Ban:Chec:Register + + std::size_t iteration = 1; + std::size_t len_minus_last = len - lens.back(); + while (overflow > 0) { + std::size_t overflow_at_start = overflow; + DEBUG("format.abbrev", + "Overflow starting at " << overflow << " chars"); +#if DEBUG_ON + index = 0; +#endif + std::size_t counter = lens.size(); + std::list::iterator x = parts.begin(); + for (std::list::iterator i = lens.begin(); + i != lens.end(); + i++) { + if (--counter == 0 || overflow == 0) + break; + DEBUG("format.abbrev", "Overflow is " << overflow << " chars"); + std::size_t adjust; + if (overflow == 1) + adjust = 1; + else + adjust = std::size_t + (std::ceil(double(overflow) * + ((double(*i + counter*3) * double(iteration)) / + (double(len_minus_last) - double(counter))))); + DEBUG("format.abbrev", "Weight calc: (" << overflow + << " * (((" << *i << " + " << counter << ") * " + << iteration << ") / (" << len_minus_last + << " - " << counter << ")))"); + if (adjust == 0) + adjust = 1; + else if (adjust > overflow) + adjust = overflow; + DEBUG("format.abbrev", "The weighted part is " << adjust << " chars"); + std::size_t slack = *i - std::min(*i, account_abbrev_length); + if (adjust > slack) + adjust = slack; + if (adjust > 0) { + DEBUG("format.abbrev", + "Reducing segment " << ++index << " by " << adjust << " chars"); + while (std::isspace((*x)[*i - adjust - 1]) && adjust < *i) { + DEBUG("format.abbrev", + "Segment ends in whitespace, adjusting down"); + ++adjust; + } + (*i) -= adjust; + DEBUG("format.abbrev", + "Segment " << index << " is now " << *i << " chars wide"); + if (adjust > overflow) + overflow = 0; + else + overflow -= adjust; + DEBUG("format.abbrev", "Overflow is now " << overflow << " chars"); + } + ++x; + } + DEBUG("format.abbrev", + "Overflow ending this time at " << overflow << " chars"); + if (overflow == overflow_at_start) + break; + iteration++; + } + + assert(parts.size() == lens.size()); + + std::list::iterator i = parts.begin(); + std::list::iterator l = lens.begin(); + std::ostringstream result; + + for (; i != parts.end() && l != lens.end(); i++, l++) { + std::list::iterator x = i; + if (++x == parts.end()) { + result << *i; + break; + } + + unistring temp(*i); + if (temp.length() > *l) + result << temp.extract(0, *l) << ":"; + else + result << *i << ":"; + } + + if (overflow > 0) { + // Even abbreviated its too big to show the last account, so + // abbreviate all but the last and truncate at the beginning. + unistring temp(result.str()); + assert(temp.length() > width - 2); + buf << ".." << temp.extract(temp.length() - (width - 2), width - 2); + } else { + buf << result.str(); + } + break; + } + // fall through... + + case TRUNCATE_TRAILING: + // This method truncates at the end (the default). + buf << ustr.extract(0, width - 2) << ".."; + break; + } + + return buf.str(); +} + +} // namespace ledger diff --git a/src/format.h b/src/format.h new file mode 100644 index 00000000..15431cf1 --- /dev/null +++ b/src/format.h @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup expr + */ + +/** + * @file format.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _FORMAT_H +#define _FORMAT_H + +#include "expr.h" +#include "unistring.h" + +namespace ledger { + +class unistring; + +DECLARE_EXCEPTION(format_error, std::runtime_error); + +class format_t : public expr_base_t, public noncopyable +{ + typedef expr_base_t base_type; + + struct element_t : public supports_flags<>, public noncopyable + { +#define ELEMENT_ALIGN_LEFT 0x01 + + enum kind_t { STRING, EXPR }; + + kind_t type; + std::size_t min_width; + std::size_t max_width; + variant data; + scoped_ptr next; + + element_t() throw() + : supports_flags<>(), type(STRING), min_width(0), max_width(0) { + TRACE_CTOR(element_t, ""); + } + ~element_t() throw() { + TRACE_DTOR(element_t); + } + + element_t& operator=(const element_t& elem) { + if (this != &elem) { + supports_flags<>::operator=(elem); + type = elem.type; + min_width = elem.min_width; + max_width = elem.max_width; + data = elem.data; + } + return *this; + } + + friend inline void mark_red(std::ostream& out, const element_t * elem) { + out.setf(std::ios::left); + out.width(0); + out << "\033[31m"; + + if (elem->has_flags(ELEMENT_ALIGN_LEFT)) + out << std::left; + else + out << std::right; + + if (elem->min_width > 0) + out.width(static_cast(elem->min_width)); + } + + void dump(std::ostream& out) const; + }; + + scoped_ptr elements; + +public: + static enum elision_style_t { + TRUNCATE_TRAILING, + TRUNCATE_MIDDLE, + TRUNCATE_LEADING, + ABBREVIATE + } default_style; + + static bool default_style_changed; + +private: + static element_t * parse_elements(const string& fmt, + const optional& tmpl); + +public: + format_t() : base_type() { + TRACE_CTOR(format_t, ""); + } + format_t(const string& _str, scope_t * context = NULL) + : base_type(context) { + if (! _str.empty()) + parse_format(_str); + TRACE_CTOR(format_t, "const string&"); + } + virtual ~format_t() { + TRACE_DTOR(format_t); + } + + void parse_format(const string& _format, + const optional& tmpl = none) { + elements.reset(parse_elements(_format, tmpl)); + set_text(_format); + } + + virtual void mark_uncompiled() { + for (element_t * elem = elements.get(); elem; elem = elem->next.get()) { + if (elem->type == element_t::EXPR) { + expr_t& expr(boost::get(elem->data)); + expr.mark_uncompiled(); + } + } + } + + virtual result_type real_calc(scope_t& scope); + + virtual void dump(std::ostream& out) const { + for (const element_t * elem = elements.get(); + elem; + elem = elem->next.get()) + elem->dump(out); + } + + static string truncate(const unistring& str, + const std::size_t width, + const std::size_t account_abbrev_length = 0); +}; + +} // namespace ledger + +#endif // _FORMAT_H diff --git a/src/generate.cc b/src/generate.cc new file mode 100644 index 00000000..95cdfa2e --- /dev/null +++ b/src/generate.cc @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "generate.h" +#include "session.h" + +namespace ledger { + +generate_posts_iterator::generate_posts_iterator + (session_t& _session, + unsigned int _seed, + std::size_t _quantity) + : session(_session), seed(_seed), quantity(_quantity), + + rnd_gen(seed == 0 ? static_cast(std::time(0)) : seed), + + year_range(1900, 2300), year_gen(rnd_gen, year_range), + mon_range(1, 12), mon_gen(rnd_gen, mon_range), + day_range(1, 28), day_gen(rnd_gen, day_range), + + upchar_range('A', 'Z'), upchar_gen(rnd_gen, upchar_range), + downchar_range('a', 'z'), downchar_gen(rnd_gen, downchar_range), + numchar_range('0', '9'), numchar_gen(rnd_gen, numchar_range), + + truth_range(0, 1), truth_gen(rnd_gen, truth_range), + three_range(1, 3), three_gen(rnd_gen, three_range), + six_range(1, 6), six_gen(rnd_gen, six_range), + two_six_range(2, 6), two_six_gen(rnd_gen, two_six_range), + strlen_range(1, 40), strlen_gen(rnd_gen, strlen_range), + + neg_number_range(-10000, -1), neg_number_gen(rnd_gen, neg_number_range), + pos_number_range(1, 10000), pos_number_gen(rnd_gen, pos_number_range) +{ + std::ostringstream next_date_buf; + generate_date(next_date_buf); + next_date = parse_date(next_date_buf.str()); + + std::ostringstream next_aux_date_buf; + generate_date(next_aux_date_buf); + next_aux_date = parse_date(next_aux_date_buf.str()); + + TRACE_CTOR(generate_posts_iterator, "bool"); +} + +void generate_posts_iterator::generate_string(std::ostream& out, int len, + bool only_alpha) +{ + DEBUG("generate.post.string", + "Generating string of length " << len << ", only alpha " << only_alpha); + + int last = -1; + bool first = true; + for (int i = 0; i < len; i++) { + int next = only_alpha ? 3 : three_gen(); + bool output = true; + switch (next) { + case 1: // colon + if (! first && last == 3 && strlen_gen() % 10 == 0 && i + 1 != len) + out << ':'; + else { + i--; + output = false; + } + break; + case 2: // space + if (! first && last == 3 && strlen_gen() % 20 == 0 && i + 1 != len) + out << ' '; + else { + i--; + output = false; + } + break; + case 3: // character + switch (three_gen()) { + case 1: // uppercase + out << char(upchar_gen()); + break; + case 2: // lowercase + out << char(downchar_gen()); + break; + case 3: // number + if (! only_alpha && ! first) + out << char(numchar_gen()); + else { + i--; + output = false; + } + break; + } + break; + } + if (output) { + last = next; + first = false; + } + } +} + +bool generate_posts_iterator::generate_account(std::ostream& out, + bool no_virtual) +{ + bool must_balance = true; + bool is_virtual = false; + + if (! no_virtual) { + switch (three_gen()) { + case 1: + out << '['; + is_virtual = true; + break; + case 2: + out << '('; + must_balance = false; + is_virtual = true; + break; + case 3: + break; + } + } + + generate_string(out, strlen_gen()); + + if (is_virtual) { + if (must_balance) + out << ']'; + else + out << ')'; + } + + return must_balance; +} + +void generate_posts_iterator::generate_commodity(std::ostream& out, + const string& exclude) +{ + string comm; + do { + std::ostringstream buf; + generate_string(buf, six_gen(), true); + comm = buf.str(); + } + while (comm == exclude || comm == "h" || comm == "m" || comm == "s" || + comm == "and" || comm == "any" || comm == "all" || comm == "div" || + comm == "false" || comm == "or" || comm == "not" || + comm == "true" || comm == "if" || comm == "else"); + + out << comm; +} + +string generate_posts_iterator::generate_amount(std::ostream& out, + value_t not_this_amount, + bool no_negative, + const string& exclude) +{ + std::ostringstream buf; + + if (truth_gen()) { // commodity goes in front + generate_commodity(buf, exclude); + if (truth_gen()) + buf << ' '; + if (no_negative || truth_gen()) + buf << pos_number_gen(); + else + buf << neg_number_gen(); + } else { + if (no_negative || truth_gen()) + buf << pos_number_gen(); + else + buf << neg_number_gen(); + if (truth_gen()) + buf << ' '; + generate_commodity(buf, exclude); + } + + // Possibly generate an annotized commodity, but make it rarer + if (! no_negative && three_gen() == 1) { + if (three_gen() == 1) { + buf << " {"; + generate_amount(buf, value_t(), true); + buf << '}'; + } + if (six_gen() == 1) { + buf << " ["; + generate_date(buf); + buf << ']'; + } + if (six_gen() == 1) { + buf << " ("; + generate_string(buf, six_gen()); + buf << ')'; + } + } + + if (! not_this_amount.is_null() && + value_t(buf.str()).as_amount().commodity() == + not_this_amount.as_amount().commodity()) + return ""; + + out << buf.str(); + + return buf.str(); +} + +bool generate_posts_iterator::generate_post(std::ostream& out, bool no_amount) +{ + out << " "; + bool must_balance = generate_account(out, no_amount); + out << " "; + + if (! no_amount) { + value_t amount(generate_amount(out)); + if (truth_gen()) + generate_cost(out, amount); + } + if (truth_gen()) + generate_note(out); + out << '\n'; + + return must_balance; +} + +void generate_posts_iterator::generate_cost(std::ostream& out, value_t amount) +{ + std::ostringstream buf; + + if (truth_gen()) + buf << " @ "; + else + buf << " @@ "; + + if (! generate_amount(buf, amount, true, + amount.as_amount().commodity().symbol()).empty()) + out << buf.str(); +} + +void generate_posts_iterator::generate_date(std::ostream& out) +{ + out.width(4); + out.fill('0'); + out << year_gen(); + + out.width(1); + out << '/'; + + out.width(2); + out.fill('0'); + out << mon_gen(); + + out.width(1); + out << '/'; + + out.width(2); + out.fill('0'); + out << day_gen(); +} + +void generate_posts_iterator::generate_state(std::ostream& out) +{ + switch (three_gen()) { + case 1: + out << "* "; + break; + case 2: + out << "! "; + break; + case 3: + out << ""; + break; + } +} + +void generate_posts_iterator::generate_code(std::ostream& out) +{ + out << '('; + generate_string(out, six_gen()); + out << ") "; +} + +void generate_posts_iterator::generate_payee(std::ostream& out) +{ + generate_string(out, strlen_gen()); +} + +void generate_posts_iterator::generate_note(std::ostream& out) +{ + out << "\n ; "; + generate_string(out, strlen_gen()); +} + +void generate_posts_iterator::generate_xact(std::ostream& out) +{ + out << format_date(next_date, FMT_WRITTEN); + next_date += gregorian::days(six_gen()); + if (truth_gen()) { + out << '='; + out << format_date(next_aux_date, FMT_WRITTEN); + next_aux_date += gregorian::days(six_gen()); + } + out << ' '; + + generate_state(out); + generate_code(out); + generate_payee(out); + if (truth_gen()) + generate_note(out); + out << '\n'; + + int count = three_gen() * 2; + bool has_must_balance = false; + for (int i = 0; i < count; i++) { + if (generate_post(out)) + has_must_balance = true; + } + if (has_must_balance) + generate_post(out, true); + + out << '\n'; +} + +void generate_posts_iterator::increment() +{ + post_t * post = *posts++; + + if (post == NULL && quantity > 0) { + std::ostringstream buf; + generate_xact(buf); + + DEBUG("generate.post", "The post we intend to parse:\n" << buf.str()); + + try { + shared_ptr in(new std::istringstream(buf.str())); + + parse_context_stack_t parsing_context; + parsing_context.push(in); + parsing_context.get_current().journal = session.journal.get(); + parsing_context.get_current().scope = &session; + + if (session.journal->read(parsing_context) != 0) { + VERIFY(session.journal->xacts.back()->valid()); + posts.reset(*session.journal->xacts.back()); + post = *posts++; + } + } + catch (std::exception&) { + add_error_context(_f("While parsing generated transaction (seed %1%):") + % seed); + add_error_context(buf.str()); + throw; + } + catch (int) { + add_error_context(_f("While parsing generated transaction (seed %1%):") + % seed); + add_error_context(buf.str()); + throw; + } + + quantity--; + } + + m_node = post; +} + +} // namespace ledger diff --git a/src/generate.h b/src/generate.h new file mode 100644 index 00000000..5272c7db --- /dev/null +++ b/src/generate.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +/** + * @addtogroup generate + */ + +/** + * @file generate.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _GENERATE_H +#define _GENERATE_H + +#include "iterators.h" + +namespace ledger { + +class session_t; + +class generate_posts_iterator + : public iterator_facade_base +{ + session_t& session; + unsigned int seed; + std::size_t quantity; + date_t next_date; + date_t next_aux_date; + + mt19937 rnd_gen; + + typedef variate_generator > int_generator_t; + typedef variate_generator > real_generator_t; + + uniform_int<> year_range; + int_generator_t year_gen; + uniform_int<> mon_range; + int_generator_t mon_gen; + uniform_int<> day_range; + int_generator_t day_gen; + + uniform_int<> upchar_range; + int_generator_t upchar_gen; + uniform_int<> downchar_range; + int_generator_t downchar_gen; + uniform_int<> numchar_range; + int_generator_t numchar_gen; + + uniform_int<> truth_range; + int_generator_t truth_gen; + uniform_int<> three_range; + int_generator_t three_gen; + uniform_int<> six_range; + int_generator_t six_gen; + uniform_int<> two_six_range; + int_generator_t two_six_gen; + + uniform_int<> strlen_range; + int_generator_t strlen_gen; + + uniform_real<> neg_number_range; + real_generator_t neg_number_gen; + uniform_real<> pos_number_range; + real_generator_t pos_number_gen; + + xact_posts_iterator posts; + +public: + generate_posts_iterator(session_t& _session, + unsigned int _seed = 0, + std::size_t _quantity = 100); + + virtual ~generate_posts_iterator() throw() { + TRACE_DTOR(generate_posts_iterator); + } + + virtual void increment(); + +protected: + void generate_string(std::ostream& out, int len, bool only_alpha = false); + bool generate_account(std::ostream& out, bool no_virtual = false); + void generate_commodity(std::ostream& out, const string& exclude = ""); + string generate_amount(std::ostream& out, + value_t not_this_amount = NULL_VALUE, + bool no_negative = false, + const string& exclude = ""); + bool generate_post(std::ostream& out, bool no_amount = false); + void generate_cost(std::ostream& out, value_t amount); + void generate_date(std::ostream& out); + void generate_state(std::ostream& out); + void generate_code(std::ostream& out); + void generate_payee(std::ostream& out); + void generate_note(std::ostream& out); + void generate_xact(std::ostream& out); +}; + +} // namespace ledger + +#endif // _GENERATE_H diff --git a/src/global.cc b/src/global.cc new file mode 100644 index 00000000..32d3cc5a --- /dev/null +++ b/src/global.cc @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2003-2018, John Wiegley. 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. + */ + +#include + +#include "global.h" +#if HAVE_BOOST_PYTHON +#include "pyinterp.h" +#else +#include "session.h" +#endif +#include "item.h" +#include "journal.h" +#include "pool.h" + +namespace ledger { + +static bool args_only = false; +std::string _init_file; + +global_scope_t::global_scope_t(char ** envp) +{ + epoch = CURRENT_TIME(); + +#if HAVE_BOOST_PYTHON + if (! python_session.get()) { + python_session.reset(new ledger::python_interpreter_t); + session_ptr = python_session; + } +#else + session_ptr.reset(new session_t); +#endif + + set_session_context(session_ptr.get()); + + // Create the report object, which maintains state relating to each + // command invocation. Because we're running from main(), the + // distinction between session and report doesn't really matter, but if + // a GUI were calling into Ledger it would have one session object per + // open document, with a separate report_t object for each report it + // generated. + report_stack.push_front(new report_t(*session_ptr)); + scope_t::default_scope = &report(); + scope_t::empty_scope = &empty_scope; + + // Read the user's options, in the following order: + // + // 1. environment variables (LEDGER_