diff options
author | John Wiegley <johnw@newartisans.com> | 2012-04-26 16:39:25 -0500 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2012-04-26 16:39:25 -0500 |
commit | 64a9b42381c26baf24e58b40f50f0b253e551811 (patch) | |
tree | 5447a29dff64c3a8b7be8100a01bcb4a2d73b0bb | |
parent | 7cc550fc22357e2ded194d3e65287c6b3317f5ae (diff) | |
parent | b4407c10c0071365322b2963747bf42a57fd7304 (diff) | |
download | fork-ledger-64a9b42381c26baf24e58b40f50f0b253e551811.tar.gz fork-ledger-64a9b42381c26baf24e58b40f50f0b253e551811.tar.bz2 fork-ledger-64a9b42381c26baf24e58b40f50f0b253e551811.zip |
Merge branch 'release/v3.0.0-20120426'
320 files changed, 15014 insertions, 4931 deletions
@@ -6,83 +6,83 @@ *.pyc *~ .timestamp -/*.tar.bz2 -/*.tar.gz -/.deps/ -/.libs/ -/ABOUT-NLS -/BaselineTests -/Doxyfile.gen -/Makefile -/Makefile.am -/Makefile.in -/PyUnitTests -/RegressionTests -/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 -/data_tests -/depcomp -/doc/*.aux -/doc/*.cp -/doc/*.fn -/doc/*.ky -/doc/*.log -/doc/*.pdf -/doc/*.pg -/doc/*.toc -/doc/*.tp -/doc/*.vr -/doc/.dirstamp -/doc/html/ -/doc/latex/ -/doc/ledger.info -/doc/refman.pdf -/doc/report/ -/elisp-comp -/expr_tests -/install-sh -/intl/ -/ledger -/libtool -/ltmain.sh -/m4/ -/make.sh -/math_tests -/missing -/mkinstalldirs -/po/ -/py-compile -/report_tests -/shave -/shave-libtool -/src/system.hh.gch -/stamp-h1 -/test/python/ -/texinfo.tex -/tmpcvs*/ -/tmpwrk*/ -/util_tests -/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/ -/dist/win/vc9/system.hh -/doc/ledger.1.html -/doc/ledger.html -/doc/ledger3.html +*.tar.bz2 +*.tar.gz +.deps/ +.libs/ +ABOUT-NLS +BaselineTests +Doxyfile.gen +Makefile +Makefile.am +Makefile.in +PyUnitTests +RegressionTests +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 +data_tests +depcomp +doc/*.aux +doc/*.cp +doc/*.fn +doc/*.ky +doc/*.log +doc/*.pdf +doc/*.pg +doc/*.toc +doc/*.tp +doc/*.vr +doc/.dirstamp +doc/html/ +doc/latex/ +doc/ledger.info +doc/refman.pdf +doc/report/ +elisp-comp +expr_tests +install-sh +intl/ +ledger +libtool +ltmain.sh +m4/ +make.sh +math_tests +missing +mkinstalldirs +po/ +py-compile +report_tests +shave +shave-libtool +src/system.hh.gch +stamp-h1 +texinfo.tex +tmpcvs*/ +tmpwrk*/ +util_tests +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/ +dist/win/vc9/system.hh +doc/ledger.1.html +doc/ledger.html +doc/ledger3.html +src/TAGS diff --git a/README.textile b/README.textile index b4192928..4a370fab 100644 --- a/README.textile +++ b/README.textile @@ -101,7 +101,7 @@ sudo port install ledger h3. Ubuntu -If you're going to be build on Ubuntu, @sudo apt-get install ...@ the +If you're going to build on Ubuntu, @sudo apt-get install ...@ the following packages (current as of Ubuntu Hardy): <pre> @@ -122,6 +122,24 @@ sudo apt-get install build-essential libtool autoconf automake \ libmpfr-dev </pre> +h3. Debian + +Debian squeeze (6.0): the version of boost in squeeze is too old +for ledger and unfortunately no backport is available at the moment. + +Debian wheezy (7.0) contains all components needed to build ledger. +You can install all required build dependencies using the following +command: + +<pre> +sudo apt-get install build-essential libtool autoconf automake \ + autopoint texinfo python-dev zlib1g-dev libbz2-dev \ + libgmp3-dev gettext libmpfr-dev libboost-date-time1.49-dev \ + libboost-filesystem1.49-dev libboost-graph1.49-dev \ + libboost-iostreams1.49-dev libboost-python1.49-dev \ + libboost-regex1.49-dev libboost-test1.49-dev +</pre> + h2. Building The next step is preparing your environment for building. While you can use @@ -36,7 +36,7 @@ LEVELS = {'DEBUG': logging.DEBUG, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL} -search_prefixes = [ '/opt/local', '/usr/local', '/sw', '/usr' ] +search_prefixes = [ '/usr/local', '/opt/local', '/sw', '/usr' ] class BoostInfo(object): log = None @@ -52,21 +52,6 @@ class BoostInfo(object): def configure(self, suffix = None, file_suffix = None, home_path = None, include_path = None, library_path = None): - path = library_path or self.library_path - if not isabs(path): - path = join(home_path or self.home_path, path) - if not exists(path) or not isdir(path) or \ - not self.check_for_boost_regex_lib(path, suffix or self.suffix, - file_suffix or self.file_suffix): - return False - - path = include_path or self.include_path - if not isabs(path): - path = join(home_path or self.home_path, path) - if not exists(path) or not isdir(path) or \ - not self.check_for_boost_regex_hpp(path): - return False - self.log.debug('Configuring Boost details') if suffix: self.log.debug('Setting Boost suffix to => ' + suffix) @@ -84,6 +69,21 @@ class BoostInfo(object): self.log.debug('Setting Boost lib directory to => ' + library_path) self.library_path = library_path + path = library_path or self.library_path + if not isabs(path): + path = join(home_path or self.home_path, path) + if not exists(path) or not isdir(path) or \ + not self.check_for_boost_regex_lib(path, suffix or self.suffix, + file_suffix or self.file_suffix): + return False + + path = include_path or self.include_path + if not isabs(path): + path = join(home_path or self.home_path, path) + if not exists(path) or not isdir(path) or \ + not self.check_for_boost_regex_hpp(path): + return False + self.configured = True return True # The object was configured @@ -254,8 +254,8 @@ class CommandLineApp(object): force_exit = True # If true, always ends run() with sys.exit() log_handler = None darwin_gcc = False - boost_version = "1_48_0" - boost_major = "1_48" + boost_version = "1_49_0" + boost_major = "1_49" options = { 'debug': False, @@ -508,6 +508,12 @@ class PrepareBuild(CommandLineApp): op.add_option('', '--no-python', action='store_true', dest='no_python', default=False, help='Do not enable Python support by default') + op.add_option('', '--cache', action='store_true', + dest='enable_cache', default=False, + help='Enable use of Boost.Serialization (--cache)') + 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('', '--enable-cache', action='store_true', dest='enable_cache', default=False, help='Enable use of Boost.Serialization (--cache)') @@ -526,9 +532,12 @@ class PrepareBuild(CommandLineApp): op.add_option('', '--gcc47', action='store_true', dest='gcc47', default=False, help='Require the use of gcc 4.7') - op.add_option('', '--cpp0x', action='store_true', - dest='cpp0x', default=False, - help='Use C++0x extensions (requires Clang or gcc 4.6/7)') + op.add_option('', '--gcc48', action='store_true', + dest='gcc48', default=False, + help='Require the use of gcc 4.8') + op.add_option('', '--cpp11', action='store_true', + dest='use_cpp11', default=False, + help='Use C++11 extensions (requires Clang or gcc 4.6/7/8)') op.add_option('', '--output', metavar='DIR', action="callback", callback=self.option_output, help='Build in the specified directory') @@ -555,11 +564,6 @@ class PrepareBuild(CommandLineApp): help='Enable full warning flags') def main(self, *args): - if self.options.python: - self.configure_args.append('--enable-python') - if self.options.no_python: - self.configure_args.remove('--enable-python') - if args and args[0] in ['default', 'debug', 'opt', 'gcov', 'gprof']: self.current_flavor = args[0] args = args[1:] @@ -1012,10 +1016,24 @@ class PrepareBuild(CommandLineApp): self.envvars['PYTHON_HOME'] = '/opt/local' if self.options.use_clang: - self.boost_inc_ident = "clang" - self.boost_lib_ident = "clang-darwin28" + self.log.info('Setting up for using Clang') - self.log.debug("Setting Python home to /opt/local") + self.boost_inc_ident = "clang31" + self.boost_lib_ident = "clang-darwin" + + if self.options.use_cpp11: + self.CXXFLAGS.append('-std=c++11') + self.CXXFLAGS.append('-stdlib=libc++') + self.CXXFLAGS.append('-nostdlibinc') + self.CXXFLAGS.append('-isystem /usr/local/include') + self.CXXFLAGS.append('-isystem /usr/local/include/c++/v1') + self.CXXFLAGS.append('-isystem /usr/include') + + self.LDFLAGS.append('-stdlib=libc++') + self.LDFLAGS.append('/usr/local/lib/libc++.dylib') + else: + global search_prefixes + search_prefixes = [ '/opt/local', '/usr/local', '/sw', '/usr' ] self.log.debug('Using Clang ident: %s/%s' % (self.boost_inc_ident, self.boost_lib_ident)) @@ -1032,20 +1050,15 @@ class PrepareBuild(CommandLineApp): (self.boost_inc_ident, self.boost_lib_ident)) if self.current_flavor == 'debug': - if exists('/usr/local/stow/icu-%s/include' % self.boost_inc_ident): - self.sys_include_dirs.insert( - 0, '/usr/local/stow/icu-%s/include' % self.boost_inc_ident) - self.sys_library_dirs.insert( - 0, '/usr/local/stow/icu-%s/lib' % self.boost_inc_ident) - self.configure_args.append('--disable-shared') - elif self.current_flavor == 'gcov': self.configure_args.append('--disable-shared') else: self.CXXFLAGS.append('-march=nocona') self.CXXFLAGS.append('-msse3') + self.CXXFLAGS.append('-DDOCUMENT_MODEL=1') + self.locate_darwin_libraries() def setup_for_system(self): @@ -1071,14 +1084,14 @@ class PrepareBuild(CommandLineApp): elif system == 'Darwin': if self.options.use_clang: - if exists('/opt/local/bin/clang++-mp-3.1'): - self.envvars['CC'] = '/opt/local/bin/clang-mp-3.1' - self.envvars['CXX'] = '/opt/local/bin/clang++-mp-3.1' - self.envvars['LD'] = '/opt/local/bin/clang++-mp-3.1' - elif exists('/usr/local/bin/clang++'): + if exists('/usr/local/bin/clang++'): self.envvars['CC'] = '/usr/local/bin/clang' self.envvars['CXX'] = '/usr/local/bin/clang++' self.envvars['LD'] = '/usr/local/bin/clang++' + elif exists('/opt/local/bin/clang++-mp-3.1'): + self.envvars['CC'] = '/opt/local/bin/clang-mp-3.1' + self.envvars['CXX'] = '/opt/local/bin/clang++-mp-3.1' + self.envvars['LD'] = '/opt/local/bin/clang++-mp-3.1' else: self.envvars['CC'] = 'clang' self.envvars['CXX'] = 'clang++' @@ -1088,11 +1101,17 @@ class PrepareBuild(CommandLineApp): not self.options.gcc45 and \ not self.options.gcc46 and \ not self.options.gcc47 and \ + not self.options.gcc48 and \ exists('/usr/bin/g++-4.2'): self.envvars['CC'] = '/usr/bin/gcc-4.2' self.envvars['CXX'] = '/usr/bin/g++-4.2' self.envvars['LD'] = '/usr/bin/g++-4.2' self.darwin_gcc = True + elif exists('/usr/local/bin/g++-mp-4.8') and \ + self.options.gcc47: + self.envvars['CC'] = '/usr/local/bin/gcc-mp-4.8' + self.envvars['CXX'] = '/usr/local/bin/g++-mp-4.8' + self.envvars['LD'] = '/usr/local/bin/g++-mp-4.8' elif exists('/usr/local/bin/g++-mp-4.7') and \ self.options.gcc47: self.envvars['CC'] = '/usr/local/bin/gcc-mp-4.7' @@ -1126,9 +1145,10 @@ class PrepareBuild(CommandLineApp): self.configure_args.append('--enable-doxygen') if self.options.enable_cache: self.configure_args.append('--enable-cache') - - if self.options.cpp0x: - self.CXXFLAGS.append('-std=c++0x') + if self.options.python: + self.configure_args.append('--enable-python') + if self.options.no_python: + self.configure_args.remove('--enable-python') if exists('/Users/johnw/Projects/ledger/plan/TODO'): self.setup_for_johnw() @@ -1211,6 +1231,7 @@ class PrepareBuild(CommandLineApp): self.CXXFLAGS.append('-Wno-missing-prototypes') self.CXXFLAGS.append('-Wno-missing-noreturn') self.CXXFLAGS.append('-Wno-disabled-macro-expansion') + self.CXXFLAGS.append('-Wno-unused-parameter') self.CXXFLAGS.append('-fno-limit-debug-info') #self.CXXFLAGS.append('-Wold-style-cast') @@ -1306,15 +1327,8 @@ class PrepareBuild(CommandLineApp): def locate_darwin_libraries(self): if self.current_flavor == 'debug': - if self.boost_info.configure( - home_path = '/usr/local/stow/boost_%s-%s' % \ - (self.boost_version, self.boost_inc_ident), - suffix = '-%s-sd-%s' % \ - (self.boost_lib_ident, self.boost_major), - file_suffix = '.dylib', - include_path = 'include/boost-%s' % self.boost_major): - pass - elif self.boost_info.configure( + if (not self.options.use_clang or self.options.use_cpp11) and \ + self.boost_info.configure( home_path = '/usr/local/stow/boost_%s-%s' % \ (self.boost_version, self.boost_inc_ident), suffix = '-%s-d-%s' % \ @@ -1328,15 +1342,8 @@ class PrepareBuild(CommandLineApp): else: if self.boost_info.configure(): pass - elif self.boost_info.configure( - home_path = '/usr/local/stow/boost_%s-%s' % \ - (self.boost_version, self.boost_inc_ident), - suffix = '-%s-s-%s' % \ - (self.boost_lib_ident, self.boost_major), - file_suffix = '.dylib', - include_path = 'include/boost-%s' % self.boost_major): - pass - elif self.boost_info.configure( + elif (not self.options.use_clang or self.options.use_cpp11) and \ + self.boost_info.configure( home_path = '/usr/local/stow/boost_%s-%s' % \ (self.boost_version, self.boost_inc_ident), suffix = '-%s-%s' % \ @@ -1352,7 +1359,8 @@ class PrepareBuild(CommandLineApp): def setup_flavor_debug(self): self.configure_args.append('--enable-debug') - if self.options.gcc45 or self.options.gcc46 or self.options.gcc47: + if self.options.gcc45 or self.options.gcc46 or \ + self.options.gcc47 or self.options.gcc48: self.CXXFLAGS.append('-g2') self.CXXFLAGS.append('-ggdb') self.LDFLAGS.append('-g2') @@ -1363,7 +1371,7 @@ class PrepareBuild(CommandLineApp): def setup_flavor_opt(self): self.CPPFLAGS.append('-DNDEBUG=1') - for i in ['-O3', '-fomit-frame-pointer']: + for i in ['-O3']: self.CXXFLAGS.append(i) self.CFLAGS.append(i) self.LDFLAGS.append(i) @@ -1528,6 +1536,8 @@ class PrepareBuild(CommandLineApp): make_args.append('-j%d' % self.options.jobs) make_args.append('JOBS=%d' % self.options.jobs) + make_args.append('FLAVOR=%s' % self.current_flavor) + self.log.debug('Configure arguments => ' + str(config_args)) self.log.debug('Makefile arguments => ' + str(make_args)) @@ -1648,7 +1658,11 @@ class PrepareBuild(CommandLineApp): isdir(self.build_directory()): self.log.info('=== Wiping build directory %s ===' % self.build_directory()) - shutil.rmtree(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_distcheck(self, *args): self.log.info('Executing phase: distcheck') @@ -1678,8 +1692,19 @@ class PrepareBuild(CommandLineApp): def phase_rsync(self, *args): self.log.info('Executing phase: rsync') - source_copy_dir = join(self.ensure(self.products_directory()), - 'ledger-proof') + proof_dir = 'ledger-proof' + + if self.options.python: + proof_dir += "-python" + + if self.options.gcc47: + proof_dir += "-gcc47" + elif self.options.gcc48: + proof_dir += "-gcc48" + elif self.options.use_clang: + proof_dir += "-clang" + + source_copy_dir = join(self.ensure(self.products_directory()), proof_dir) self.execute('rsync', '-a', '--delete', '--exclude=.git/', '--exclude=b/', @@ -1720,35 +1745,19 @@ class PrepareBuild(CommandLineApp): self.configure_flavor('opt', reset) - system_hh_gch = join(self.source_dir, 'src', 'system.hh.gch') - if exists(system_hh_gch): - os.remove(system_hh_gch) - self.log.info('=== Building opt ===') self.phase_make(*args) self.configure_flavor('gcov', reset) - system_hh_gch = join(self.source_dir, 'src', 'system.hh.gch') - if exists(system_hh_gch): - os.remove(system_hh_gch) - self.log.info('=== Building gcov ===') self.phase_make(*args) - system_hh_gch = join(self.source_dir, 'src', 'system.hh.gch') - if exists(system_hh_gch): - os.remove(system_hh_gch) - self.log.info('=== Building default ===') self.phase_make(*args) self.configure_flavor('debug', reset) - system_hh_gch = join(self.source_dir, 'src', 'system.hh.gch') - if exists(system_hh_gch): - os.remove(system_hh_gch) - self.log.info('=== Building debug ===') self.phase_make(*args) diff --git a/contrib/vim/ftplugin/ledger.vim b/contrib/vim/ftplugin/ledger.vim index 63de88b5..0067f9f8 100644 --- a/contrib/vim/ftplugin/ledger.vim +++ b/contrib/vim/ftplugin/ledger.vim @@ -11,7 +11,7 @@ let b:did_ftplugin = 1 let b:undo_ftplugin = "setlocal ". \ "foldmethod< foldtext< ". - \ "include< comments< omnifunc< formatprg<" + \ "include< comments< commentstring< omnifunc< formatprg<" " don't fill fold lines --> cleaner look setl fillchars="fold: " @@ -19,6 +19,7 @@ setl foldtext=LedgerFoldText() setl foldmethod=syntax setl include=^!include setl comments=b:; +setl commentstring=;%s setl omnifunc=LedgerComplete " set location of ledger binary for checking and auto-formatting diff --git a/contrib/vim/indent/ledger.vim b/contrib/vim/indent/ledger.vim new file mode 100644 index 00000000..ce5d508d --- /dev/null +++ b/contrib/vim/indent/ledger.vim @@ -0,0 +1,46 @@ +" Vim filetype indent file +" filetype: ledger +" by Johann Klähn; Use according to the terms of the GPL>=2. +" vim:ts=2:sw=2:sts=2:foldmethod=marker + +if exists("b:did_indent") + finish +endif +let b:did_indent = 1 + +setl autoindent +setl indentexpr=GetLedgerIndent() + +if exists("*GetLedgerIndent") + finish +endif + +function GetLedgerIndent(...) + " You can pass in a line number when calling this function manually. + let lnum = a:0 > 0 ? a:1 : v:lnum + " If this line is empty look at (the indentation of) the last line. + " Note that inside of a transaction no blank lines are allowed. + let line = getline(lnum) + let prev = getline(lnum - 1) + + if line =~ '^\s\+\S' + " Lines that already are indented (→postings, sub-directives) keep their indentation. + return &sw + elseif line =~ '^\s*$' + " Current line is empty, try to guess its type based on the previous line. + if prev =~ '^\([[:digit:]~=]\|\s\+\S\)' + " This is very likely a posting or a sub-directive. + " While lines following the start of a transaction are automatically + " indented you will have to indent the first line following a + " pre-declaration manually. This makes it easier to type long lists of + " 'account' pre-declarations without sub-directives, for example. + return &sw + else + return 0 + endif + else + " Everything else is not indented: + " start of transactions, pre-declarations, apply/end-lines + return 0 + endif +endf diff --git a/contrib/vim/syntax/ledger.vim b/contrib/vim/syntax/ledger.vim index 73aaa0c3..c516bab0 100644 --- a/contrib/vim/syntax/ledger.vim +++ b/contrib/vim/syntax/ledger.vim @@ -17,15 +17,24 @@ syntax clear " DATE[=EDATE] [*|!] [(CODE)] DESC <-- first line of transaction " ACCOUNT AMOUNT [; NOTE] <-- posting -syn region ledgerTransaction start=/^[[:digit:]~]/ skip=/^\s/ end=/^/ - \ fold keepend transparent contains=ledgerTransactionDate,ledgerMetadata,ledgerPosting +syn region ledgerTransaction start=/^[[:digit:]~=]/ skip=/^\s/ end=/^/ + \ fold keepend transparent + \ contains=ledgerTransactionDate,ledgerMetadata,ledgerPosting,ledgerTransactionExpression syn match ledgerTransactionDate /^\d\S\+/ contained +syn match ledgerTransactionExpression /^[=~]\s\+\zs.*/ contained syn match ledgerPosting /^\s\+[^[:blank:];][^;]*\ze\%($\|;\)/ \ contained transparent contains=ledgerAccount,ledgerMetadata " every space in an account name shall be surrounded by two non-spaces " every account name ends with a tab, two spaces or the end of the line syn match ledgerAccount /^\s\+\zs\%(\S\@<= \S\|\S\)\+\ze\%( \|\t\|\s*$\)/ contained +syn region ledgerPreDeclaration start=/^\(account\|payee\|commodity\|tag\)/ skip=/^\s/ end=/^/ + \ keepend transparent + \ contains=ledgerPreDeclarationType,ledgerPreDeclarationName,ledgerPreDeclarationDirective +syn match ledgerPreDeclarationType /^\(account\|payee\|commodity\|tag\)/ contained +syn match ledgerPreDeclarationName /^\S\+\s\+\zs.*/ contained +syn match ledgerPreDeclarationDirective /^\s\+\zs\S\+/ contained + syn match ledgerComment /^;.*$/ " comments at eol must be preceeded by at least 2 spaces / 1 tab syn region ledgerMetadata start=/\%( \|\t\|^\s\+\);/ skip=/^\s\+;/ end=/^/ @@ -34,22 +43,27 @@ syn match ledgerTag /:[^:]\+:/hs=s+1,he=e-1 contained syn match ledgerTag /\%(\%(;\|^tag\)[^:]\+\)\@<=[^:]\+:\ze[^:]\+$/ contained syn match ledgerTypedTag /\%(\%(;\|^tag\)[^:]\+\)\@<=[^:]\+::\ze[^:]\+$/ contained -syn region ledgerTagStack - \ matchgroup=ledgerTagPush start=/^tag\>/ - \ matchgroup=ledgerTagPop end=/^pop\>/ - \ contains=ledgerTagHead,ledgerTagStack,ledgerTransaction,ledgerComment -syn match ledgerTagHead /\%(^tag\s\+\)\@<=\S.*$/ contained contains=ledgerTag transparent +syn region ledgerApply + \ matchgroup=ledgerStartApply start=/^apply\>/ + \ matchgroup=ledgerEndApply end=/^end\s\+apply\>/ + \ contains=ledgerApplyHead,ledgerApply,ledgerTransaction,ledgerComment +syn match ledgerApplyHead /\%(^apply\s\+\)\@<=\S.*$/ contained highlight default link ledgerTransactionDate Constant +highlight default link ledgerTransactionExpression Statement highlight default link ledgerMetadata Tag highlight default link ledgerTypedTag Keyword highlight default link ledgerTag Type -highlight default link ledgerTagPop Tag -highlight default link ledgerTagPush Tag +highlight default link ledgerStartApply Tag +highlight default link ledgerEndApply Tag +highlight default link ledgerApplyHead Type highlight default link ledgerAccount Identifier +highlight default link ledgerPreDeclarationType Type +highlight default link ledgerPreDeclarationName Identifier +highlight default link ledgerPreDeclarationDirective Type " syncinc is easy: search for the first transaction. syn sync clear -syn sync match ledgerSync grouphere ledgerTransaction "^[[:digit:]~]" +syn sync match ledgerSync grouphere ledgerTransaction "^[[:digit:]~=]" let b:current_syntax = "ledger" diff --git a/doc/Doxyfile b/doc/Doxyfile index 33750741..d59d3f82 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NUMBER = 3.0 # 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 = doc +OUTPUT_DIRECTORY = %builddir%/doc # 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 @@ -1415,7 +1415,7 @@ DIRECTORY_GRAPH = YES # generated by dot. Possible values are png, jpg, or gif # If left blank png will be used. -DOT_IMAGE_FORMAT = png +DOT_IMAGE_FORMAT = jpg # 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. diff --git a/doc/ledger.1 b/doc/ledger.1 index 5a3bd6db..b96e4a7d 100644 --- a/doc/ledger.1 +++ b/doc/ledger.1 @@ -1,4 +1,4 @@ -.Dd June 22, 2010 +.Dd March 23, 2012 .Dt ledger 1 .Sh NAME .Nm ledger @@ -32,6 +32,11 @@ or command loop, allowing several commands to be executed against the same dataset without reparsing. .Pp The following is a complete list of reporting commands accepted by Ledger: +.Bl -tag -width accounts +.It Nm accounts Oo Ar report-query Oc +Lists all accounts for postings matching the +.Ar report-query . +.El .Pp .Bl -tag -width balance .It Nm balance Oo Ar report-query Oc @@ -76,6 +81,12 @@ 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 Nm commodities Oo Ar report-query Oc +Lists all commodities for postings matching the +.Ar report-query . +.It Nm convert +Reads data from a CSV (comma-separated values) file and generates Ledger +transactions. .It Nm csv Oo Ar report-query Oc Report of postings matching the .Ar report-query @@ -112,11 +123,19 @@ 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 transactions to carry forward those balances. +.It Nm org +Produces a journal file suitable for use in the Emacs org mode. +.It Nm payees Oo Ar report-query Oc +Lists all payees for postings matching the +.Ar report-query . +.It Nm pricemap +Produces a file which can be used to generate a graph with graphviz showing +the relationship of commodities in the Ledger file. .It Nm prices Oo Ar report-query Oc Reports prices for all commodities in postings matching the .Ar report-query . The prices are reported with the granularity of a single day. -.It Nm pricesdb Oo Ar report-query Oc +.It Nm pricedb Oo Ar report-query Oc Reports prices for all commodities in postings matching the .Ar report-query . Prices are reported down to the second, using the same format as the @@ -163,6 +182,7 @@ Show any gains (or losses) in commodity values over time. Only show the top .Ar number postings. +.It Fl \-historical Pq Fl H .It Fl \-invert Invert the value of amounts shown. .It Fl \-market Pq Fl V @@ -230,6 +250,13 @@ This command requires that Python support be active. If so, it starts up an 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 Nm select Oo Ar sql-query Oc +List all postings matching the +.Ar sql-query . +This command allows to generate SQL-like queries. +.It Nm source +Parses a journal file and checks it for errors. Ledger will return success +if no errors are found. .It Nm stats Oo Ar report-query Oc Provides summary information about all the postings matching .Ar report-query . @@ -263,13 +290,14 @@ transactions they are contained in. See the manual for more information. .It Fl \-account Ar STR .It Fl \-account-width Ar INT .It Fl \-actual Pq Fl L -.It Fl \-actual-dates .It Fl \-add-budget .It Fl \-amount Ar EXPR Pq Fl t .It Fl \-amount-data Pq Fl j .It Fl \-amount-width Ar INT .It Fl \-anon .It Fl \-args-only +.It Fl \-auto-match +.It Fl \-aux-date .It Fl \-average Pq Fl A .It Fl \-balance-format Ar FMT .It Fl \-base @@ -280,6 +308,7 @@ transactions they are contained in. See the manual for more information. .It Fl \-budget-format Ar FMT .It Fl \-by-payee Pq Fl P .It Fl \-cache Ar FILE +.It Fl \-check-payees .It Fl \-cleared Pq Fl C .It Fl \-cleared-format Ar FMT .It Fl \-collapse Pq Fl n @@ -297,6 +326,8 @@ See .It Fl \-date-format Ar DATEFMT Pq Fl y .It Fl \-datetime-format Ar FMT .It Fl \-date-width Ar INT +.It Fl \-day-break +.It Fl \-dc .It Fl \-debug Ar STR .It Fl \-decimal-comma .It Fl \-depth Ar INT @@ -306,12 +337,12 @@ See .It Fl \-display-total Ar EXPR .It Fl \-dow .It Fl \-download -.It Fl \-effective .It Fl \-empty Pq Fl E .It Fl \-end Pq Fl e .It Fl \-equity .It Fl \-exact .It Fl \-exchange Ar COMM Oo , COMM, ... Oc Pq Fl X +.It Fl \-explicit .It Fl \-file Ar FILE .It Fl \-first Ar INT See @@ -335,6 +366,7 @@ See .It Fl \-help-calc .It Fl \-help-comm .It Fl \-help-disp +.It Fl \-immediate .It Fl \-import Ar STR .It Fl \-init-file Ar FILE .It Fl \-inject Ar STR @@ -346,8 +378,8 @@ See .It Fl \-leeway Ar INT Pq Fl Z .It Fl \-limit Ar EXPR Pq Fl l .It Fl \-lot-dates +.It Fl \-lot-notes .It Fl \-lot-prices -.It Fl \-lot-tags .It Fl \-lots .It Fl \-lots-actual .It Fl \-market Pq Fl V @@ -356,6 +388,7 @@ See .It Fl \-meta-width Ar INT .It Fl \-monthly Pq Fl M .It Fl \-no-color +.It Fl \-no-pager .It Fl \-no-rounding .It Fl \-no-titles .It Fl \-no-total @@ -366,10 +399,12 @@ See .It Fl \-pager Ar STR .It Fl \-payee .It Fl \-payee-width Ar INT +.It Fl \-pedantic .It Fl \-pending .It Fl \-percent Pq Fl \% .It Fl \-period Ar PERIOD Pq Fl p .It Fl \-period-sort +.It Fl \-permissive .It Fl \-pivot Ar STR .It Fl \-plot-amount-format Ar FMT .It Fl \-plot-total-format Ar FMT @@ -382,6 +417,7 @@ See .Fl \-leeway . .It Fl \-prices-format Ar FMT .It Fl \-pricedb-format Ar FMT +.It Fl \-primary-date .It Fl \-quantity Pq Fl O .It Fl \-quarterly .It Fl \-raw @@ -396,6 +432,7 @@ appeared in the original journal file. .It Fl \-revalued .It Fl \-revalued-only .It Fl \-revalued-total Ar EXPR +.It Fl \-rich-data .It Fl \-seed Ar INT .It Fl \-script .It Fl \-sort Ar EXPR Pq Fl S @@ -405,6 +442,7 @@ appeared in the original journal file. .It Fl \-strict .It Fl \-subtotal Pq Fl s .It Fl \-tail Ar INT +.It Fl \-time-report .It Fl \-total Ar EXPR .It Fl \-total-data Pq Fl J .It Fl \-total-width Ar INT @@ -416,8 +454,10 @@ appeared in the original journal file. .It Fl \-unrealized-gains .It Fl \-unrealized-losses .It Fl \-unround +.It Fl \-value-expr Ar EXPR .It Fl \-verbose .It Fl \-verify +.It Fl \-verify-memory .It Fl \-version .It Fl \-weekly Pq Fl W .It Fl \-wide Pq Fl w diff --git a/doc/ledger3.texi b/doc/ledger3.texi index 9d709748..30d7d5a4 100644 --- a/doc/ledger3.texi +++ b/doc/ledger3.texi @@ -45,7 +45,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @titlepage @title Ledger: Command-Line Accounting @subtitle For Version 3.0 of Ledger -@subtitle Draft Manual Time-stamp: <2011-12-16 21:23 (cpearls)> @author John Wiegley @end titlepage @@ -71,13 +70,14 @@ twinkling in their father's CRT. * Ledger Tutorial :: * Principles of Accounting:: * Keeping a Journal:: +* Transactions :: * Building Reports:: * Reporting Commands:: * Command-line Syntax:: * Budgeting and Forecasting:: * Value Expressions:: * Format Strings:: -* Journal File Format:: +* Ledger for Developers:: * Extending with Python:: * Major Changes from version 2.6:: * Example Data File:: @@ -114,9 +114,11 @@ 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. If you use -another GUI accounting program like GNUCash, the vast majority of its -functionality is geared towards helping you keep a journal. +so we'll describe double-entry accounting in terms of that. + +@c If you use +@c another GUI accounting program like GNUCash, the vast majority of its +@c functionality is geared towards helping you keep a journal. A checkbook journal records debits (subtractions, or withdrawals) and credits (additions, or deposits) with reference to a single account: @@ -261,14 +263,6 @@ can be searched directly from the command line using the following options: @option{ledger --help} bring up this entire manual in your tty. -@option{ledger --help-info} brings up help on how to use the info system. - -@option{ledger --help-comm concept} searches the manual index and brings up pages associated with `concept'. - -@option{ledger --help-calc} brings up the value expressions chapter of this manual (@pxref{Value Expressions}). - -@option{ledger --help-disp} brings up the Format Strings chapter of this manual (@pxref{Format Strings}). - If you need help on how to use Ledger, or run into problems, you can join the Ledger mailing list at the following Web address: @@ -347,7 +341,8 @@ Ledger will generate: $ -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. +@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: @@ -494,6 +489,7 @@ cannot display any currency symbols other than dollar signs ($). * Reporting Commands Quick Reference:: * Basic Options Quick Reference:: * Report Filtering Quick Reference:: +* Error Checking and Calculation Options:: * Output Customization Quick Reference:: * Grouping Options:: * Commodity Reporting Quick Reference:: @@ -528,7 +524,7 @@ cannot display any currency symbols other than dollar signs ($). @item @code{-a NAME} @tab @code{--account NAME} @tab specify default account name for QIF file postings @end multitable -@node Report Filtering Quick Reference, Output Customization Quick Reference, Basic Options Quick Reference, Command Line Quick Reference +@node Report Filtering Quick Reference, Error Checking and Calculation Options, Basic Options Quick Reference, Command Line Quick Reference @subsection Report Filtering @multitable @columnfractions .1 .25 .65 @item @strong{Short} @tab @strong{Long} @tab @strong{Description} @@ -538,6 +534,7 @@ cannot display any currency symbols other than dollar signs ($). @item @code{-p STR} @tab @code{--period} @tab Set report period to STR @item @code{ } @tab @code{--period-sort} @tab Sort postings within each period @item @code{-C} @tab @code{--cleared} @tab Display only cleared postings +@item @code{} @tab @code{--dc} @tab Display register or balance in debit/credit format @item @code{-U} @tab @code{--uncleared} @tab Display only uncleared postings @item @code{-R} @tab @code{--real} @tab Display only real postings @item @code{-L} @tab @code{--actual} @tab Displays only actual postings, not automated @@ -551,14 +548,25 @@ cannot display any currency symbols other than dollar signs ($). @item @code{-T EXPR} @tab @code{--total} @tab Change the value expression used for ``totals'' column in register and balance reports @end multitable -@node Output Customization Quick Reference, Grouping Options, Report Filtering Quick Reference, Command Line Quick Reference +@node Error Checking and Calculation Options, Output Customization Quick Reference, Report Filtering Quick Reference, Command Line Quick Reference +@subsection Error Checking and Calculation Options + +@multitable @columnfractions .1 .25 .65 +@item @strong{Short} @tab @strong{Long} @tab @strong{Description} +@item @code{} @tab @code{--strict} @tab accounts, tags or commodities not previously declared will cause warnings +@item @code{} @tab @code{--pedantic} @tab accounts, tags or commodities not previously declared will cause errors +@item @code{} @tab @code{--check-payees} @tab enable strict and pedantic checking for payees as well as accounts, commodities and tags. +@item @code{} @tab @code{--immediate} @tab instructs ledger to evaluate calculations immediately rather than lazily +@end multitable + + +@node Output Customization Quick Reference, Grouping Options, Error Checking and Calculation Options, Command Line Quick Reference @subsection Output Customization @multitable @columnfractions .15 .4 .45 @item @strong{Short} @tab @strong{Long} @tab @strong{Description} @item @code{-n} @tab @code{--collapse} @tab Collapse transactions with multiple postings @item @code{-s} @tab @code{--subtotal} @tab Report register as a single subtotal @item @code{-P} @tab @code{--by-payee} @tab Report subtotals by payee -@item @code{-x} @tab @code{--comm-as-payee} @tab Change the payee of every posting to be the commodity used in that posting @item @code{-E} @tab @code{--empty} @tab Include empty accounts in report @item @code{-W} @tab @code{--weekly} @tab Report posting totals by week @item @code{-Y} @tab @code{--yearly} @tab Report posting totals by year @@ -639,7 +647,7 @@ cannot display any currency symbols other than dollar signs ($). 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 you +latter. With ledger you can handle your personal finances or your businesses. Double-entry accounting scales. @cindex double-entry accounting @@ -896,7 +904,7 @@ file: And two in your company ledger file: @smallexample -!account Company XYZ +apply account Company XYZ 2004/09/29 Circuit City Expenses:Computer:Software $100.00 @@ -906,10 +914,10 @@ And two in your company ledger file: Accounts Payable:Your Name $100.00 Assets:Checking $-100.00 -!end +end apply account @end smallexample -(Note: The @samp{!account} above means that all accounts mentioned in +(Note: The @samp{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). @@ -1336,7 +1344,7 @@ associated with particular transactions. Your own tastes will decide which is best for your situation. -@node Keeping a Journal, Building Reports, Principles of Accounting, Top +@node Keeping a Journal, Transactions , Principles of Accounting, Top @chapter Keeping a Journal The most important part of accounting is keeping a good journal. If you @@ -1374,8 +1382,8 @@ posting. * Structuring Your Accounts:: * Commenting on your journal:: * Currency and Commodities:: -* Advanced Transactions:: -* File Format:: +* Keeping it Consistent:: +* Journal Format:: * Archiving Previous Years :: * Using Emacs:: @end menu @@ -1422,7 +1430,7 @@ 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} +give an error and stop calculating.} @node Starting up, Structuring Your Accounts, Most Basic Entry, Keeping a Journal @@ -1536,9 +1544,9 @@ There are several forms of comments within a transaction, for example: @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 `;'s and are preserved as part of the transaction. The `:'s indicate meta-data and tags -(@pxref{Transaction Notes and Tags}). +(@pxref{Metadata}). -@node Currency and Commodities, Advanced Transactions, Commenting on your journal, Keeping a Journal +@node Currency and Commodities, Keeping it Consistent, Commenting on your journal, Keeping a Journal @section Currency and Commodities @cindex currency @@ -1604,6 +1612,7 @@ since we haven't told ledger to convert commodities. * 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 @@ -1655,7 +1664,7 @@ The @{$30.00@} is a lot price. You can also use a lot date, [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, , Buying and Selling Stock, Currency and Commodities +@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 @@ -1703,499 +1712,59 @@ both exist, you ask? To support things like this: Assets:Checking @end smallexample -This transaction says that you bought 11 gallons priced at $2.29 per +This transaction says that you bought 11 gallons priced at $2.299 per gallon at a @strong{cost to you} of $2.30 per gallon. Ledger auto-generates a balance posting in this case to Equity:Capital Losses to reflect the -11 cent difference, which is then balanced by Assets:Checking because +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 +@node Keeping it Consistent, Journal Format, Currency and Commodities, Keeping a Journal +@section Keeping it Consistent +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. -@node Advanced Transactions, File Format, Currency and Commodities, Keeping a Journal -@section Advanced Transactions -@menu -* Transaction Notes and Tags:: -* Multiple Account Transactions:: -* Virtual Transactions:: -* Automatic Transactions:: -* Checking Balances and Reconciliations:: -* Effective Dates:: -* Periodic Transactions:: -* Recording Commodity Lot Prices:: -@end menu - -@node Transaction Notes and Tags, Multiple Account Transactions, Advanced Transactions, Advanced Transactions -@subsection Transaction Notes and Tags - -Ledger 3.0 supports entry and transaction ``notes'', which may -contain new meta-data and tag markers. Here's an example: -@cindex meta-data -@cindex tags - -@smallexample - 2004/05/27 (100) Credit card company - ; This is an entry note! - ; Sample: Value - Liabilities:MasterCard $20.00 - ; This is a transaction note! - ; Sample: Another Value - ; :MyTag: - Assets:Bank:Checking - ; :AnotherTag: -@end smallexample - -An indented paragraph starting with `;' is parsed as a persistent note -for its preceding category. These notes will get printed back to you -with the ``print'' command. They are accessible to value expressions -using the ``note'' variable. - -Further, any occurrence of ``:foo:'' in a note will cause a meta-data tag -for "foo" to be registered for that entry. You can then search for -such transactions using: -@findex % -@cindex tags -@smallexample - ledger reg %foo - ledger reg tag foo -@end smallexample - -@cindex setting the value of a tag -@cindex value tags - -Also, if any word in the note ends (but does not start) with a colon, -the remainder of that line will be taken to be the meta-data value for -that tag. That is: - -@smallexample - ; :foo:bar:baz: <-- These are three tags - ; name: value <-- this is a tag with a value -@end smallexample -@cindex searching for tags -@cindex tags, searching for -Tags with value can be searched for just like tags. In addition, you -can further limit your tag search by looking for only those tags that -have specific values: - -@smallexample - ledger reg %name=value - ledger reg tag name=value -@end smallexample -@findex group-by "tag('foo')" -@cindex group by tags -The group-by and sort functions also support tags: -@smallexample -ledger --group-by "tag('foo')" bal -@end smallexample -Will produce a balance summary of all transaction with tag `foo' group -by transactions with the same value for `foo'. - -@smallexample -ledger reg --sort "tag('foo')" %foo -@end smallexample -Produces a register view with the transaction have tag `foo' sorted by -the tags value. - -Comments that occur before an entry, or which starts at column zero, are -always ignored and are neither searched nor printed back. - -If a posting comment is a date (with brackets), it modifies the date for that posting: -@smallexample -2010/02/01 Sample - Assets:Bank $400.00 - Income:Check $-400.00 ; [2010/01/01] -@end smallexample -You can use meta-data to override the payee field for individual postings within a transaction: (source) -@cindex overriding payee using meta-data -@cindex meta-data, overriding payee -@smallexample -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 -Meta-data are normally strings, but you can create meta-data of other types: - -@smallexample -2010/06/17 Sample - Assets:Bank $400.00 - Income:Check1 $-100.00 - ; Date:: [2010/09/01] - ; Amount:: $100.00 -@end smallexample -(Note that this Date tag is not the same as the posting date.) - -@cindex @@tag directive -@cindex tags, applying to several transactions. -You apply a tag or tags to a group of transactions by surrounding them with a @code{@@tag ... @@end tag} block. -For example, if you wanted a -conceptual ``page'' of transactions relating to business trip to -Chicago, you could do this: - -@smallexample - @@tag Location: Chicago - @@tag Purpose: Business - - ... transactions go here - - @@end tag - @@end tag -@end smallexample -It would be as if you'd applied "; Location: Chicago", etc., to every transaction. - -@node Multiple Account Transactions, Virtual Transactions, Transaction Notes and Tags, Advanced Transactions -@subsection Multiple Account Transactions - -Often times a transaction needs to be split across several accounts. -This is trivially simple in a Ledger journal: - -@cindex splitting transactions across accounts -@cindex transactions, splitting across accounts -@smallexample -2011/09/15 * Deposit Acme Bytepumps Monthly Paycheck - Income:Taxable:Acme Bytepumps Inc. $-2500.00 - Assets:Brokerage:Checking $175.00 - Assets:Investments:401K Deferred $250.00 - Expenses:Tax:Medicare $36.25 - Expenses:Tax:Federal Tax $200.00 - Expenses:Tax:State Tax $20.00 - Expenses:Insurance:Life $18.75 - Assets:Credit Union:Joint Checking -@end smallexample - -This is an example of a paycheck entry. The money comes @strong{out} of your -income account, and is spent into several other accounts. The last line -doesn't require an amount, as ledger will automatically balance the -transaction (it will be $1800 into the Joint Checking account) - - -@node Virtual Transactions, Automatic Transactions, Multiple Account Transactions, Advanced Transactions -@subsection Virtual Transactions - - -A virtual posting is when you, in your mind, see money as moving -to a certain place, when in reality that money has not moved at all. -There are several scenarios in which this type of tracking comes in -handy, and each of them will be discussed in detail. - -To enter a virtual posting, surround the account name in -parentheses. This form of usage does not need to balance. However, -if you want to ensure the virtual posting balances with other -virtual postings in the same transaction, use square brackets. For -example: - -@smallexample -10/2 Paycheck - Assets:Checking $1000.00 - Income:Salary $-1000.00 - (Debt:Alimony) $200.00 -@end smallexample - -In this example, after receiving a paycheck an alimony debt is -increased---even though no money has moved around yet. - -@smallexample -10/2 Paycheck - Assets:Checking $1000.00 - Income:Salary $-1000.00 - [Savings:Trip] $200.00 - [Assets:Checking] $-200.00 -@end smallexample - -In this example, $200 has been deducted from checking toward savings -for a trip. It will appear as though the money has been moved from -the account into @samp{Savings:Trip}, although no money has actually -moved anywhere. - -When balances are displayed, virtual postings will be factored in. -To view balances without any virtual balances factored in, using the -@option{-R} flag, for ``reality''. - - -@node Automatic Transactions, Checking Balances and Reconciliations, Virtual Transactions, Advanced Transactions -@subsection Automatic 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 -; This automated transaction will compute Huqúqu'lláh based on this -; journal's postings. Any 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. 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 -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 -ledger balance Liabilities:Huquq -@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: - -@smallexample -ledger -Q -t "/Liab.*Huquq/?(a/P@{2.22 AU@}<=@{-1.0@}&a):a" -s 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: - -@smallexample -ledger -Q -T "/Liab.*Huquq/?(O/P@{2.22 AU@}<=@{-1.0@}&O):O" -s 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 -= /^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}. - -Automated transactions can use the full range of value expressions in -their predicate. If you wanted to specify a transaction only occur to -certain accounts that meet certain value criteria you could specify: - -@smallexample -= /Employees:.*:Payroll$/ and expr (amount >= $1000 and amount < $10000) - Expenses:Tax 0.27 -@end smallexample -In this case, @samp{amount} is tied to the amount of the posting being -tested. - -But, wait! There's more! - -@cindex Tax Bracket automation -@cindex value expressions in automatic transactions - -In the short example above we calculated the taxes due for income within -a certain bracket. But in reality this calculation is more difficult. -There are different rates for different marginal incomes and those taxes -are not easily described by a simple multiplicative coefficient. -Automated transactions can use value expressions in their postings to -determine the amounts. So to expand the example above for a three tax -bracket system we could enter: - +In order to combat inconsistency you can define allowable accounts and +or payees. For simplicity, create a separate text file and enter define +accounts a payees like @smallexample -= /Employees:.*:Payroll$/ and expr (amount < $10000.00) - (Expenses:Tax) 0.1 -= /Employees:.*:Payroll$/ and expr (amount > $10000.00 and amount < $100000.00 ) - (Expenses:Tax) ($1000.00 + .15 * (amount - $10000.00)) -= /Employees:.*:Payroll$/ and expr (amount > $100000.00) - (Expenses:Tax) ($13500.00 + .20 * (amount-$100000.00)) -@end smallexample - -@node Checking Balances and Reconciliations, Effective Dates, Automatic Transactions, Advanced Transactions -@subsection Forcing balances and Reconciling Accounts - - -Ledger has a feature for ensuring known past balances. Here's -an example entry: -@cindex forcing a balance -@cindex balance verification -@smallexample - 2008/11/26 (Interest) EXTND INS SWEEP ACCT(FDIC-INS) - * Assets:Brokerage $0.07 = $970.64 - Income:Interest $-0.07 -@end smallexample - -What this says is that as of 11/26/08 (bank perspective), the -Assets:Brokerage account was known to equal $970.64. It @strong{must} -equal this amount at this point in the Ledger file, or there will be a -balancing error. - -If you reconcile bank statements you can enter the reconciliation and -link to a file (if you have it) -@cindex statement reconciliation -@cindex reconcile statements - -@smallexample -2009/12/01 Foo - Assets:Checking $10.00 - Equity - -2009/12/10 Reconciled statement dated 2009/12/08 - ; Link: [[file:statements/checking/2009-12.pdf][2009-12.pdf]] - [Assets:Checking] = $10.00 -@end smallexample - -@node Effective Dates, Periodic Transactions, Checking Balances and Reconciliations, Advanced Transactions -@subsection Effective Dates -@cindex effective dates - -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 -2008/01/01=2008/01/14 Client invoice ; estimated date you'll be paid - Assets:Accounts Receivable $100.00 - Income: Client name -@end smallexample -@noindent Then, when you receive the payment, you change it to - -@smallexample -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 -2008/01/15 Client payment - Assets:Checking $100.00 - Assets:Accounts Receivable +account Expenses +account Expenses:Utilities +... @end smallexample -Now - +Using the @samp{--strict} option will cause Ledger to complain if any accounts are not previously defined: @smallexample - ledger --subtotal --begin 2008/01/01 --end 2008/01/14 bal Income +15:27:39 ~/ledger (next) > 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 -@noindent gives you your accrued income in the first two weeks of the year, and -@smallexample - ledger --effective --subtotal --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 cost $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. +If you have a large Ledger register already created use the @samp{accounts} command to get started: @smallexample -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 +ledger accounts >> Accounts.dat @end smallexample -This entry accomplishes this. Every month until you'll start with an -automatic $37.50 deficit like you should, while your checking account -really knows that it debited $225 this month. - - -@node Periodic Transactions, Recording Commodity Lot Prices, Effective Dates, Advanced Transactions -@subsection Periodic Transactions - -A periodic transaction starts with a ~ followed by a period expression. -Periodic transactions are used for budgeting and forecasting only, they -have no effect without the @samp{--budget} option specified. - -See @ref{Budgeting and Forecasting} for examples and details. +@noindent You will have to edit this file to add the @samp{account} directive. -@node Recording Commodity Lot Prices, , Periodic Transactions, Advanced Transactions -@subsection Recording Commodity Lot Prices - -If you are tracking investments it is often necessary to keep track of -specific purchases of a commodity bought at difference prices. These -specific purchases are referred to as ``lots''. Tracking lots using ledger -requires some additional info in the journal as well as additional -command-line options when generating reports. - -Say you want to record purchase of two separate lots of ACME, then sell -some shares. The correct way to do this is: - -@smallexample -2010-09-01 * Buy 2 shares of ACME @@ $100 - Assets:Broker 2 ACME @@ $100.00 - Assets:Cash - -2010-09-10 * Buy 2 share of ACME @@ $110 - Assets:Broker 2 ACME @@ $110.00 - Assets:Cash - -2011-09-20 * Sell 2 shares of ACME @@ $150 - Assets:Broker -1 ACME @{$100.00@} @@ $150.00 - Assets:Broker -1 ACME @{$200.00@} @@ $150.00 - Assets:Cash -@end smallexample - -To report which lots of commodities you hold, use the -@samp{--lot-prices} option. For example, after buying the 2 shares at -$100 and 1 at $200 it would show you: -@smallexample -$ ledger balance --lot-prices Assets:Broker until 2011-09-15 - 2 ACME @{$100.00@} - 1 ACME @{$200.00@} Assets:Broker -@end smallexample -@noindent without the @samp{--lot-prices} option you would only see the total number of shares you held: -@smallexample -$ ledger balance Assets:Broker until 2011-09-15 - 3 ACME Assets:Broker -@end smallexample -@noindent and after the sale on @samp{2011-09-20} it would show you: -@smallexample -$ ledger balance --lot-prices Assets:Broker - 1 ACME @{$100.00@} Assets:Broker -@end smallexample +@node Journal Format, Archiving Previous Years , 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. -@node File Format, Archiving Previous Years , Advanced Transactions, Keeping a Journal -@section File Format for Users @menu -* File Format Intro:: * Transaction and Comments:: * Command Directives:: @end menu -@node File Format Intro, Transaction and Comments, File Format, File Format -@subsection Introduction -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. - -@node Transaction and Comments, Command Directives, File Format Intro, File Format +@node Transaction 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: @@ -2232,7 +1801,7 @@ posting cost, by specifying @samp{@@ AMOUNT}, or a complete posting cost with @samp{@@@@ AMOUNT}. Lastly, the @samp{NOTE} may specify an actual and/or effective date for the posting by using the syntax @samp{[ACTUAL_DATE]} or @samp{[=EFFECTIVE_DATE]} or -@samp{[ACTUAL_DATE=EFFECTIVE_DATE]}.(See @pxref{Virtual Transactions}) +@samp{[ACTUAL_DATE=EFFECTIVE_DATE]}.(See @pxref{Virtual postings}) @item P Specifies a historical price for a commodity. These are usually found @@ -2249,7 +1818,7 @@ sign. After this initial line there should be a set of one or more postings, just as if it were normal transaction. If the amounts of the postings have no commodity, they will be applied as modifiers to -whichever real posting is matched by the value expression(See @pxref{Automatic Transactions}). +whichever real posting is matched by the value expression(See @pxref{Automated transactions}). @item ~ A period transaction. A period expression must appear after the tilde. @@ -2268,17 +1837,61 @@ tags can be used to augment the reporting and filtering capabilities of Ledger. @end table -@node Command Directives, , Transaction and Comments, File Format +@node Command Directives, , Transaction and Comments, Journal Format @subsection Command Directives @table @code -@item ! @@ -A line beginning with an exclamation mark or an @@ sign denotes a -command directive. It must be immediately followed by a command word. -The supported commands are: +@item beginning of line +Command directives must occur at the beginning of a line. Use of ! and +@@ is deprecated. + +@item account + +Pre-declare valid account names. This only has effect if +@samp{--strict} or @samp{--pedantic} is used (see below). The +@samp{account} directive supports several optional sub-directives, if +they immediately follow the account directive and if they begin with +whitespace: + +@smallexample + 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 @samp{note} sub-directive associates a textual note with the account. This can +be accessed later using the @samp{note} valexpr function in any account context. + +The @samp{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 @samp{payee} sub-directive, which can occur multiple times, provides regexps +that identify the account if that payee is encountered and an account within +its transaction ends in the name "Unknown". Example: + +@smallexample + 2012-02-27 KFC + Expenses:Unknown $10.00 ; Read now as "Expenses:Food" + Assets:Cash +@end smallexample +The @samp{check} and @samp{assert} directives warn or error (respectively) if the given +value expression evaluates to false within the context of any posting. -@item @@account +The @samp{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 @samp{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 @c instance_t::master_account_directive Sets the root for all accounts following the directive. Ledger supports a hierarchical tree of accounts. It may be convenient to keep two @@ -2288,27 +1901,26 @@ could preface all personal accounts with @code{personal:} and all business account with @code{business:}. You can easily split out large groups of transaction without manually editing them using the account directive. For example: -@smallexample -@@account Personal +@smallexample +apply account Personal 2011/11/15 Supermarket Expenses:Groceries Assets:Checking - @end smallexample Would result in all postings going into @code{Personal:Expenses:Groceries} and @code{Personal:Assets:hecking} -until and @code{@@end account} directive was found. +until and @code{end apply account} directive was found. -@item @@alias +@item 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 -@@alias Dining=Expenses:Entertainment:Dining -@@alias Checking=Assets:Credit Union:Joint Checking Account +alias Dining=Expenses:Entertainment:Dining +alias Checking=Assets:Credit Union:Joint Checking Account 2011/11/28 YummyPalace Dining $10.00 @@ -2317,32 +1929,32 @@ of accounts, it may be convenient to define an alias, for example: @end smallexample The aliases are only in effect for transactions read in after the alias -is defined and are effected by @code{@@account} directives that precede +is defined and are effected by @code{account} directives that precede them. -@item @@assert +@item assert @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> +assert <VALUE EXPRESSION BOOLEAN RESULT> @end smallexample -@item @@bucket +@item 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 calculate balance the transaction in the posting. The -@code{@@bucket} allows you to fill in all postings and automatically +@code{bucket} allows you to fill in all postings and automatically generate an additional posting to the bucket account balancing the transaction. The following example set the @code{Assets:Checking} as the bucket: @smallexample -@@bucket Assets:Checking +bucket Assets:Checking 2011/01/25 Tom's Used Cars Expenses:Auto $ 5,500.00 @@ -2354,13 +1966,13 @@ the bucket: @end smallexample -@item @@capture +@item capture @c instance_t::account_mapping_directive Directs Ledger to replace any account matching a regex with the given account. For example: @smallexample -@@capture Expenses:Deductible:Medical Medical +capture Expenses:Deductible:Medical Medical @end smallexample Would cause any posting with @code{Medical} in it's name to be replaced with @@ -2370,23 +1982,55 @@ Would cause any posting with @code{Medical} in it's name to be replaced with Ledger will display the mapped payees in @code{print} and @code{register} reports. -@item @@check +@item check @c instance_t::check_directive in textual.cc A check can issue a warning if a condition is not met during Ledger's run. @smallexample -@@check <VALUE EXPRESSION BOOLEAN RESULT> +check <VALUE EXPRESSION BOOLEAN RESULT> @end smallexample -@item @@comment +@item comment @c instance_t::comment_directive in textual.cc -Start a block comment, closed by @code{@@end comment}. -@item @@define +Start a block comment, closed by @code{end comment}. + +@item commodity +Pre-declare commodity names. This only has effect if @samp{--strict} or +@samp{--pedantic} is used (see below). + + commodity $ + commodity CAD + +The @samp{commodity} directive supports several optional sub-directives, if they +immediately follow the commodity directive and if they begin with whitespace: + +@smallexample + commodity $ + note American Dollars + format $1,000.00 + nomarket + default +@end smallexample + +The @samp{note} sub-directive associates a textual note with the commodity. At +present this has no value other than documentation. + +The @samp{format} directive gives you a way to tell Ledger how to format this +commodity. In future using this directive will disable Ledger's observation +of other ways that commodity is used, and will provide the ``canonical'' +representation. + +The @samp{nomarket} directive states that the commodity's price should never be +auto-downloaded. + +The @samp{default} directive marks this as the ``default'' commodity. + +@item define @c instance_t::define_directive in textual.cc Allows you to define value expression for future use. For example: @smallexample -@@define var_name=$100 +define var_name=$100 2011/12/01 Test Expenses (var_name*4) @@ -2394,41 +2038,46 @@ Allows you to define value expression for future use. For example: @end smallexample The posting will have a cost of $400. -@item @@end +@item end @c instance_t::end_directive in textual.cc -Closes block commands like @code{@@tag} or @code{@@comment}. -@item @@expr +Closes block commands like @code{tag} or @code{comment}. +@item expr @c instance_t::expr_directive in textual.cc -@item @@fixed +@item fixed @c instance_t::fixed_directive in textual.cc -@item @@include +@item include @c instance_t::include_directive in textual.cc Include the stated file as if it were part of the current file. -@item @@payee +@item payee @c instance_t::payee_mapping_directive in textual.cc -Directs Ledger to replace any payee matching a regex with the given -payee. You may download transactions from your bank that you want to be -shortened or altered. For example, the the payee for the grocery store -near me is only one character different than the payee if I buy gasoline -at the grocery story. I can enter payee mappings that make this very clear: +The @samp{payee} directive supports one optional sub-directive, if it immediately +follows the payee directive and if it begins with whitespace: + +@smallexample + payee KFC + alias KENTUCKY FRIED CHICKEN +@end smallexample + +The @samp{alias} directive provides a regexp which, if it matches a parsed payee, +the declared payee name is substituted: @smallexample -@@payee Supermarket Gas Supermarket 4 -@@payee Supermarket Groceries Supermarket 1 + 2012-02-27 KENTUCKY FRIED CHICKEN ; will be read as being 'KFC' + ... @end smallexample Ledger will display the mapped payees in @code{print} and @code{register} reports. -@item @@tag +@item 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 -@@tag hastag -@@tag nestedtag: true +apply tag hastag +apply tag nestedtag: true 2011/01/25 Tom's Used Cars Expenses:Auto $ 5,500.00 ; :nobudget: @@ -2438,12 +2087,12 @@ Allows you to designate a block of transactions and assign the same tag to all. Expenses:Books $20.00 Liabilities:MasterCard -@@end tag nestedtag +end apply tag nestedtag 2011/12/01 Sale Assets:Checking:Business $ 30.00 Income:Sales -@@end tag hastag +end apply tag hastag @end smallexample @noindent is the equivalent of @@ -2468,18 +2117,42 @@ Allows you to designate a block of transactions and assign the same tag to all. Income:Sales @end smallexample -Note that anything following "@code{@@end tag}" is ignored. placing the +Note that anything following ``@code{end tag}'' is ignored. placing the name of the tag that is being closed is a simple way to keep track. -@item @@test +@item tag +Pre-declares tag names. This only has effect if @samp{--strict} or +@samp{--pedantic} is used (see below). + +@smallexample + tag Receipt + tag CSV +@end smallexample + +The 'tag' directive supports two optional sub-directives, if they immediately +follow the tag directive and if they begin with whitespace: + +@smallexample + tag Receipt + check value =~ /pattern/ + assert value != "foobar" +@end smallexample + +The @samp{check} and @samp{assert} 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 not be a string if typed-metadata is used!). Such checks or +assertions are not called if no value is given. + +@item test @c instance_t::comment_directive in textual.cc -This is a synonym for @code{@@comment} and must be closed by and @code{@@end} tag. +This is a synonym for @code{comment} and must be closed by and @code{end} tag. -@item @@year +@item 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 Y, for -example: @samp{@@year 2004}. This is useful at the beginning of a file, to +example: @samp{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. @@ -2491,9 +2164,9 @@ alone, for backwards compatibility with older Ledger versions. @table @code @item A -See @code{@@bucket} +See @code{bucket} @item Y -See @code{@@year} +See @code{year} @item N SYMBOL @@ -2532,7 +2205,7 @@ timelog files. See the timeclock's documentation for more info on the syntax of its timelog files. @end table -@node Archiving Previous Years , Using Emacs, File Format, Keeping a Journal +@node Archiving Previous Years , Using Emacs, Journal Format, Keeping a Journal @section Archiving Previous Years @@ -2792,7 +2465,1112 @@ kill the report buffer -@node Building Reports, Reporting Commands, Keeping a Journal, Top + +@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 vs. 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 + 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 + 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 + 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 + 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 + 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 + +You can associate a second date with a transaction by following the primary +date with an equals sign: + +@smallexample + 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 --aux-date, then all reports and calculations +(including pricing) will use the aux 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 + 2012-03-10 (#100) KFC + Expenses:Food $20.00 + Assets:Checking +@end smallexample + +@node Transaction state, Transaction notes, Codes, Transactions +@section Transaction state + +A transaction can have a ``state'': cleared, pending, or uncleared. The default +is uncleared. To mark a transaction cleared, put a * before the payee, and +after date or code: + +@smallexample + 2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +@noindent To mark it pending, use a !: + +@smallexample + 2012-03-10 ! KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +What these mean is entirely up to you. The --cleared option will limits to +reports to only cleared items, while --uncleared shows both uncleared and +pending items, and --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 + 2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +@noindent Is the same as writing: + +@smallexample + 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 + 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 this a ``hard separator''), you may +introduce a note about the transaction using the ; character: + +@smallexample + 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 + 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 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 + 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 + +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: 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: + +@smallexample + 2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; :TAG: +@end smallexample + +You can gang up multiple tags by sharing colons: + +@smallexample + 2012-03-10 * KFC + Expenses:Food $20.00 + Assets:Cash + ; :TAG1:TAG2:TAG3: +@end smallexample + +@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 + 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 ::, it's 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 + 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 + +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 @samp{--real}. + +To specify a virtual account, surround the account name with parentheses: + +@smallexample + 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 + 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 usually a numerical figure with an (optional) commodity, but it +can also be any value expression. To indicate this, surround the amount +expression with parentheses: + +@smallexample + 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 +@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. + +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. + +@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 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 + 2012-03-10 Adjustment + Assets:Cash = $500.00 + Equity:Adjustments +@end smallexample + +Since the second posting is also null, it's 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 true empty, it is indirectly provided +by the balance assignment's value. + +@node Balancing transactions, , Resetting a balance, Balance verification +@subsection Balancing transactions + +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 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 --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 get +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 + 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 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 @ symbol after the amount +or amount expression: + +@smallexample + 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 + 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 + +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 -V flag will never convert a +primary commodity into any other 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 + 2012-03-10 My Broker + Assets:Brokerage 10 AAPL @ ($500.00 / 10) + Assets:Brokerage:Cash +@end smallexample + +You can even have both: + +@smallexample + 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 @ character specifies the @emph{per-unit} price for +the commodity being transferred. If you'd like to specify the total cost +instead, use @@@@: + +@smallexample + 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 + 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 @@ or @@@@ with parentheses: + +@smallexample + 2012-03-10 My Brother + Assets:Brokerage 1000 AAPL (@@) $1 + Income:Gifts Received +@end smallexample + +@node Commodity prices, Prices vs. costs, Virtual posting costs, Transactions +@section Commodity prices + +When a transaction occurs that exchange 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 @samp{--lot-prices} to show these hidden price figures. + +For example, consider the stock sale given above: + +@smallexample + 2012-03-10 My Broker + Assets:Brokerage 10 AAPL @@ $50.00 + Assets:Brokerage:Cash +@end smallexample + +The commodity transferred into Assets:Brokerage is not actually 10 AAPL, but +rather 10 AAPL @{$5.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 $5.00 was the price of that +exchange. + +This becomes significant if you later sell that commodity again. For example, +you might write this: + +@smallexample + 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 + 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 + 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 commodity, whereas +@{$5.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 form in the output. The double price +form is a shorthand only. + +Plus, it comes with dangers. This works fine: + +@smallexample + 2012-04-10 My Broker + Assets:Brokerage 10 AAPL @ $50.00 + Assets:Brokerage:Cash $750.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 $750.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 figure and must be +rounded, the capital gains figure could be off by a cent. Use with caution. + +@node Prices vs. costs, Lot dates, Commodity prices, Transactions +@section Prices vs. 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 $750.00 + + 2012-04-10 My Broker + Assets:Brokerage 10 AAPL @{$50.00@} + Assets:Brokerage:Cash $750.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. + +@section Fixated prices + +If you buy 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 @{=AMOUNT@}: + +@smallexample + 2012-04-10 My Broker + Assets:Brokerage 10 AAPL @{=$50.00@} + Assets:Brokerage:Cash $750.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. + +@menu +* Fixated costs:: +@end menu + +@node Fixated costs, , Prices vs. costs, Prices vs. costs +@subsection Fixated costs + +Since price annotations are 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 $750.00 +@end smallexample + +This is the same as the previous transaction, with the same caveats found in +the section ``Prices vs. costs''. + +@node Lot dates, Lot notes, Prices vs. costs, Transactions +@section Lot dates + +In addition to lot prices, you can specify lot dates and reveal them with +@samp{--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 + +You can also associate arbitrary notes for your own record keeping in +parentheses, and reveal them with --lot-notes. One caveat is that the note +cannot begin with an @ 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 any combination of lot prices, dates or notes, in any order. They are +all optional. + +To show all lot information in a report, use @samp{--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 -Q +was specified and a suitable ``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 argument 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 + 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 + 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: ``$''). + 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 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 + 2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +If I write this automated transaction before it in the file: + +@smallexample + = 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 + 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:: +@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 + = 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 + 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 + = 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 + 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 want to refer to the account that matched in some way within the +automated transaction itself. This is done by using the string $account, +anywhere within the account part of the automated posting: + +@smallexample + = food + (Budget:$account) 10 + + 2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Becomes: + +@smallexample + 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 + = food + ; Foo: Bar + (Budget:$account) 10 + + 2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Becomes: + +@smallexample + 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 + = food + (Budget:$account) 10 + ; Foo: Bar + + 2012-03-10 KFC + Expenses:Food $20.00 + Assets:Cash +@end smallexample + +Becomes: + +@smallexample + 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 * or ! 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 + +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 +2008/01/01=2008/01/14 Client invoice ; estimated date you'll be paid + Assets:Accounts Receivable $100.00 + Income: Client name +@end smallexample +@noindent Then, when you receive the payment, you change it to + +@smallexample +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 +2008/01/15 Client payment + Assets:Checking $100.00 + Assets:Accounts Receivable +@end smallexample +Now + +@smallexample + ledger --subtotal --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 + ledger --effective --subtotal --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 cost $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 +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 until you'll start with an +automatic $37.50 deficit like you should, while your checking account +really knows that it debited $225 this month. + + +@node Periodic Transactions, , Effective Dates, Automated transactions +@subsection Periodic Transactions + +A periodic transaction starts with a ~ followed by a period expression. +Periodic transactions are used for budgeting and forecasting only, they +have no effect without the @samp{--budget} option specified. + +See @ref{Budgeting and Forecasting} for examples and details. + + + + + +@node Building Reports, Reporting Commands, Transactions , Top @chapter Building Reports @menu @@ -2891,17 +3669,17 @@ there are none. The second looks for any account with ``Bo'', which is If you want to know exactly how much you have spent in a particular account on a particular payee, the following are equivalent: @smallexample -> ledger balance Auto:Fuel and @@Chevron +> ledger balance Auto:Fuel and Chevron > ledger balance --limit "(account=~/Fuel/) & (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 power 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. +@noindent will show you the amount expended on gasoline at Chevron. +The second example is the first example of the very power 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: +If you want to exclude specific accounts from the report, you can +exclude multiple accounts with parentheses: @smallexample ledger -s bal Expenses and not \(Expenses:Drinks or Expenses:Candy or Expenses:Gifts\) @end smallexample @@ -3062,10 +3840,10 @@ current allocation? Using the balance command and some tricky formatting! ledger bal Allocation --current --format "\ %-17((depth_spacer)+(partial_account))\ %10(percent(market(display_total), market(parent.total)))\ - %16(market(display_total))\n" + %16(market(display_total))\n%/" @end smallexample -Which yields: +@noindent Which yields: @smallexample Allocation 100.00% $100000.00 @@ -3074,6 +3852,7 @@ Allocation 100.00% $100000.00 Domestic 95.31% $58196.29 Global 4.69% $2863.71 @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'' @@ -3088,9 +3867,10 @@ 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 give 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} format their +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 the line. +current market value of the the line. The last two characters ``%/'' +tell Ledger what to do for the last line, in this case, nothing. @cindex plotting @cindex GNUplot @@ -3116,13 +3896,6 @@ 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. -@menu -* Typical plots:: -@end menu - -@node Typical plots, , Visualizing with Gnuplot, Visualizing with Gnuplot -@subsubsection Typical plots - Here are some useful plots: @smallexample @@ -3256,7 +4029,7 @@ downloads. Unfortunately the file formats, aside form the commas, are all different. The ledger convert command tried to help as much as it can. -Your banks csv files will have field in different orders from other +Your banks 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. @@ -3366,14 +4139,12 @@ You can combine multiple source code blocks before executing ledger and do all kinds of other wonderful things with Babel (and org). -@subsubsection Using Ledger for Accounting in Org-mode with Babel +@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. -@unnumberedsubsubsec Getting Started - With a recent version of org (7.01+), Ledger support is provided. To use it, enable Ledger support. Check the Babel documentation on Worg for instructions on how to achieve this but I currently do this directly as @@ -3406,7 +4177,7 @@ least) in which these can be included: The first two are described in more detail in this short tutorial. -@unnumberedsubsubsec Embedded Ledger example with single source block +@subsubheading Embedded Ledger example with single source block The easiest, albeit possibly less useful, way in which to use Ledger within an org file is to use a single source block to record all Ledger @@ -3466,7 +4237,7 @@ financial state. Eventually, babel will support passing arguments to currently. Instead, we can use the concepts of literary programming, as implemented by the noweb features of babel, to help us. -@unnumberedsubsubsec Multiple Ledger source blocks with noweb +@subsubheading Multiple Ledger source blocks with noweb The 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 @@ -3475,7 +4246,7 @@ transactions together to generate reports. Using the same transactions used above, we could consider splitting these into expenses and income, as follows: -@unnumberedsubsubsec Income Entries +@subsubheading 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 @@ -3502,7 +4273,7 @@ have the :noweb yes babel header argument specified. income:salary #+end_src @end smallexample -@unnumberedsubsubsec Expenses +@subsubheading Expenses The following entries relate to personal expenses, such as rent and food. Again, these have all been placed in a single src block but could @@ -3519,7 +4290,7 @@ have been done individually. #+end_src @end smallexample -@unnumberedsubsubsec Financial Summaries +@subsubheading 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, @@ -3527,7 +4298,7 @@ blocks, we can now refer to these using the noweb expansion directives, reports for those transactions. Below are two examples, one to generate a balance report and one to generate a register report of all transactions. -@unnumberedsubsubsec An overall balance summary +@subsubheading An overall balance summary The overall balance of your account and expenditure with a breakdown according to category is specified by passing the :cmdline bal argument @@ -3569,7 +4340,7 @@ tell Ledger to include sub-accounts in the report. : £-2000.00 salary : £-1300.00 starting balances @end smallexample -@unnumberedsubsubsec Generating a monthly register +@subsubheading Generating a monthly register You can also generate a monthly register (the reg command) by executing the following src block. This presents a summary of transactions for @@ -3614,7 +4385,7 @@ the running total of the assets in our ledger. : 2010/08/01 - 2010/08/01 assets:bank:chequing £1000.00 £2653.53 @end smallexample -@unnumberedsubsubsec Summary +@subsubheading Summary This short tutorial shows how Ledger entries can be embedded in a org file and manipulated using Babel. However, only simple Ledger features @@ -3644,8 +4415,8 @@ The general format used for Ledger data is: @end smallexample The data stream is enclosed in a @samp{ledger} tag, which contains a -series of one or more transactions. Each @samp{xact} describes the transaction -and contains a series of one or more postings: +series of one or more transactions. Each @samp{xact} describes the +transaction and contains a series of one or more postings: @smallexample <xact> @@ -3662,16 +4433,15 @@ and contains a series of one or more postings: @end smallexample The date format for @samp{en:date} is always @samp{YYYY/MM/DD}. The -@samp{en:cleared} tag is optional, and indicates whether the -posting has been cleared or not. There is also an -@samp{en:pending} tag, for marking pending postings. The -@samp{en:code} and @samp{en:payee} tags both contain whatever text the -user wishes. +@samp{en:cleared} tag is optional, and indicates whether the posting has +been cleared or not. There is also an @samp{en:pending} tag, for +marking pending postings. The @samp{en:code} and @samp{en:payee} tags +both contain whatever text the user wishes. After the initial transaction data, there must follow a set of postings -marked with @samp{en:postings}. Typically these postings will -all balance each other, but if not they will be automatically balanced -into an account named @samp{<Unknown>}. +marked with @samp{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 @samp{en:postings} tag is a series of one or more @samp{posting}'s, which have the following form: @@ -3870,9 +4640,9 @@ backwards compatibility with Ledger 2.X. @node payees, , entry and xact, Reports about your Journals @subsection payees The @command{payees} reports all of the unique payees in the journal. To -filter the payees displayed you must use the @@ prefix: +filter the payees displayed you must use the prefix: @smallexample -macbook-2:$ ledger payees '@@Tar.+t' +macbook-2:$ ledger payees 'Tar.+t' El Dorade Restaraunt My Big Fat Greek Restaraunt Target @@ -3979,7 +4749,8 @@ Print version information and exit. @node Pre-commands, , Debug Options, Developer Commands @subsection Pre-Commands -Pre-commands are useful when you aren't sure how a command or option will work. +Pre-commands are useful when you aren't sure how a command or option +will work. @table @code @item args evaluate the given arguments against the following model transaction: @@ -3996,12 +4767,15 @@ evaluate the given arguments against the following model transaction: @item eval evaluate the given value expression against the model transaction @item expr "LIMIT EXPRESSION" -Print details of how ledger parses the given limit expression and apply it against a model transaction. +Print details of how ledger parses the given limit expression and apply +it against a model transaction. @item format "FORMATTING" -Print details of how ledger uses the given formatting description and apply it against a model transaction. +Print details of how ledger uses the given formatting description and +apply it against a model transaction. @item generate @item parse <VALUE EXPR> -Print details of how ledger uses the given value expression description and apply it against a model transaction. +Print details of how ledger uses the given value expression description +and apply it against a model transaction. @item period evaluate the given period and report how Ledger interprets it: @smallexample @@ -4023,7 +4797,8 @@ END_REACHED: <EOF> 1: 11-Jan-01 @end smallexample @item query -evaluate the given query and report how Ledger interprets it against the model transaction: +evaluate the given query and report how Ledger interprets it against the +model transaction: @smallexample 20:25:42 ~/ledger (next)> ledger query "/Book/" @@ -4115,13 +4890,6 @@ However, none of them are required to use the basic reporting commands. - - - - - - - @node Detailed Options Description, Period Expressions, Basic Usage, Command-line Syntax @section Detailed Option Description @@ -4156,23 +4924,12 @@ database. @option{--help} Displays the info page for ledger. -@option{--help-calc} -Displays the Value Expression chapter in the info ledger. - -@option{--help-comm <ARG>} -Search the info index for @code{<ARG>}. - -@option{--help-disp} -Displays the Format String chapter in the info ledger. - -@option{--help-info} -Displays the info page for the info reader. - @option{--init-file <PATH>} Specifies the location of the init file @file{.ledgerrc} -@option{--options} -Display the options in effect for this Ledger invocation, along with their values and the source of those values, for example: +@option{--options} Display the options in effect for this Ledger +invocation, along with their values and the source of those values, for +example: @smallexample 14:15:02 > ledger --options bal --cleared -f ~/ledger/test/input/drewr3.dat =============================================================================== @@ -4219,20 +4976,22 @@ 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. -@option{--decimal-comma} -Direct Ledger to parse journals using the European standard comma as decimal separator, vice a period. +@option{--decimal-comma} Direct Ledger to parse journals using the +European standard comma as decimal separator, vice a period. -@option{--download} -Direct Ledger to download prices using the script defined in @code{--getquote}. +@option{--download} Direct Ledger to download prices using the script +defined in @code{--getquote}. @option{--file <PATH>} Specify the input file path for this invocation. @cindex getquote @cindex download prices -@option{--getquote <PATH>} Tells ledger where to find the user defined script to download prices information. -@option{--input-date-format <DATE-FORMAT>} -Specify the input date format for journal entries. For example, +@option{--getquote <PATH>} Tells ledger where to find the user defined +script to download prices information. + +@option{--input-date-format <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 @@ -4241,8 +5000,8 @@ Would convert the @file{Export.csv} file to ledger format, assuming the the dates in the CSV file are like 12/23/2009 (@pxref{Date and Time Format Codes}). -@option{--master-account <ARGUMENT>} -Prepends all account names with the argument. +@option{--master-account <ARGUMENT>} Prepends all account names with the +argument. @smallexample 21:51:39 ~/ledger (next)> ledger -f test/input/drewr3.dat bal --master-account HUMBUG 0 HUMBUG @@ -4267,8 +5026,8 @@ Prepends all account names with the argument. $ 200.00 Mortgage:Principal @end smallexample -@option{--price-db <PATH>} -Specify the location of the price entry data file. +@option{--price-db <PATH>} Specify the location of the price entry data +file. @option{--price-exp INTEGER_MINUTES} Set the expected freshness of price quotes, in minutes. That is, if the last known quote for any commodity @@ -4305,17 +5064,16 @@ reported. That is, the option @code{--account Personal} would tack @code{Personal:} to the beginning of every account reported in a balance report or register report. -@option{--account-width <INT>} -Set the width of the account column in the @code{register} report to @code{N} characters. +@option{--account-width <INT>} Set the width of the account column in +the @code{register} report to @code{N} characters. -@option{--actual-dates} -Show actual dates of transactions (@pxref{Effective Dates}). Also @code{-L}. +@option{--actual-dates} Show actual dates of transactions +(@pxref{Effective Dates}). Also @code{-L}. -@option{--actual} -Report only real transactions, with no automated or virtual transactions used. +@option{--actual} Report only real transactions, with no automated or +virtual transactions used. -@option{--add-budget} -Show only unbudgeted postings. +@option{--add-budget} Show only unbudgeted postings. @option{--amount-data} On a register report print only the dates and amount of postings. Useful for graphing and spreadsheet applications. @@ -4325,37 +5083,35 @@ amount of postings. Useful for graphing and spreadsheet applications. amount (@pxref{Value Expressions}). Using @code{--amount} you can apply an arbitrary transformation to the postings. -@option{--amount-width <INT>} -Set the width in characters of the amount column in the register report. +@option{--amount-width <INT>} Set the width in characters of the amount +column in the register report. -@option{--anon} -anonymizes registry output, mostly for sending in bug reports. +@option{--anon} anonymizes registry output, mostly for sending in bug +reports. -@option{--average} -Print average values over the number of transactions instead of running totals. +@option{--average} Print average values over the number of transactions +instead of running totals. -@option{--balance-format <STR>} -specifies the format to use for the @code{balance} report (@pxref{Format Strings}). The default is: +@option{--balance-format <STR>} specifies the format to use for the +@code{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 -@option{--base} -ASK JOHN +@option{--base} ASK JOHN -@option{--basis} -Report the cost basis on all posting +@option{--basis} Report the cost basis on all posting -@option{--begin <DATE>} -Specify the start date of all calculations. Transactions before that date will be ignored. +@option{--begin <DATE>} Specify the start date of all calculations. +Transactions before that date will be ignored. + +@option{--bold-if <EXPR>} print the entire line in bold if the given +value expression is true (@pxref{Value Expressions}). -@option{--bold-if <EXPR>} -print the entire line in bold if the given value expression is true (@pxref{Value Expressions}). @smallexample ledger reg Expenses --begin Dec --bold-if "amount > 100" @end smallexample @@ -4378,8 +5134,9 @@ accounts in the budget (@pxref{Budgeting and Forecasting}). @option{--by-payee <REGEXP>} group the register report by payee. -@option{--cleared-format <FORMAT_STRING>} -specifies the format to use for the @code{cleared} report (@pxref{Format Strings}). The default is: +@option{--cleared-format <FORMAT_STRING>} specifies the format to use +for the @code{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, " @@ -4392,26 +5149,26 @@ specifies the format to use for the @code{cleared} report (@pxref{Format Strings "---------------- ---------------- ---------\n" @end smallexample -@option{--cleared} -consider only transaction that have been cleared for display and calculation. +@option{--cleared} consider only transaction that have been cleared for +display and calculation. +@option{--collapse} By default ledger prints all account in an account +tree. With @code{--collapse} it print only the top level account +specified. +@option{--collapse-if-zero} Collapses the account display only if it has +a zero balance. -@option{--collapse} -By default ledger prints all account in an account tree. With @code{--collapse} it print only the top level account specified. -@option{--collapse-if-zero} -Collapses the account display only if it has a zero balance. -@option{--color} -use color is the tty supports it. +@option{--color} use color is the tty supports it. -@option{--columns <INT>} -specify the width of the register report in characters. +@option{--columns <INT>} specify the width of the register report in +characters. -@option{--count} -Direct ledger to report the number of items when appended to the commodities, accounts or payees command. +@option{--count} Direct ledger to report the number of items when +appended to the commodities, accounts or payees command. -@option{--csv-format} -specifies the format to use for the @code{csv} report (@pxref{Format Strings}). The default is: +@option{--csv-format} specifies the format to use for the @code{csv} +report (@pxref{Format Strings}). The default is: @smallexample "%(quoted(date))," "%(quoted(code))," @@ -4428,36 +5185,86 @@ Shorthand for @code{--limit "date <= today"} @option{--daily} Shorthand for @code{--period "daily"} -@option{--date-format <DATE-FORMAT>} -specifies format ledger should use to print dates (@pxref{Date and Time Format Codes}). +@option{--date-format <DATE-FORMAT>} specifies format ledger should use +to print dates (@pxref{Date and Time Format Codes}). -@option{--date <EXPR>} -transforms the date of the transaction using @code{EXPR} +@option{--date <EXPR>} transforms the date of the transaction using +@code{EXPR} -@option{--date-width <INT>} -specifies the width, in characters, of the date column in the register report. +@option{--date-width <INT>} specifies the width, in characters, of the +date column in the register report. @option{--datetime-format} ASK JOHN +@option{--dc} Display register or balance in debit/credit format +If you use @samp{--dc} with either the register (reg) or 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 @samp{--dc}: + +@smallexample + $70 Assets:Cash + $30 Expenses:Food + $-100 Income:Employer + -------------------- + 0 +@end smallexample + +@noindent And with @samp{--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 + + @option{--depth <INT>} limit the depth of the account tree. In a balance report, for example, a @code{--depth 2} statement will print balances only for account with two levels, i.e. @code{Expenses:Entertainment} but not @code{Expenses:entertainemnt:Dining}. This is a display predicate, which means it only affects display, not the total calculations. -@option{--deviation} - reports each posting’s deviation from the average. It is only mean- -ingful in the register and prices reports. +@option{--deviation} reports each posting’s deviation from the + average. It is only mean- ingful in the register and prices reports. -@option{--display-amount <EXPR>} -apply a transform to the @strong{displayed} amount. This occurs after calculations occur. +@option{--display-amount <EXPR>} apply a transform to the +@strong{displayed} amount. This occurs after calculations occur. @option{--display <BOOLEAN_EXPR>} display lines that satisfy the expression given. -@option{--display-total <EXPR>} -apply a transform to the @strong{displayed} total. This occurs after calculations occur. +@option{--display-total <EXPR>} apply a transform to the +@strong{displayed} total. This occurs after calculations occur. @option{--dow} group transactions by the day of the week. @@ -4482,11 +5289,11 @@ register report. @option{--exact} ASK JOHN -@option{--exchange <COMMODITY>} -display values in terms of the given commodity. The latest available price is used. +@option{--exchange <COMMODITY>} display values in terms of the given +commodity. The latest available price is used. -@option{--flat} -force the full names of accounts to be used inthe balance report. The balance report will not use an indented tree. +@option{--flat} force the full names of accounts to be used inthe +balance report. The balance report will not use an indented tree. @option{--force-color} output tty color codes even if the tty doesn't support them. Ueful for TTY that don't advertise their capabilities @@ -4515,8 +5322,9 @@ report. EXPR can be anything, although most common would be @code{"payee"} or @code{"commodity"}. The @code{tags()} function is also useful here. -@option{--group-title-format} -sets the format for the headers that separate reports section of a grouped report. Only has effect with a @code{--group-by} register report. +@option{--group-title-format} sets the format for the headers that +separate reports section of a grouped report. Only has effect with a +@code{--group-by} register report. @smallexample ledger reg Expenses --group-by "payee" --group-title-format "------------------------ %-20(value) ---------------------\n" ------------------------ 7-Eleven --------------------- @@ -4540,22 +5348,22 @@ See email from John W. @option{--invert} Change the sign of all reported values. -@option{--limit <EXPR>} -Only transactions that satisfy the expression will be considered in the calculation. +@option{--limit <EXPR>} Only transactions that satisfy the expression +will be considered in the calculation. @option{--lot-dates} FIX THIS ENTRY -@option{lot-prices} +@option{--lot-prices} FIX THIS ENTRY -@option{lot-tags} +@option{--lot-tags} FIX THIS ENTRY -@option{lots-actual} +@option{--lots-actual} FIX THIS ENTRY -@option{lots} +@option{--lots} FIX THIS ENTRY @option{market} @@ -4567,31 +5375,31 @@ FIX THIS ENTRY @option{meta-width} FIX THIS ENTRY -@option{monthly} +@option{--monthly} FIX THIS ENTRY -@option{no-color} -FIX THIS ENTRY +@option{--no-color} +suppress any color TTY output. -@option{no-rounding} +@option{--no-rounding} FIX THIS ENTRY -@option{no-titles} +@option{--no-titles} FIX THIS ENTRY -@option{no-total} +@option{--no-total} FIX THIS ENTRY -@option{now} +@option{--now} FIX THIS ENTRY @option{only} FIX THIS ENTRY -@option{output} +@option{--output} FIX THIS ENTRY -@option{pager} +@option{--pager} FIX THIS ENTRY @option{payee} @@ -4600,8 +5408,8 @@ FIX THIS ENTRY @option{payee-width} FIX THIS ENTRY -@option{pending} -FIX THIS ENTRY +@option{--pending} +Use only postings tht are marked pending @option{percent} FIX THIS ENTRY @@ -4609,7 +5417,7 @@ FIX THIS ENTRY @option{period} FIX THIS ENTRY -@option{pivot} +@option{--pivot} FIX THIS ENTRY @option{plot-amount-format} @@ -4636,14 +5444,14 @@ FIX THIS ENTRY @option{quantity} FIX THIS ENTRY -@option{quarterly} +@option{--quarterly} FIX THIS ENTRY @option{raw} FIX THIS ENTRY -@option{real} -FIX THIS ENTRY +@option{--real} Account using only real transactions ignoring virtual +and automatic transactions. @option{register-format} FIX THIS ENTRY @@ -4651,16 +5459,16 @@ FIX THIS ENTRY @option{related-all} FIX THIS ENTRY -@option{related} -FIX THIS ENTRY +@option{--related} +In a register report show the related account. -@option{revalued-only} +@option{--revalued-only} FIX THIS ENTRY -@option{revalued-total} +@option{--revalued-total} FIX THIS ENTRY -@option{revalued} +@option{--revalued} FIX THIS ENTRY @option{seed} @@ -4669,20 +5477,21 @@ FIX THIS ENTRY @option{sort-all} FIX THIS ENTRY -@option{sort} -FIX THIS ENTRY +@option{--sort <VEXPR>} +Sort the register report based on the value expression given to sort -@option{sort-xacts} +@option{--sort-xacts} FIX THIS ENTRY -@option{--start-of-week <INT>} -Tell ledger to use a particular day of the week to start its ``weekly'' summary. @code{--start-of-week=1} specifies Monday as the start of the week. +@option{--start-of-week <INT>} Tell ledger to use a particular day of +the week to start its ``weekly'' summary. @code{--start-of-week=1} +specifies Monday as the start of the week. -@option{subtotal} +@option{--subtotal} FIX THIS ENTRY @option{--tail <INT>} -FIX THIS ENTRY +report only the last <INT> entries. Only useful ona register report. @option{total-data} FIX THIS ENTRY @@ -4717,8 +5526,8 @@ FIX THIS ENTRY @option{--weekly} synonymn for @code{--period "weekly"} -@option{--wide} -lets the register report use 132 columns. Identical to @code{--columns "132"} +@option{--wide} lets the register report use 132 columns. Identical to +@code{--columns "132"} @option{yearly} synonymn for @code{--period "yearly"} @@ -4817,7 +5626,7 @@ the right of the date). @option{--real} (@option{-R}) displays only real postings, not virtual. (A virtual posting is indicated by surrounding the account name with -parentheses or brackets; see @ref{Virtual Transactions} for more +parentheses or brackets; see @ref{Virtual postings} for more information). @option{--actual} (@option{-L}) displays only actual postings, and @@ -4928,9 +5737,6 @@ transaction. @option{--by-payee} (@option{-P}) reports subtotals by payee. -@option{--comm-as-payee} (@option{-x}) changes the payee of every -posting to be the commodity used in that posting. This can be -useful when combined with other options, such as @option{-s}. @option{--empty} (@option{-E}) includes even empty accounts in the @command{balance} report. @@ -5209,7 +6015,24 @@ balance against itself, and against any AAPL if @samp{--lots} is not specified. But if you do specify @samp{--lot-prices}, for example, then it will balance against that specific price for AAPL. +Normally when you use @samp{-X <commodity>} to request that amounts be reported in a +specific commodity, Ledger uses these values: +@itemize + +@item Register Report + For the register report, use the value of that commodity on the date of + the posting being reported, with a <Revalued> posting added at the end of + today's value is different from the value of the last posting. + +@item Balance Report + For the balance report, use the value of that commodity as of today. +@end itemize +You can now specify -H to ask that all valuations for any amount be done +relative to the date that amount was encountered. + +You can also now use -X (and -H) in conjunction with -B and -I, to see +valuation reports of just your basis costs or lot prices. @node Environment Variables, , Commodity Reporting, Detailed Options Description @subsection Environment variables @@ -5683,7 +6506,7 @@ Useful specifying a date in plain terms. For example, you could say @end table -@node Format Strings, Journal File Format, Value Expressions, Top +@node Format Strings, Ledger for Developers, Value Expressions, Top @chapter Format Strings @menu @@ -6103,9 +6926,240 @@ Additional date format parameters which can be used : @option{%F} yields @code{%Y-%m-%d 2010-02-10} +@node Ledger for Developers, Extending with Python, Format Strings, Top +@chapter Ledger for Developers + +@menu +* Internal Design:: +* Journal File Format:: +@end menu + +@node Internal Design, Journal File Format, 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) + + An 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. -@node Journal File Format, Extending with Python, Format Strings, Top -@chapter Journal File Format for Developers +@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 amount_t by a 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 there own, but are purely translated from the input string + (cash) down to the corresponding value expression (account =~ /cash/). + This is a convenience layer. + +@item Format strings + + Format strings let you interpolate value expressions into string, 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 "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 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 journal_t object. This is the go-to object for querying ad reporting + on your data. + +@item Textual journal parser + + There is a textual parser, wholly contained in 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 + iterators.h and 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 --sort. + +@item Temporaries + + Many reports bring pseudo-journal objects into existence, like postings + which report totals in a "<Total>" account. These objects are created and + managed by a 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 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 filters.h, I see 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 chain.cc. It took a really long time to get + this logic exactly write, 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 call output modules. They are + implemented just like a regular filter, but they don't have a "next" filter + to pass the time 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 --debug. +@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, , 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, @@ -6155,26 +7209,20 @@ amount of the first posting is typically positive. Consider: @menu * Comments and meta-data:: * Specifying Amounts:: +* Posting costs:: +* Primary commodities:: @end menu @node Comments and meta-data, Specifying Amounts, Journal File Format, Journal File Format -@section Comments and meta-data -@menu -* Comments:: -* Meta-data:: -@end menu +@subsection Comments and meta-data -@node Comments, Meta-data, Comments and meta-data, Comments and meta-data -@subsection Comments Comments are generally started using a ';'. 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: @code{#}, @code{|}, and @code{*}. -@node Meta-data, , Comments, Comments and meta-data -@subsection Matador -@node Specifying Amounts, , Comments and meta-data, Journal File Format -@section Specifying Amounts +@node Specifying Amounts, Posting costs, Comments and meta-data, Journal File Format +@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 @@ -6191,7 +7239,7 @@ spaces between the end of the post and the beginning of the amount @end menu @node Integer Amounts, Commoditized Amounts, Specifying Amounts, Specifying Amounts -@subsection Integer Amounts +@subsubsection Integer Amounts In the simplest form, bare decimal numbers are accepted: @@ -6245,7 +7293,7 @@ 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 -@subsection Commoditized 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 @@ -6292,7 +7340,8 @@ does not change how other amounts in that commodity will be displayed. An example of this is found in cost expressions, covered next. -@section Posting costs +@node Posting costs, Primary commodities, Specifying Amounts, Journal File Format +@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 @@ -6342,6 +7391,7 @@ postings are involved: 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 @subsection Primary commodities In every transaction involving more than one commodity, there is @@ -6374,8 +7424,161 @@ considered a primary. In fact, when Ledger goes about ensures that all transactions balance to zero, it only ever asks this of primary commodities. -@node Extending with Python, Major Changes from version 2.6, Journal File Format, Top +@node Extending with Python, Major Changes from version 2.6, Ledger for Developers, 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 vs. Cooked:: +* Queries:: +* Embedded Python:: +* Amounts:: +@end menu + +@node Basic data traversal, Raw vs. 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 Commodity's +that Amount's refer to. + +The make a Session useful, you must read a Journal into it, using the function +`@samp{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 Transaction's, Posting's, and other objects +related to your data. There are also AutomatedTransaction's and +PeriodicTransaction's, 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 vs. Cooked, Queries, Basic data traversal, Extending with Python +@section Raw vs. 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 + = 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 + 2012-03-01 KFC + Expenses:Food $100 + Assets:Credit +@end smallexample + +While the @emph{cooked} form is: + +@smallexample + 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 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 "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 vs. Cooked, Extending with Python +@section Queries + +The Journal.query() method accepts every argument you can specify on the +command-line, including --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 + +Can you embed Python into your data files using the 'python' directive: + +@smallexample + python + import so + 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 Major Changes from version 2.6, Example Data File, Extending with Python, Top @chapter Major Changes from version 2.6 diff --git a/lib/Makefile b/lib/Makefile index d7b1071b..cb05e44d 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -5,8 +5,8 @@ STOW_ROOT = /usr/local/stow PRODUCTS = $(HOME)/Products -GCC_VERSION = 4.6 -BOOST_VERSION = 1_48_0 +GCC_VERSION = 4.7 +BOOST_VERSION = 1_49_0 CC = gcc-mp-$(GCC_VERSION) ifeq ($(CC),clang) @@ -20,22 +20,20 @@ LD = gcc-mp-$(GCC_VERSION) DIR_SUFFIX = gcc$(subst .,,$(GCC_VERSION)) OPTJ = #-j8 endif -#CPPFLAGS = -D_GLIBCXX_FULLY_DYNAMIC_STRING=1 -#ifneq ($(CC),clang) -#CPPFLAGS += -D_GLIBCXX_DEBUG=1 -#endif CFLAGS = $(CPPFLAGS) -g2 -ggdb LDFLAGS = -g2 -ggdb BOOST_SOURCE = boost-release -#BOOST_DEFINES = define=_GLIBCXX_FULLY_DYNAMIC_STRING=1 +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 -#BOOST_DEFINES += define=_GLIBCXX_DEBUG=1 endif -#BOOST_FLAGS = --architecture=x86 --address_model=32_64 BOOST_FLAGS = toolset=$(BOOST_TOOLSET) --layout=versioned \ link=shared threading=single $(BOOST_DEFINES) BOOST_DIR = boost_$(BOOST_VERSION)-$(DIR_SUFFIX) @@ -55,7 +53,7 @@ prepare-boost: boost-build: prepare-boost (cd $(BOOST_SOURCE) && \ sh bootstrap.sh && \ - ./b2 $(OPTJ) debug --prefix=$(BOOST_STOW) \ + ./b2 $(OPTJ) debug release --prefix=$(BOOST_STOW) \ --build-dir=$(BOOST_BUILD) $(BOOST_FLAGS) install) icu-build: diff --git a/lib/build.sh b/lib/build.sh new file mode 100755 index 00000000..28408d73 --- /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 <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 CXX=clang++ LD=clang++ CC=clang OPTJ=-j20 \ + BOOST_TOOLSET=clang-darwin DIR_SUFFIX=clang31 \ + BOOST_DEFINES="-sICU_PATH=/usr/local cxxflags=\"-g -std=c++11 -nostdlibinc -I/usr/local/include -I/usr/local/include/c++/v1 -I/opt/local/include -I/usr/include -stdlib=libc++\" linkflags=\"-g -Z -L/usr/local/lib -L/opt/local/lib -L/usr/lib /usr/local/lib/libc++.dylib -stdlib=libc++\"" diff --git a/lisp/ldg-mode.el b/lisp/ldg-mode.el index cc9e8d92..4d13d7d2 100644 --- a/lisp/ldg-mode.el +++ b/lisp/ldg-mode.el @@ -47,6 +47,7 @@ (define-key map [(control ?c) (control ?e)] 'ledger-toggle-current-entry) (define-key map [(control ?c) (control ?r)] 'ledger-reconcile) (define-key map [(control ?c) (control ?s)] 'ledger-sort) + (define-key map [(control ?c) (control ?t)] 'ledger-test-run) (define-key map [tab] 'pcomplete) (define-key map [(control ?i)] 'pcomplete) (define-key map [(control ?c) tab] 'ledger-fully-complete-entry) diff --git a/lisp/ldg-post.el b/lisp/ldg-post.el index da4a2806..05b9d352 100644 --- a/lisp/ldg-post.el +++ b/lisp/ldg-post.el @@ -4,7 +4,7 @@ "" :group 'ledger) -(defcustom ledger-post-auto-adjust-amounts t +(defcustom ledger-post-auto-adjust-amounts nil "If non-nil, ." :type 'boolean :group 'ledger-post) diff --git a/lisp/ldg-test.el b/lisp/ldg-test.el new file mode 100644 index 00000000..478c62d8 --- /dev/null +++ b/lisp/ldg-test.el @@ -0,0 +1,75 @@ +(defcustom ledger-source-directory "~/src/ledger" + "Directory where the Ledger sources are located." + :type 'directory + :group 'ledger) + +(defcustom ledger-test-binary "~/Products/ledger/debug/ledger" + "Directory where the Ledger sources are located." + :type 'file + :group 'ledger) + +(defun ledger-test-org-narrow-to-entry () + (outline-back-to-heading) + (narrow-to-region (point) (progn (outline-next-heading) (point))) + (goto-char (point-min))) + +(defun ledger-test-create () + (interactive) + (let ((uuid (org-entry-get (point) "ID"))) + (when (string-match "\\`\\([^-]+\\)-" uuid) + (let ((prefix (match-string 1 uuid)) + input output) + (save-restriction + (ledger-test-org-narrow-to-entry) + (goto-char (point-min)) + (while (re-search-forward "#\\+begin_src ledger" nil t) + (goto-char (match-end 0)) + (forward-line 1) + (let ((beg (point))) + (re-search-forward "#\\+end_src") + (setq input + (concat (or input "") + (buffer-substring beg (match-beginning 0)))))) + (goto-char (point-min)) + (while (re-search-forward ":OUTPUT:" nil t) + (goto-char (match-end 0)) + (forward-line 1) + (let ((beg (point))) + (re-search-forward ":END:") + (setq output + (concat (or output "") + (buffer-substring beg (match-beginning 0))))))) + (find-file-other-window + (expand-file-name (concat prefix ".test") + (expand-file-name "test/regress" + ledger-source-directory))) + (ledger-mode) + (if input + (insert input) + (insert "2012-03-17 Payee\n") + (insert " Expenses:Food $20\n") + (insert " Assets:Cash\n")) + (insert "\ntest reg\n") + (if output + (insert output)) + (insert "end test\n"))))) + +(defun ledger-test-run () + (interactive) + (save-excursion + (goto-char (point-min)) + (when (re-search-forward "^test \\(.+?\\)\\( ->.*\\)?$" nil t) + (let ((command (expand-file-name ledger-test-binary)) + (args (format "--args-only --columns=80 --no-color -f \"%s\" %s" + buffer-file-name (match-string 1)))) + (setq args (replace-regexp-in-string "\\$sourcepath" + ledger-source-directory args)) + (kill-new args) + (message "Testing: ledger %s" args) + (let ((prev-directory default-directory)) + (cd ledger-source-directory) + (unwind-protect + (async-shell-command (format "\"%s\" %s" command args)) + (cd prev-directory))))))) + +(provide 'ldg-test) diff --git a/lisp/ledger.el b/lisp/ledger.el index 1c8aade3..4fc21d6a 100644 --- a/lisp/ledger.el +++ b/lisp/ledger.el @@ -128,12 +128,12 @@ text that should replace the format specifier." (defvar bold 'bold) (defvar ledger-font-lock-keywords - '(("\\( \\| \\|^\\)\\(;.*\\)" 2 font-lock-comment-face) + '(("\\( \\| \\|^\\)\\(;.*\\)" 2 font-lock-comment-face) ("^[0-9]+[-/.=][-/.=0-9]+\\s-+\\(([^)]+)\\s-+\\)?\\([^*].+?\\)\\(\\( ;\\| ;\\|$\\)\\)" 2 bold) ;;("^[0-9]+[-/.=][-/.=0-9]+\\s-+\\(([^)]+)\\s-+\\)?\\([*].+?\\)\\(\\( ;\\| ;\\|$\\)\\)" ;; 2 font-lock-type-face) ("^\\s-+\\([*]\\s-*\\)?\\(\\([[(]\\)?\\([^*;]\\)+?\\(:\\|\\s-\\)[^]); - ]+?\\([])]\\)?\\)\\( \\| \\|$\\)" + ]+?\\([])]\\)?\\)\\( \\| \\|$\\)" 2 font-lock-keyword-face) ("^\\([~=].+\\)" 1 font-lock-function-name-face) ("^\\([A-Za-z]+ .+\\)" 1 font-lock-function-name-face)) @@ -545,7 +545,7 @@ dropped." (defun ledger-reconcile-visit () (interactive) (let ((where (get-text-property (point) 'where))) - (when (or (equal (car where) "<stdin>") (equal (car where) "/dev/stdin")) + (when (markerp (cdr where)) (switch-to-buffer-other-window ledger-buf) (goto-char (cdr where))))) diff --git a/src/account.cc b/src/account.cc index e201be64..206e2350 100644 --- a/src/account.cc +++ b/src/account.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -89,9 +89,13 @@ account_t * account_t::find_account(const string& acct_name, if (has_flags(ACCOUNT_GENERATED)) account->add_flags(ACCOUNT_GENERATED); - std::pair<accounts_map::iterator, bool> result - = accounts.insert(accounts_map::value_type(first, account)); +#if defined(DEBUG_ON) + std::pair<accounts_map::iterator, bool> result = +#endif + accounts.insert(accounts_map::value_type(first, account)); +#if defined(DEBUG_ON) assert(result.second); +#endif } else { account = (*i).second; } @@ -137,7 +141,10 @@ void account_t::add_post(post_t * post) bool account_t::remove_post(post_t * post) { - assert(! posts.empty()); + // 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; @@ -243,6 +250,10 @@ namespace { 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; } @@ -279,6 +290,26 @@ namespace { 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); + } + template <value_t (*Func)(account_t&)> value_t get_wrapper(call_scope_t& args) { return (*Func)(args.context<account_t>()); @@ -351,6 +382,13 @@ expr_t::ptr_op_t account_t::lookup(const symbol_t::kind_t kind, 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>); @@ -359,15 +397,21 @@ expr_t::ptr_op_t account_t::lookup(const symbol_t::kind_t kind, break; case 'l': - if (fn_name == "latest_cleared") - return WRAP_FUNCTOR(get_wrapper<&get_latest_cleared>); - else if (fn_name[1] == '\0') + 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>); 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': @@ -613,6 +657,14 @@ void account_t::xdata_t::details_t::update(post_t& post, 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; + if (post.state() == item_t::CLEARED) { posts_cleared_count++; diff --git a/src/account.h b/src/account.h index 7a632b35..c0e3e1f7 100644 --- a/src/account.h +++ b/src/account.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -67,15 +67,23 @@ public: unsigned short depth; accounts_map accounts; posts_list posts; + optional<expr_t> value_expr; mutable string _fullname; +#ifdef DOCUMENT_MODEL + mutable void * data; +#endif account_t(account_t * _parent = NULL, const string& _name = "", const optional<string>& _note = none) : supports_flags<>(), scope_t(), parent(_parent), name(_name), note(_note), - depth(static_cast<unsigned short>(parent ? parent->depth + 1 : 0)) { + depth(static_cast<unsigned short>(parent ? parent->depth + 1 : 0)) +#ifdef DOCUMENT_MODEL + , data(NULL) +#endif + { TRACE_CTOR(account_t, "account_t *, const string&, const string&"); } account_t(const account_t& other) @@ -84,10 +92,14 @@ public: name(other.name), note(other.note), depth(other.depth), - accounts(other.accounts) { + accounts(other.accounts) +#ifdef DOCUMENT_MODEL + , data(NULL) +#endif + { TRACE_CTOR(account_t, "copy"); } - ~account_t(); + virtual ~account_t(); virtual string description() { return string(_("account ")) + fullname(); @@ -169,6 +181,9 @@ public: date_t latest_post; date_t latest_cleared_post; + datetime_t earliest_checkin; + datetime_t latest_checkout; + std::set<path> filenames; std::set<string> accounts_referenced; std::set<string> payees_referenced; @@ -185,7 +200,26 @@ public: posts_cleared_count(0), posts_last_7_count(0), posts_last_30_count(0), - posts_this_month_count(0) {} + posts_this_month_count(0) { + 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) + { + 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); diff --git a/src/accum.cc b/src/accum.cc index 8f3d5185..3add051b 100644 --- a/src/accum.cc +++ b/src/accum.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -35,6 +35,9 @@ namespace ledger { +straccstream _accum; +std::ostringstream _accum_buffer; + std::streamsize straccbuf::xsputn(const char * s, std::streamsize num) { if (index == 0) { diff --git a/src/accum.h b/src/accum.h index 236a7714..628a6b36 100644 --- a/src/accum.h +++ b/src/accum.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, John Wiegley. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -51,7 +51,12 @@ protected: std::string::size_type index; public: - straccbuf() : index(0) {} + straccbuf() : index(0) { + TRACE_CTOR(straccbuf, ""); + } + ~straccbuf() throw() { + TRACE_DTOR(straccbuf); + } protected: virtual std::streamsize xsputn(const char * s, std::streamsize num); @@ -66,8 +71,12 @@ protected: public: straccstream() : std::ostream(0) { + TRACE_CTOR(straccstream, ""); rdbuf(&buf); } + ~straccstream() throw() { + TRACE_DTOR(straccstream); + } void clear() { std::ostream::clear(); @@ -83,6 +92,20 @@ public: #define ACCUM(obj) (static_cast<const straccstream&>(obj).str()) +extern straccstream _accum; +extern std::ostringstream _accum_buffer; + +inline string str_helper_func() { + string buf = _accum_buffer.str(); + _accum_buffer.clear(); + _accum_buffer.str(""); + return buf; +} + +#define STR(msg) \ + ((_accum_buffer << ACCUM(_accum << msg)), \ + _accum.clear(), str_helper_func()) + } // namespace ledger #endif // _ACCUM_H diff --git a/src/amount.cc b/src/amount.cc index 85afc3d8..5e933215 100644 --- a/src/amount.cc +++ b/src/amount.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -63,16 +63,16 @@ struct amount_t::bigint_t : public supports_flags<> #define MP(bigint) ((bigint)->val) bigint_t() : prec(0), refc(1) { - TRACE_CTOR(bigint_t, ""); mpq_init(val); + TRACE_CTOR(bigint_t, ""); } bigint_t(const bigint_t& other) : supports_flags<>(static_cast<uint_least8_t> (other.flags() & ~BIGINT_BULK_ALLOC)), prec(other.prec), refc(1) { - TRACE_CTOR(bigint_t, "copy"); mpq_init(val); mpq_set(val, other.val); + TRACE_CTOR(bigint_t, "copy"); } ~bigint_t() { TRACE_DTOR(bigint_t); @@ -120,11 +120,13 @@ namespace { { char * buf = NULL; try { +#if defined(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. @@ -347,24 +349,24 @@ void amount_t::_release() amount_t::amount_t(const double val) : commodity_(NULL) { - TRACE_CTOR(amount_t, "const double"); 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) { - TRACE_CTOR(amount_t, "const unsigned long"); 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) { - TRACE_CTOR(amount_t, "const long"); quantity = new bigint_t; mpq_set_si(MP(quantity), val, 1); + TRACE_CTOR(amount_t, "const long"); } @@ -393,11 +395,11 @@ int amount_t::compare(const amount_t& amt) const throw_(amount_error, _("Cannot compare two uninitialized amounts")); } - if (has_commodity() && amt.has_commodity() && - commodity() != amt.commodity()) + if (has_commodity() && amt.has_commodity() && commodity() != amt.commodity()) { throw_(amount_error, - _("Cannot compare amounts with different commodities: %1 and %2") - << commodity().symbol() << amt.commodity().symbol()); + _("Cannot compare amounts with different commodities: '%1' and '%2'") + << commodity() << amt.commodity()); + } return mpq_cmp(MP(quantity), MP(amt.quantity)); } @@ -428,12 +430,11 @@ amount_t& amount_t::operator+=(const amount_t& amt) throw_(amount_error, _("Cannot add two uninitialized amounts")); } - if (has_commodity() && amt.has_commodity() && - commodity() != amt.commodity()) + if (has_commodity() && amt.has_commodity() && commodity() != amt.commodity()) { throw_(amount_error, - _("Adding amounts with different commodities: %1 != %2") - << (has_commodity() ? commodity().symbol() : _("NONE")) - << (amt.has_commodity() ? amt.commodity().symbol() : _("NONE"))); + _("Adding amounts with different commodities: '%1' != '%2'") + << commodity() << amt.commodity()); + } _dup(); @@ -459,12 +460,11 @@ amount_t& amount_t::operator-=(const amount_t& amt) throw_(amount_error, _("Cannot subtract two uninitialized amounts")); } - if (has_commodity() && amt.has_commodity() && - commodity() != amt.commodity()) + if (has_commodity() && amt.has_commodity() && commodity() != amt.commodity()) { throw_(amount_error, - _("Subtracting amounts with different commodities: %1 != %2") - << (has_commodity() ? commodity().symbol() : _("NONE")) - << (amt.has_commodity() ? amt.commodity().symbol() : _("NONE"))); + _("Subtracting amounts with different commodities: '%1' != '%2'") + << commodity() << amt.commodity()); + } _dup(); @@ -605,16 +605,13 @@ void amount_t::in_place_negate() } } -amount_t amount_t::inverted() const +void amount_t::in_place_invert() { if (! quantity) throw_(amount_error, _("Cannot invert an uninitialized amount")); - amount_t t(*this); - t._dup(); - mpq_inv(MP(t.quantity), MP(t.quantity)); - - return t; + _dup(); + mpq_inv(MP(quantity), MP(quantity)); } void amount_t::in_place_round() @@ -729,24 +726,24 @@ void amount_t::in_place_unreduce() } optional<amount_t> -amount_t::value(const optional<datetime_t>& moment, - const optional<commodity_t&>& in_terms_of) const +amount_t::value(const datetime_t& moment, + const commodity_t * in_terms_of) const { if (quantity) { #if defined(DEBUG_ON) - DEBUG("commodity.prices.find", + DEBUG("commodity.price.find", "amount_t::value of " << commodity().symbol()); - if (moment) - DEBUG("commodity.prices.find", - "amount_t::value: moment = " << *moment); + if (! moment.is_not_a_date_time()) + DEBUG("commodity.price.find", + "amount_t::value: moment = " << moment); if (in_terms_of) - DEBUG("commodity.prices.find", + 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<price_point_t> point; - optional<commodity_t&> comm(in_terms_of); + const commodity_t * comm(in_terms_of); if (has_annotation() && annotation().price) { if (annotation().has_flags(ANNOTATION_PRICE_FIXATED)) { @@ -756,14 +753,14 @@ amount_t::value(const optional<datetime_t>& moment, "amount_t::value: fixated price = " << point->price); } else if (! comm) { - comm = annotation().price->commodity(); + comm = annotation().price->commodity_ptr(); } } - if (! point) { - if (comm && commodity().referent() == comm->referent()) - return *this; + 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 @@ -788,7 +785,7 @@ amount_t::value(const optional<datetime_t>& moment, return none; } -amount_t amount_t::price() const +optional<amount_t> amount_t::price() const { if (has_annotation() && annotation().price) { amount_t tmp(*annotation().price); @@ -796,7 +793,7 @@ amount_t amount_t::price() const DEBUG("amount.price", "Returning price of " << *this << " = " << tmp); return tmp; } - return *this; + return none; } @@ -868,10 +865,10 @@ bool amount_t::fits_in_long() const return mpfr_fits_slong_p(tempf, GMP_RNDN); } -commodity_t& amount_t::commodity() const +commodity_t * amount_t::commodity_ptr() const { - return (has_commodity() ? - *commodity_ : *commodity_pool_t::current_pool->null_commodity); + return (commodity_ ? + commodity_ : commodity_pool_t::current_pool->null_commodity); } bool amount_t::has_commodity() const @@ -1030,12 +1027,12 @@ bool amount_t::parse(std::istream& in, const parse_flags_t& flags) } // Allocate memory for the amount's quantity value. We have to - // monitor the allocation in an auto_ptr because this function gets + // 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. - std::auto_ptr<bigint_t> new_quantity; + unique_ptr<bigint_t> new_quantity; if (quantity) { if (quantity->refc > 1) @@ -1061,10 +1058,6 @@ bool amount_t::parse(std::istream& in, const parse_flags_t& flags) if (! commodity_) commodity_ = commodity_pool_t::current_pool->create(symbol); assert(commodity_); - - if (details) - commodity_ = - commodity_pool_t::current_pool->find_or_create(*commodity_, details); } // Quickly scan through and verify the correctness of the amount's use of @@ -1200,6 +1193,14 @@ bool amount_t::parse(std::istream& in, const parse_flags_t& flags) 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; diff --git a/src/amount.h b/src/amount.h index f7e877a7..cd77a79a 100644 --- a/src/amount.h +++ b/src/amount.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, John Wiegley. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -155,17 +155,17 @@ public: commodity_t::null_commodity. The number may be of infinite precision. */ explicit amount_t(const string& val) : quantity(NULL) { - TRACE_CTOR(amount_t, "const string&"); 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) { - TRACE_CTOR(amount_t, "const char *"); assert(val); parse(val); + TRACE_CTOR(amount_t, "const char *"); } /*@}*/ @@ -195,21 +195,21 @@ public: 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) { - TRACE_CTOR(amount_t, "copy"); 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) { - TRACE_CTOR(amount_t, "const amount_t&, const annotation_t&"); 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 @@ -327,7 +327,12 @@ public: return *this; } - amount_t inverted() const; + 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 @@ -399,10 +404,10 @@ public: $100.00. */ optional<amount_t> - value(const optional<datetime_t>& moment = none, - const optional<commodity_t&>& in_terms_of = none) const; + value(const datetime_t& moment = datetime_t(), + const commodity_t * in_terms_of = NULL) const; - amount_t price() const; + optional<amount_t> price() const; /*@}*/ @@ -528,7 +533,10 @@ public: number() returns a commodity-less version of an amount. This is useful for accessing just the numeric portion of an amount. */ - commodity_t& commodity() const; + commodity_t * commodity_ptr() const; + commodity_t& commodity() const { + return *commodity_ptr(); + } bool has_commodity() const; void set_commodity(commodity_t& comm) { @@ -536,6 +544,15 @@ public: *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<commodity_t&>(comm)); + return tmp; + } + } void clear_commodity() { commodity_ = NULL; } diff --git a/src/annotate.cc b/src/annotate.cc index 8ba46f4f..41e7a752 100644 --- a/src/annotate.cc +++ b/src/annotate.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -33,11 +33,51 @@ #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 { @@ -52,6 +92,12 @@ void annotation_t::parse(std::istream& in) throw_(amount_error, _("Commodity specifies more than one price")); in.get(c); + c = static_cast<char>(in.peek()); + if (c == '{') { + in.get(c); + add_flags(ANNOTATION_PRICE_NOT_PER_UNIT); + } + c = peek_next_nonws(in); if (c == '=') { in.get(c); @@ -59,10 +105,18 @@ void annotation_t::parse(std::istream& in) } READ_INTO(in, buf, 255, c, c != '}'); - if (c == '}') + if (c == '}') { in.get(c); - else - throw_(amount_error, _("Commodity price lacks closing brace")); + if (has_flags(ANNOTATION_PRICE_NOT_PER_UNIT)) { + c = static_cast<char>(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); @@ -84,17 +138,46 @@ void annotation_t::parse(std::istream& in) date = parse_date(buf); } else if (c == '(') { - if (tag) - throw_(amount_error, _("Commodity specifies more than one tag")); - in.get(c); - READ_INTO(in, buf, 255, c, c != ')'); - if (c == ')') - in.get(c); - else - throw_(amount_error, _("Commodity tag lacks closing parenthesis")); + c = static_cast<char>(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")); - tag = buf; + in.get(c); + READ_INTO(in, buf, 255, c, c != ')'); + if (c == ')') { + in.get(c); + c = static_cast<char>(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(); @@ -128,6 +211,9 @@ void annotation_t::print(std::ostream& out, bool keep_base, if (tag && (! no_computed_annotations || ! has_flags(ANNOTATION_TAG_CALCULATED))) out << " (" << *tag << ')'; + + if (value_expr && ! has_flags(ANNOTATION_VALUE_EXPR_CALCULATED)) + out << " ((" << *value_expr << "))"; } bool keep_details_t::keep_all(const commodity_t& comm) const @@ -157,6 +243,54 @@ bool annotated_commodity_t::operator==(const commodity_t& comm) const return true; } +optional<price_point_t> +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 defined(DEBUG_ON) + if (target) + DEBUG("commodity.price.find", "target commodity: " << target->symbol()); +#endif + + if (details.value_expr) + return find_price_from_expr(const_cast<expr_t&>(*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) { @@ -192,8 +326,7 @@ annotated_commodity_t::strip_annotations(const keep_details_t& what_to_keep) if ((keep_price && details.price) || (keep_date && details.date) || - (keep_tag && details.tag)) - { + (keep_tag && details.tag)) { new_comm = pool().find_or_create (referent(), annotation_t(keep_price ? details.price : none, keep_date ? details.date : none, diff --git a/src/annotate.h b/src/annotate.h index b590ca45..37ee0685 100644 --- a/src/annotate.h +++ b/src/annotate.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -46,29 +46,39 @@ #ifndef _ANNOTATE_H #define _ANNOTATE_H +#include "expr.h" + namespace ledger { struct annotation_t : public supports_flags<>, public equality_comparable<annotation_t> { -#define ANNOTATION_PRICE_CALCULATED 0x01 -#define ANNOTATION_PRICE_FIXATED 0x02 -#define ANNOTATION_DATE_CALCULATED 0x04 -#define ANNOTATION_TAG_CALCULATED 0x08 +#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<amount_t> price; optional<date_t> date; optional<string> tag; - - explicit annotation_t(const optional<amount_t>& _price = none, - const optional<date_t>& _date = none, - const optional<string>& _tag = none) - : supports_flags<>(), price(_price), date(_date), tag(_tag) { - TRACE_CTOR(annotation_t, "const optional<amount_t>& + date_t + string"); + optional<expr_t> value_expr; + + explicit annotation_t(const optional<amount_t>& _price = none, + const optional<date_t>& _date = none, + const optional<string>& _tag = none, + const optional<expr_t>& _value_expr = none) + : supports_flags<>(), price(_price), date(_date), tag(_tag), + value_expr(_value_expr) { + TRACE_CTOR(annotation_t, + "optional<amount_t> + date_t + string + expr_t"); } annotation_t(const annotation_t& other) : supports_flags<>(other.flags()), - price(other.price), date(other.date), tag(other.tag) { + price(other.price), date(other.date), tag(other.tag), + value_expr(other.value_expr) + { TRACE_CTOR(annotation_t, "copy"); } ~annotation_t() { @@ -76,17 +86,20 @@ struct annotation_t : public supports_flags<>, } operator bool() const { - return price || date || tag; + 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); + 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; @@ -132,6 +145,12 @@ inline void to_xml(std::ostream& out, const annotation_t& details) push_xml y(out, "tag"); out << y.guard(*details.tag); } + + if (details.value_expr) + { + push_xml y(out, "value-expr"); + out << y.guard(details.value_expr->text()); + } } struct keep_details_t @@ -207,8 +226,9 @@ protected: explicit annotated_commodity_t(commodity_t * _ptr, const annotation_t& _details) : commodity_t(_ptr->parent_, _ptr->base), ptr(_ptr), details(_details) { - TRACE_CTOR(annotated_commodity_t, "commodity_t *, annotation_t"); annotated = true; + qualified_symbol = _ptr->qualified_symbol; + TRACE_CTOR(annotated_commodity_t, "commodity_t *, annotation_t"); } public: @@ -230,7 +250,31 @@ public: return *ptr; } + virtual optional<expr_t> value_expr() const { + if (details.value_expr) + return details.value_expr; + return commodity_t::value_expr(); + } + + optional<price_point_t> + 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; diff --git a/src/archive.cc b/src/archive.cc index 28760512..72ec0419 100644 --- a/src/archive.cc +++ b/src/archive.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/archive.h b/src/archive.h index 1ebf3496..4ce5e0e7 100644 --- a/src/archive.h +++ b/src/archive.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/balance.cc b/src/balance.cc index 7ce9d994..ded3d38a 100644 --- a/src/balance.cc +++ b/src/balance.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -41,23 +41,23 @@ namespace ledger { balance_t::balance_t(const double val) { - TRACE_CTOR(balance_t, "const double"); 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) { - TRACE_CTOR(balance_t, "const unsigned long"); 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) { - TRACE_CTOR(balance_t, "const long"); 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) @@ -185,8 +185,8 @@ balance_t& balance_t::operator/=(const amount_t& amt) } optional<balance_t> -balance_t::value(const optional<datetime_t>& moment, - const optional<commodity_t&>& in_terms_of) const +balance_t::value(const datetime_t& moment, + const commodity_t * in_terms_of) const { balance_t temp; bool resolved = false; @@ -202,16 +202,6 @@ balance_t::value(const optional<datetime_t>& moment, return resolved ? temp : optional<balance_t>(); } -balance_t balance_t::price() const -{ - balance_t temp; - - foreach (const amounts_map::value_type& pair, amounts) - temp += pair.second.price(); - - return temp; -} - optional<amount_t> balance_t::commodity_amount(const optional<const commodity_t&>& commodity) const { @@ -250,51 +240,100 @@ balance_t::strip_annotations(const keep_details_t& what_to_keep) const return temp; } -void balance_t::print(std::ostream& out, - const int first_width, - const int latter_width, - const uint_least8_t flags) const +void balance_t::map_sorted_amounts(function<void(const amount_t&)> fn) const { - bool first = true; - int lwidth = latter_width; + if (! amounts.empty()) { + if (amounts.size() == 1) { + const amount_t& amount((*amounts.begin()).second); + if (amount) + fn(amount); + } + else { + typedef std::vector<const amount_t *> amounts_array; + amounts_array sorted; - if (lwidth == -1) - lwidth = first_width; + foreach (const amounts_map::value_type& pair, amounts) + if (pair.second) + sorted.push_back(&pair.second); - typedef std::vector<const amount_t *> amounts_array; - amounts_array sorted; + std::stable_sort(sorted.begin(), sorted.end(), + commodity_t::compare_by_commodity()); - foreach (const amounts_map::value_type& pair, amounts) - if (pair.second) - sorted.push_back(&pair.second); + foreach (const amount_t * amount, sorted) + fn(*amount); + } + } +} - std::stable_sort(sorted.begin(), sorted.end(), - commodity_t::compare_by_commodity()); +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); + } - foreach (const amount_t * amount, sorted) { - int width; - if (! first) { - out << std::endl; - width = lwidth; - } else { - first = false; - width = first_width; + 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); } - 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; + } + }; +} - if (first) { - out.width(first_width); - 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 to_xml(std::ostream& out, const balance_t& bal) diff --git a/src/balance.h b/src/balance.h index ac22f3e7..704b4072 100644 --- a/src/balance.h +++ b/src/balance.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -108,26 +108,26 @@ public: TRACE_CTOR(balance_t, ""); } balance_t(const amount_t& amt) { - TRACE_CTOR(balance_t, "const amount_t&"); 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) { - TRACE_CTOR(balance_t, "const string&"); 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) { - TRACE_CTOR(balance_t, "const char *"); amount_t temp(val); amounts.insert(amounts_map::value_type(&temp.commodity(), temp)); + TRACE_CTOR(balance_t, "const char *"); } /** @@ -384,10 +384,8 @@ public: } optional<balance_t> - value(const optional<datetime_t>& moment = none, - const optional<commodity_t&>& in_terms_of = none) const; - - balance_t price() const; + 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: @@ -509,6 +507,14 @@ public: balance_t strip_annotations(const keep_details_t& what_to_keep) 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<void(const amount_t&)> 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 diff --git a/src/chain.cc b/src/chain.cc index 450e3758..52d52f14 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -88,10 +88,9 @@ post_handler_ptr chain_pre_post_handlers(post_handler_ptr base_handler, predicate_t(report.HANDLER(forecast_while_).str(), report.what_to_keep()), report, - report.HANDLED(forecast_years_) ? - static_cast<std::size_t> - (report.HANDLER(forecast_years_).value.to_long()) : - 5UL); + (report.HANDLED(forecast_years_) ? + lexical_cast<std::size_t> + (report.HANDLER(forecast_years_).value) : 5UL)); forecast_handler->add_period_xacts(report.session.journal->period_xacts); handler.reset(forecast_handler); @@ -115,10 +114,13 @@ post_handler_ptr chain_post_handlers(post_handler_ptr base_handler, predicate_t only_predicate; display_filter_posts * display_filter = NULL; - assert(report.HANDLED(amount_)); 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_)) { @@ -134,9 +136,9 @@ post_handler_ptr chain_post_handlers(post_handler_ptr base_handler, handler.reset (new truncate_xacts(handler, report.HANDLED(head_) ? - report.HANDLER(head_).value.to_int() : 0, + lexical_cast<int>(report.HANDLER(head_).value) : 0, report.HANDLED(tail_) ? - report.HANDLER(tail_).value.to_int() : 0)); + lexical_cast<int>(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 @@ -205,7 +207,7 @@ post_handler_ptr chain_post_handlers(post_handler_ptr base_handler, // 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, expr)); + handler.reset(new posts_as_equity(handler, report, expr)); else if (report.HANDLED(subtotal)) handler.reset(new subtotal_posts(handler, expr)); } @@ -217,13 +219,11 @@ post_handler_ptr chain_post_handlers(post_handler_ptr base_handler, // interval_posts groups posts together based on a time period, such as // weekly or monthly. - if (report.HANDLED(period_)) { + if (report.HANDLED(period_)) handler.reset(new interval_posts(handler, expr, report.HANDLER(period_).str(), report.HANDLED(exact), report.HANDLED(empty))); - handler.reset(new sort_posts(handler, "date")); - } if (report.HANDLED(date_)) handler.reset(new transfer_details(handler, transfer_details::SET_DATE, diff --git a/src/chain.h b/src/chain.h index 7bd76712..15ae12ba 100644 --- a/src/chain.h +++ b/src/chain.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -50,8 +50,9 @@ class post_t; class account_t; template <typename T> -struct item_handler : public noncopyable +class item_handler : public noncopyable { +protected: shared_ptr<item_handler> handler; public: diff --git a/src/commodity.cc b/src/commodity.cc index 5fd54d11..a72d85c8 100644 --- a/src/commodity.cc +++ b/src/commodity.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -35,430 +35,162 @@ #include "commodity.h" #include "annotate.h" #include "pool.h" +#include "scope.h" namespace ledger { bool commodity_t::decimal_comma_by_default = false; -void commodity_t::history_t::add_price(commodity_t& source, - const datetime_t& date, - const amount_t& price, - const bool reflexive) +void commodity_t::add_price(const datetime_t& date, const amount_t& price, + const bool reflexive) { - DEBUG("commodity.prices.add", "add_price to " << source - << (reflexive ? " (secondary)" : " (primary)") - << " : " << date << ", " << price); - - history_map::iterator i = prices.find(date); - if (i != prices.end()) { - (*i).second = price; - } else { - std::pair<history_map::iterator, bool> result - = prices.insert(history_map::value_type(date, price)); - assert(result.second); - } - if (reflexive) { - amount_t inverse = price.inverted(); - inverse.set_commodity(const_cast<commodity_t&>(source)); - price.commodity().add_price(date, inverse, false); + DEBUG("history.find", "Marking " + << price.commodity().symbol() << " as a primary commodity"); + price.commodity().add_flags(COMMODITY_PRIMARY); } else { - DEBUG("commodity.prices.add", - "marking commodity " << source.symbol() << " as primary"); - source.add_flags(COMMODITY_PRIMARY); + DEBUG("history.find", "Marking " << symbol() << " as a primary commodity"); + add_flags(COMMODITY_PRIMARY); } -} -bool commodity_t::history_t::remove_price(const datetime_t& date) -{ - DEBUG("commodity.prices.add", "remove_price: " << date); + DEBUG("history.find", "Adding price: " << symbol() + << " for " << price << " on " << date); - history_map::size_type n = prices.erase(date); - if (n > 0) - return true; - return false; + pool().commodity_price_history.add_price(referent(), date, price); + + base->price_map.clear(); // a price was added, invalid the map } -void commodity_t::varied_history_t:: - add_price(commodity_t& source, - const datetime_t& date, - const amount_t& price, - const bool reflexive) +void commodity_t::remove_price(const datetime_t& date, commodity_t& commodity) { - optional<history_t&> hist = history(price.commodity()); - if (! hist) { - std::pair<history_by_commodity_map::iterator, bool> result - = histories.insert(history_by_commodity_map::value_type - (&price.commodity(), history_t())); - assert(result.second); - - hist = (*result.first).second; - } - assert(hist); + pool().commodity_price_history.remove_price(referent(), commodity, date); - hist->add_price(source, date, price, reflexive); + DEBUG("history.find", "Removing price: " << symbol() << " on " << date); + + base->price_map.clear(); // a price was added, invalid the map } -bool commodity_t::varied_history_t::remove_price(const datetime_t& date, - commodity_t& comm) +void commodity_t::map_prices(function<void(datetime_t, const amount_t&)> fn, + const datetime_t& moment, + const datetime_t& _oldest, + bool bidirectionally) { - DEBUG("commodity.prices.add", "varied_remove_price: " << date << ", " << comm); + datetime_t when; + if (! moment.is_not_a_date_time()) + when = moment; + else if (epoch) + when = *epoch; + else + when = CURRENT_TIME(); - if (optional<history_t&> hist = history(comm)) - return hist->remove_price(date); - return false; + pool().commodity_price_history.map_prices(fn, referent(), when, _oldest, + bidirectionally); } optional<price_point_t> -commodity_t::history_t::find_price(const optional<datetime_t>& moment, - const optional<datetime_t>& oldest -#if defined(DEBUG_ON) - , const int indent -#endif - ) const +commodity_t::find_price_from_expr(expr_t& expr, const commodity_t * commodity, + const datetime_t& moment) const { - price_point_t point; - bool found = false; - -#if defined(DEBUG_ON) -#define DEBUG_INDENT(cat, indent) \ - do { \ - if (SHOW_DEBUG(cat)) \ - for (int _i = 0; _i < indent; _i++) \ - ledger::_log_buffer << " "; \ - } while (false) -#else -#define DEBUG_INDENT(cat, indent) -#endif - #if defined(DEBUG_ON) - DEBUG_INDENT("commodity.prices.find", indent); - if (moment) - DEBUG("commodity.prices.find", "find price nearest before or on: " << *moment); - else - DEBUG("commodity.prices.find", "find any price"); - - if (oldest) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "but no older than: " << *oldest); + 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 (prices.size() == 0) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "there are no prices in this history"); - return none; - } - - if (! moment) { - history_map::const_reverse_iterator r = prices.rbegin(); - point.when = (*r).first; - point.price = (*r).second; - found = true; + if (is_expr(result)) { + value_t call_args; - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "using most recent price"); - } else { - history_map::const_iterator i = prices.upper_bound(*moment); - if (i == prices.end()) { - history_map::const_reverse_iterator r = prices.rbegin(); - point.when = (*r).first; - point.price = (*r).second; - found = true; - - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "using last price"); - } else { - point.when = (*i).first; - if (*moment < point.when) { - if (i != prices.begin()) { - --i; - point.when = (*i).first; - point.price = (*i).second; - found = true; - } - } else { - point.price = (*i).second; - found = true; - } + call_args.push_back(string_value(base_symbol())); + call_args.push_back(moment); + if (commodity) + call_args.push_back(string_value(commodity->symbol())); - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "using found price"); - } + result = as_expr(result)->call(call_args, *scope_t::default_scope); } - if (! found) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "could not find a price"); - return none; - } - else if (moment && point.when > *moment) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "price is too young "); - return none; - } - else if (oldest && point.when < *oldest) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "price is too old "); - return none; - } - else { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", - "returning price: " << point.when << ", " << point.price); - return point; - } + return price_point_t(moment, result.to_amount()); } optional<price_point_t> -commodity_t::varied_history_t::find_price(const commodity_t& source, - const optional<commodity_t&>& commodity, - const optional<datetime_t>& moment, - const optional<datetime_t>& oldest -#if defined(DEBUG_ON) - , const int indent -#endif - ) const +commodity_t::find_price(const commodity_t * commodity, + const datetime_t& moment, + const datetime_t& oldest) const { - optional<price_point_t> point; - optional<datetime_t> limit = oldest; - -#if defined(VERIFY_ON) - if (commodity) { - VERIFY(source != *commodity); - VERIFY(! commodity->has_annotation()); - VERIFY(source.referent() != commodity->referent()); - } -#endif - -#if defined(DEBUG_ON) - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "varied_find_price for: " << source); + DEBUG("commodity.price.find", "commodity_t::find_price(" << symbol() << ")"); - DEBUG_INDENT("commodity.prices.find", indent); + const commodity_t * target = NULL; if (commodity) - DEBUG("commodity.prices.find", "looking for: commodity '" << *commodity << "'"); - else - DEBUG("commodity.prices.find", "looking for: any commodity"); - - if (moment) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "time index: " << *moment); - } - - if (oldest) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "only consider prices younger than: " << *oldest); - } -#endif - - // Either we couldn't find a history for the target commodity, or we - // couldn't find a price. In either case, search all histories known - // to this commodity for a price which we can calculate in terms of - // the goal commodity. - price_point_t best; - bool found = false; - - foreach (const history_by_commodity_map::value_type& hist, histories) { - commodity_t& comm(*hist.first); - if (comm == source) - continue; - - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", - "searching for price via commodity '" << comm << "'"); - - point = hist.second.find_price(moment, limit -#if defined(DEBUG_ON) - , indent + 2 -#endif - ); - assert(! point || point->price.commodity() == comm); - - if (point) { - optional<price_point_t> xlat; + target = commodity; + else if (pool().default_commodity) + target = &*pool().default_commodity; - if (commodity && comm != *commodity) { - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", "looking for translation price"); - - xlat = comm.find_price(commodity, moment, limit, true -#if defined(DEBUG_ON) - , indent + 2 -#endif - ); - if (xlat) { - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", "found translated price " - << xlat->price << " from " << xlat->when); - - point->price = xlat->price * point->price; - if (xlat->when < point->when) { - point->when = xlat->when; - - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", - "adjusting date of result back to " << point->when); - } - } else { - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", "saw no translated price there"); - continue; - } - } - - assert(! commodity || point->price.commodity() == *commodity); - - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", - "saw a price there: " << point->price << " from " << point->when); + if (target && this == target) + return none; - if (! limit || point->when > *limit) { - limit = point->when; - best = *point; - found = true; + base_t::memoized_price_entry entry(moment, oldest, + commodity ? commodity : NULL); - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", - "search limit adjusted to " << *limit); - } - } else { - DEBUG_INDENT("commodity.prices.find", indent + 1); - DEBUG("commodity.prices.find", "saw no price there"); + 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; } } - if (found) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.download", - "found price " << best.price << " from " << best.when); - return best; - } - return none; -} - -optional<commodity_t::history_t&> -commodity_t::varied_history_t::history(const optional<commodity_t&>& commodity) -{ - commodity_t * comm = NULL; - if (! commodity) { - if (histories.size() > 1) - return none; - comm = (*histories.begin()).first; - } else { - comm = &(*commodity); + 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<price_point_t> + 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()); } - history_by_commodity_map::iterator i = histories.find(comm); - if (i != histories.end()) - return (*i).second; - - return none; -} - -optional<price_point_t> -commodity_t::find_price(const optional<commodity_t&>& commodity, - const optional<datetime_t>& moment, - const optional<datetime_t>& oldest, - const bool nested -#if defined(DEBUG_ON) - , const int indent -#endif - ) const -{ - if (! has_flags(COMMODITY_WALKED) && base->varied_history) { - optional<base_t::time_and_commodity_t> pair; -#if defined(VERIFY_ON) - optional<price_point_t> checkpoint; - bool found = false; -#endif - - if (! nested) { - pair = base_t::time_and_commodity_t - (base_t::optional_time_pair_t(moment, oldest), - commodity ? &(*commodity) : NULL); - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "looking for memoized args: " - << (moment ? format_datetime(*moment) : "NONE") << ", " - << (oldest ? format_datetime(*oldest) : "NONE") << ", " - << (commodity ? commodity->symbol() : "NONE")); - - base_t::memoized_price_map::iterator i = base->price_map.find(*pair); - if (i != base->price_map.end()) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "found! returning: " - << ((*i).second ? (*i).second->price : amount_t(0L))); -#if defined(VERIFY_ON) - IF_VERIFY() { - found = true; - checkpoint = (*i).second; - } else -#endif // defined(VERIFY_ON) - return (*i).second; - } - } - - optional<price_point_t> point; - - const_cast<commodity_t&>(*this).add_flags(COMMODITY_WALKED); - try { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", "manually finding price..."); + DEBUG("history.find", + "remembered: " << (point ? point->price : amount_t(0L))); + base->price_map.insert(base_t::memoized_price_map::value_type(entry, point)); - point = base->varied_history->find_price(*this, commodity, - moment, oldest -#if defined(DEBUG_ON) - , indent -#endif - ); - } - catch (...) { - const_cast<commodity_t&>(*this).drop_flags(COMMODITY_WALKED); - throw; - } - const_cast<commodity_t&>(*this).drop_flags(COMMODITY_WALKED); - -#if defined(VERIFY_ON) - if (DO_VERIFY() && found) { - VERIFY(checkpoint == point); - return checkpoint; - } -#endif // defined(VERIFY_ON) - - if (! nested && pair) { - if (base->price_map.size() > base_t::max_price_map_size) { - DEBUG_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.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_INDENT("commodity.prices.find", indent); - DEBUG("commodity.prices.find", - "remembered: " << (point ? point->price : amount_t(0L))); - base->price_map.insert - (base_t::memoized_price_map::value_type(*pair, point)); - } - return point; - } - return none; + return point; } optional<price_point_t> commodity_t::check_for_updated_price(const optional<price_point_t>& point, - const optional<datetime_t>& moment, - const optional<commodity_t&>& in_terms_of) + 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) { - seconds_diff = (*moment - point->when).total_seconds(); - DEBUG("commodity.download", "moment = " << *moment); + 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(); @@ -474,10 +206,10 @@ commodity_t::check_for_updated_price(const optional<price_point_t>& point, DEBUG("commodity.download", "attempting to download a more current quote..."); if (optional<price_point_t> quote = - pool().get_commodity_quote(*this, in_terms_of)) { + pool().get_commodity_quote(referent(), in_terms_of)) { if (! in_terms_of || (quote->price.has_commodity() && - quote->price.commodity() == *in_terms_of)) + quote->price.commodity_ptr() == in_terms_of)) return quote; } } @@ -485,6 +217,16 @@ commodity_t::check_for_updated_price(const optional<price_point_t>& point, 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; @@ -641,7 +383,7 @@ void commodity_t::parse_symbol(char *& p, string& symbol) throw_(amount_error, _("Failed to parse commodity")); } -void commodity_t::print(std::ostream& out, bool elide_quotes) const +void commodity_t::print(std::ostream& out, bool elide_quotes, bool) const { string sym = symbol(); if (elide_quotes && has_flags(COMMODITY_STYLE_SEPARATED) && @@ -740,6 +482,15 @@ bool commodity_t::compare_by_commodity::operator()(const amount_t * left, if (aleftcomm.details.tag && arightcomm.details.tag) return *aleftcomm.details.tag < *arightcomm.details.tag; + if (! aleftcomm.details.value_expr && arightcomm.details.value_expr) + return true; + if (aleftcomm.details.value_expr && ! arightcomm.details.value_expr) + return false; + + if (aleftcomm.details.value_expr && arightcomm.details.value_expr) + return (aleftcomm.details.value_expr->text() < + arightcomm.details.value_expr->text()); + assert(false); return true; } @@ -767,28 +518,6 @@ void to_xml(std::ostream& out, const commodity_t& comm, if (commodity_details) { if (comm.has_annotation()) to_xml(out, as_annotated_commodity(comm).details); - - if (comm.varied_history()) { - push_xml y(out, "varied-history"); - - foreach (const commodity_t::history_by_commodity_map::value_type& pair, - comm.varied_history()->histories) { - { - push_xml z(out, "symbol"); - out << y.guard(pair.first->symbol()); - } - { - push_xml z(out, "history"); - - foreach (const commodity_t::history_map::value_type& inner_pair, - pair.second.prices) { - push_xml w(out, "price-point"); - to_xml(out, inner_pair.first); - to_xml(out, inner_pair.second); - } - } - } - } } } diff --git a/src/commodity.h b/src/commodity.h index d7747b2a..bfbabe6b 100644 --- a/src/commodity.h +++ b/src/commodity.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -47,6 +47,8 @@ #ifndef _COMMODITY_H #define _COMMODITY_H +#include "expr.h" + namespace ledger { struct keep_details_t; @@ -85,78 +87,6 @@ class commodity_t : public delegates_flags<uint_least16_t>, public equality_comparable1<commodity_t, noncopyable> { -public: - typedef std::map<const datetime_t, amount_t> history_map; - - struct history_t - { - history_map prices; - - void add_price(commodity_t& source, - const datetime_t& date, - const amount_t& price, - const bool reflexive = true); - bool remove_price(const datetime_t& date); - - optional<price_point_t> - find_price(const optional<datetime_t>& moment = none, - const optional<datetime_t>& oldest = none -#if defined(DEBUG_ON) - , const int indent = 0 -#endif - ) const; - -#if defined(HAVE_BOOST_SERIALIZATION) - private: - /** Serialization. */ - - friend class boost::serialization::access; - - template<class Archive> - void serialize(Archive& ar, const unsigned int /* version */) { - ar & prices; - } -#endif // HAVE_BOOST_SERIALIZATION - }; - - typedef std::map<commodity_t *, history_t> history_by_commodity_map; - - struct varied_history_t - { - history_by_commodity_map histories; - - void add_price(commodity_t& source, - const datetime_t& date, - const amount_t& price, - const bool reflexive = true); - bool remove_price(const datetime_t& date, commodity_t& commodity); - - optional<price_point_t> - find_price(const commodity_t& source, - const optional<commodity_t&>& commodity = none, - const optional<datetime_t>& moment = none, - const optional<datetime_t>& oldest = none -#if defined(DEBUG_ON) - , const int indent = 0 -#endif - ) const; - - optional<history_t&> - history(const optional<commodity_t&>& commodity = none); - -#if defined(HAVE_BOOST_SERIALIZATION) - private: - /** Serialization. */ - - friend class boost::serialization::access; - - template<class Archive> - void serialize(Archive& ar, const unsigned int /* version */) { - ar & histories; - } -#endif // HAVE_BOOST_SERIALIZATION - }; - protected: friend class commodity_pool_t; friend class annotated_commodity_t; @@ -178,37 +108,34 @@ protected: #define COMMODITY_SAW_ANN_PRICE_FLOAT 0x400 #define COMMODITY_SAW_ANN_PRICE_FIXATED 0x800 - string symbol; - amount_t::precision_t precision; - optional<string> name; - optional<string> note; - optional<varied_history_t> varied_history; - optional<amount_t> smaller; - optional<amount_t> larger; - - typedef std::pair<optional<datetime_t>, - optional<datetime_t> > optional_time_pair_t; - typedef std::pair<optional_time_pair_t, - commodity_t *> time_and_commodity_t; - typedef std::map<time_and_commodity_t, + string symbol; + optional<std::size_t> graph_index; + amount_t::precision_t precision; + optional<string> name; + optional<string> note; + optional<amount_t> smaller; + optional<amount_t> larger; + optional<expr_t> value_expr; + + typedef tuple<datetime_t, datetime_t, + const commodity_t *> memoized_price_entry; + typedef std::map<memoized_price_entry, optional<price_point_t> > memoized_price_map; - static const std::size_t max_price_map_size = 16; + static const std::size_t max_price_map_size = 8; mutable memoized_price_map price_map; - mutable bool searched; - public: explicit base_t(const string& _symbol) : supports_flags<uint_least16_t> (commodity_t::decimal_comma_by_default ? static_cast<uint_least16_t>(COMMODITY_STYLE_DECIMAL_COMMA) : static_cast<uint_least16_t>(COMMODITY_STYLE_DEFAULTS)), - symbol(_symbol), precision(0), searched(false) { - TRACE_CTOR(base_t, "const string&"); + symbol(_symbol), precision(0) { + TRACE_CTOR(commodity_t::base_t, "const string&"); } virtual ~base_t() { - TRACE_DTOR(base_t); + TRACE_DTOR(commodity_t::base_t); } #if defined(HAVE_BOOST_SERIALIZATION) @@ -228,7 +155,6 @@ protected: ar & precision; ar & name; ar & note; - ar & varied_history; ar & smaller; ar & larger; } @@ -239,7 +165,6 @@ protected: commodity_pool_t * parent_; optional<string> qualified_symbol; - optional<string> mapping_key_; bool annotated; explicit commodity_t(commodity_pool_t * _parent, @@ -263,6 +188,9 @@ public: 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); @@ -293,11 +221,11 @@ public: return qualified_symbol ? *qualified_symbol : base_symbol(); } - string mapping_key() const { - if (mapping_key_) - return *mapping_key_; - else - return base_symbol(); + optional<std::size_t> graph_index() const {; + return base->graph_index; + } + void set_graph_index(const optional<std::size_t>& arg = none) { + base->graph_index = arg; } optional<string> name() const { @@ -335,53 +263,37 @@ public: base->larger = arg; } - optional<varied_history_t&> varied_history() { - if (base->varied_history) - return *base->varied_history; - return none; + virtual optional<expr_t> value_expr() const { + return base->value_expr; } - optional<const varied_history_t&> varied_history() const { - if (base->varied_history) - return *base->varied_history; - return none; + void set_value_expr(const optional<expr_t>& expr = none) { + base->value_expr = expr; } - optional<history_t&> history(const optional<commodity_t&>& commodity); + 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); - // These methods provide a transparent pass-through to the underlying - // base->varied_history object. + void map_prices(function<void(datetime_t, const amount_t&)> fn, + const datetime_t& moment = datetime_t(), + const datetime_t& _oldest = datetime_t(), + bool bidirectionally = false); - void add_price(const datetime_t& date, const amount_t& price, - const bool reflexive = true) { - if (! base->varied_history) - base->varied_history = varied_history_t(); - base->varied_history->add_price(*this, date, price, reflexive); - DEBUG("commodity.prices.find", "Price added, clearing price_map"); - base->price_map.clear(); // a price was added, invalid the map - } - bool remove_price(const datetime_t& date, commodity_t& commodity) { - if (base->varied_history) { - base->varied_history->remove_price(date, commodity); - DEBUG("commodity.prices.find", "Price removed, clearing price_map"); - base->price_map.clear(); // a price was added, invalid the map - } - return false; - } + optional<price_point_t> + find_price_from_expr(expr_t& expr, const commodity_t * commodity, + const datetime_t& moment) const; optional<price_point_t> - find_price(const optional<commodity_t&>& commodity = none, - const optional<datetime_t>& moment = none, - const optional<datetime_t>& oldest = none, - const bool nested = false -#if defined(DEBUG_ON) - , const int indent = 0 -#endif - ) const; + virtual find_price(const commodity_t * commodity = NULL, + const datetime_t& moment = datetime_t(), + const datetime_t& oldest = datetime_t()) const; optional<price_point_t> check_for_updated_price(const optional<price_point_t>& point, - const optional<datetime_t>& moment, - const optional<commodity_t&>& in_terms_of); + 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. @@ -394,7 +306,8 @@ public: return temp; } - void print(std::ostream& out, bool elide_quotes = false) const; + virtual void print(std::ostream& out, bool elide_quotes = false, + bool print_annotations = false) const; bool valid() const; struct compare_by_commodity { @@ -423,14 +336,13 @@ private: ar & base; ar & parent_; ar & qualified_symbol; - ar & mapping_key_; ar & annotated; } #endif // HAVE_BOOST_SERIALIZATION }; inline std::ostream& operator<<(std::ostream& out, const commodity_t& comm) { - comm.print(out); + comm.print(out, false, true); return out; } diff --git a/src/compare.cc b/src/compare.cc index 12114c7d..e2a298c2 100644 --- a/src/compare.cc +++ b/src/compare.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -44,7 +44,7 @@ void push_sort_value(std::list<sort_value_t>& sort_values, 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->right(); + node = node->has_right() ? node->right() : NULL; } } else { bool inverted = false; diff --git a/src/compare.h b/src/compare.h index 0e7bf5e5..e1abbca1 100644 --- a/src/compare.h +++ b/src/compare.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/context.h b/src/context.h new file mode 100644 index 00000000..45bb9990 --- /dev/null +++ b/src/context.h @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2003-2012, 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<std::istream> stream; + + path pathname; + path current_directory; + journal_t * journal; + account_t * master; + scope_t * scope; + char linebuf[MAX_LINE + 1]; + istream_pos_type line_beg_pos; + istream_pos_type curr_pos; + std::size_t linenum; + std::size_t errors; + std::size_t count; + std::size_t sequence; + + 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<std::istream> _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); + } +}; + +inline parse_context_t open_for_reading(const path& pathname, + const path& cwd) +{ + path filename = resolve_path(pathname); + + if (! exists(filename)) + throw_(std::runtime_error, + _("Cannot read journal file %1") << filename); + +#if BOOST_VERSION >= 104600 && BOOST_FILESYSTEM_VERSION >= 3 + path parent(filesystem::absolute(pathname, cwd).parent_path()); +#else + path parent(filesystem::complete(pathname, cwd).parent_path()); +#endif + shared_ptr<std::istream> stream(new ifstream(filename)); + parse_context_t context(stream, parent); + context.pathname = filename; + return context; +} + +class parse_context_stack_t +{ + std::list<parse_context_t> parsing_context; + +public: + void push() { + parsing_context.push_front(parse_context_t(filesystem::current_path())); + } + void push(shared_ptr<std::istream> 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 index 493fbb7a..e8ca241e 100644 --- a/src/convert.cc +++ b/src/convert.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -56,75 +56,50 @@ value_t convert_command(call_scope_t& args) account_t * bucket = journal.master->find_account(bucket_name); account_t * unknown = journal.master->find_account(_("Expenses:Unknown")); - // Make an amounts mapping for the account under consideration - - typedef std::map<value_t, std::list<post_t *> > post_map_t; - post_map_t post_map; - - xacts_iterator journal_iter(journal); - while (xact_t * xact = *journal_iter++) { - post_t * post = NULL; - xact_posts_iterator xact_iter(*xact); - while ((post = *xact_iter++) != NULL) { - if (post->account == bucket) - break; - } - if (post) { - post_map_t::iterator i = post_map.find(post->amount); - if (i == post_map.end()) { - std::list<post_t *> post_list; - post_list.push_back(post); - post_map.insert(post_map_t::value_type(post->amount, post_list)); - } else { - (*i).second.push_back(post); - } - } - } - // 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); - ifstream data(path(args.get<string>(0))); - csv_reader reader(data); + path csv_file_path(args.get<string>(0)); - while (xact_t * xact = reader.read_xact(journal, bucket)) { - if (report.HANDLED(invert)) { - foreach (post_t * post, xact->posts) - post->amount.in_place_negate(); - } + report.session.parsing_context.push(csv_file_path); + parse_context_t& context(report.session.parsing_context.get_current()); + context.journal = &journal; + context.master = bucket; - bool matched = false; - if (! xact->posts.front()->amount.is_null()) { - post_map_t::iterator i = post_map.find(- xact->posts.front()->amount); - if (i != post_map.end()) { - std::list<post_t *>& post_list((*i).second); - foreach (post_t * post, post_list) { - if (xact->code && post->xact->code && - *xact->code == *post->xact->code) { - matched = true; - break; - } - else if (xact->actual_date() == post->actual_date()) { - matched = true; - break; - } - } + 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(); } - } - if (matched) { - DEBUG("convert.csv", "Ignored xact with code: " << *xact->code); - checked_delete(xact); // ignore it - } - else { + 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) { - // jww (2010-03-07): Bind this logic to an option: --auto-match if (account_t * acct = - lookup_probable_account(xact->payee, current_xacts.rbegin(), - current_xacts.rend(), bucket).second) + (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; @@ -141,8 +116,16 @@ value_t convert_command(call_scope_t& args) formatter(*post); } } + formatter.flush(); + } + catch (const std::exception&) { + add_error_context(_("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; } - formatter.flush(); // If not, transform the payee according to regexps diff --git a/src/convert.h b/src/convert.h index 6d02f24a..de958108 100644 --- a/src/convert.h +++ b/src/convert.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -40,27 +40,27 @@ namespace ledger { -string csv_reader::read_field(std::istream& sin) +string csv_reader::read_field(std::istream& in) { string field; char c; - if (sin.peek() == '"' || sin.peek() == '|') { - sin.get(c); + if (in.peek() == '"' || in.peek() == '|') { + in.get(c); char x; - while (sin.good() && ! sin.eof()) { - sin.get(x); + while (in.good() && ! in.eof()) { + in.get(x); if (x == '\\') { - sin.get(x); + in.get(x); } - else if (x == '"' && sin.peek() == '"') { - sin.get(x); + else if (x == '"' && in.peek() == '"') { + in.get(x); } else if (x == c) { if (x == '|') - sin.unget(); - else if (sin.peek() == ',') - sin.get(c); + in.unget(); + else if (in.peek() == ',') + in.get(c); break; } if (x != '\0') @@ -68,36 +68,36 @@ string csv_reader::read_field(std::istream& sin) } } else { - while (sin.good() && ! sin.eof()) { - sin.get(c); - if (c == ',') - break; - if (c != '\0') - field += c; + 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& sin) +char * csv_reader::next_line(std::istream& in) { - static char linebuf[MAX_LINE + 1]; + while (in.good() && ! in.eof() && in.peek() == '#') + in.getline(context.linebuf, parse_context_t::MAX_LINE); - while (sin.good() && ! sin.eof() && sin.peek() == '#') - sin.getline(linebuf, MAX_LINE); - - if (! sin.good() || sin.eof()) + if (! in.good() || in.eof()) return NULL; - sin.getline(linebuf, MAX_LINE); + in.getline(context.linebuf, parse_context_t::MAX_LINE); - return linebuf; + return context.linebuf; } -void csv_reader::read_index(std::istream& sin) +void csv_reader::read_index(std::istream& in) { - char * line = next_line(sin); + char * line = next_line(in); if (! line) return; @@ -109,8 +109,8 @@ void csv_reader::read_index(std::istream& sin) if (date_mask.match(field)) index.push_back(FIELD_DATE); - else if (date_eff_mask.match(field)) - index.push_back(FIELD_DATE_EFF); + 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)) @@ -130,60 +130,52 @@ void csv_reader::read_index(std::istream& sin) } } -xact_t * csv_reader::read_xact(journal_t& journal, account_t * bucket) +xact_t * csv_reader::read_xact(bool rich_data) { - restart: - char * line = next_line(in); + char * line = next_line(*context.stream.get()); if (! line || index.empty()) return NULL; + context.linenum++; std::istringstream instr(line); - std::auto_ptr<xact_t> xact(new xact_t); - std::auto_ptr<post_t> post(new post_t); + unique_ptr<xact_t> xact(new xact_t); + unique_ptr<post_t> post(new post_t); xact->set_state(item_t::CLEARED); xact->pos = position_t(); - xact->pos->pathname = "jww (2010-03-05): unknown"; - xact->pos->beg_pos = in.tellg(); - xact->pos->beg_line = 0; - xact->pos->sequence = 0; + 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(); -#if 0 post->pos = position_t(); - post->pos->pathname = pathname; - post->pos->beg_pos = line_beg_pos; - post->pos->beg_line = linenum; + post->pos->pathname = context.pathname; + post->pos->beg_pos = context.stream->tellg(); + post->pos->beg_line = context.linenum; post->pos->sequence = context.sequence++; -#endif post->set_state(item_t::CLEARED); post->account = NULL; std::vector<int>::size_type n = 0; amount_t amt; - string total; + string total; + string field; while (instr.good() && ! instr.eof()) { - string field = read_field(instr); + field = read_field(instr); switch (index[n]) { case FIELD_DATE: - if (field.empty()) - goto restart; - try { - xact->_date = parse_date(field); - } - catch (date_error&) { - goto restart; - } + xact->_date = parse_date(field); break; - case FIELD_DATE_EFF: - xact->_date_eff = parse_date(field); + case FIELD_DATE_AUX: + xact->_date_aux = parse_date(field); break; case FIELD_CODE: @@ -193,7 +185,7 @@ xact_t * csv_reader::read_xact(journal_t& journal, account_t * bucket) case FIELD_PAYEE: { bool found = false; - foreach (payee_mapping_t& value, journal.payee_mappings) { + foreach (payee_mapping_t& value, context.journal->payee_mappings) { DEBUG("csv.mappings", "Looking for payee mapping: " << value.first); if (value.first.match(field)) { xact->payee = value.second; @@ -243,16 +235,15 @@ xact_t * csv_reader::read_xact(journal_t& journal, account_t * bucket) n++; } -#if 0 - xact->set_tag(_("Imported"), - string(format_date(CURRENT_DATE(), FMT_WRITTEN))); - xact->set_tag(_("Original"), string(line)); - xact->set_tag(_("SHA1"), string(sha1sum(line))); -#endif + 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, journal.account_mappings) { + foreach (account_mapping_t& value, context.journal->account_mappings) { if (value.first.match(xact->payee)) { post->account = value.second; break; @@ -267,16 +258,14 @@ xact_t * csv_reader::read_xact(journal_t& journal, account_t * bucket) post->xact = xact.get(); -#if 0 post->pos = position_t(); - post->pos->pathname = pathname; - post->pos->beg_pos = line_beg_pos; - post->pos->beg_line = linenum; + post->pos->pathname = context.pathname; + post->pos->beg_pos = context.stream->tellg(); + post->pos->beg_line = context.linenum; post->pos->sequence = context.sequence++; -#endif post->set_state(item_t::CLEARED); - post->account = bucket; + post->account = context.master; if (! amt.is_null()) post->amount = - amt; @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -43,6 +43,7 @@ #define _CSV_H #include "value.h" +#include "context.h" namespace ledger { @@ -52,13 +53,11 @@ class account_t; class csv_reader { - static const std::size_t MAX_LINE = 1024; - - std::istream& in; + parse_context_t context; enum headers_t { FIELD_DATE = 0, - FIELD_DATE_EFF, + FIELD_DATE_AUX, FIELD_CODE, FIELD_PAYEE, FIELD_AMOUNT, @@ -70,7 +69,7 @@ class csv_reader }; mask_t date_mask; - mask_t date_eff_mask; + mask_t date_aux_mask; mask_t code_mask; mask_t payee_mask; mask_t amount_mask; @@ -80,29 +79,40 @@ class csv_reader std::vector<int> index; std::vector<string> names; - std::vector<string> fields; - - typedef std::map<string, string> string_map; public: - csv_reader(std::istream& _in) - : in(_in), + csv_reader(parse_context_t& _context) + : context(_context), date_mask("date"), - date_eff_mask("posted( ?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(in); + 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); - void read_index(std::istream& in); - xact_t * read_xact(journal_t& journal, account_t * bucket); + 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 diff --git a/src/draft.cc b/src/draft.cc index 2b8b45f7..43c214cb 100644 --- a/src/draft.cc +++ b/src/draft.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -67,10 +67,8 @@ void draft_t::xact_template_t::dump(std::ostream& out) const << std::endl; } else { foreach (const post_template_t& post, posts) { - straccstream accum; out << std::endl - << ACCUM(accum << _("[Posting \"%1\"]") - << (post.from ? _("from") : _("to"))) + << STR(_("[Posting \"%1\"]") << (post.from ? _("from") : _("to"))) << std::endl; if (post.account_mask) @@ -111,7 +109,14 @@ void draft_t::parse_args(const value_t& args) } 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<short>(*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); @@ -235,7 +240,7 @@ xact_t * draft_t::insert(journal_t& journal) throw std::runtime_error(_("'xact' command requires at least a payee")); xact_t * matching = NULL; - std::auto_ptr<xact_t> added(new xact_t); + unique_ptr<xact_t> added(new xact_t); if (xact_t * xact = lookup_probable_account(tmpl->payee_mask.str(), journal.xacts.rbegin(), @@ -318,7 +323,7 @@ xact_t * draft_t::insert(journal_t& journal) } foreach (xact_template_t::post_template_t& post, tmpl->posts) { - std::auto_ptr<post_t> new_post; + unique_ptr<post_t> new_post; commodity_t * found_commodity = NULL; @@ -502,7 +507,6 @@ value_t template_command(call_scope_t& args) out << std::endl << std::endl; draft_t draft(args.value()); - out << _("--- Transaction template ---") << std::endl; draft.dump(out); @@ -512,15 +516,16 @@ value_t template_command(call_scope_t& args) value_t xact_command(call_scope_t& args) { report_t& report(find_scope<report_t>(args)); - draft_t draft(args.value()); + draft_t draft(args.value()); - xact_t * new_xact = draft.insert(*report.session.journal.get()); + unique_ptr<xact_t> 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"); - // Only consider actual postings for the "xact" command - report.HANDLER(limit_).on(string("#xact"), "actual"); + report.xact_report(post_handler_ptr(new print_xacts(report)), *new_xact.get()); + } - if (new_xact) - report.xact_report(post_handler_ptr(new print_xacts(report)), *new_xact); return true; } diff --git a/src/draft.h b/src/draft.h index 59039f77..9023e6da 100644 --- a/src/draft.h +++ b/src/draft.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -68,12 +68,31 @@ class draft_t : public expr_base_t<value_t> optional<string> cost_operator; optional<amount_t> cost; - post_template_t() : from(false) {} + post_template_t() : from(false) { + TRACE_CTOR(post_template_t, ""); + } + ~post_template_t() throw() { + TRACE_DTOR(post_template_t); + } }; std::list<post_template_t> posts; - xact_template_t() {} + 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; }; @@ -82,11 +101,11 @@ class draft_t : public expr_base_t<value_t> public: draft_t(const value_t& args) : base_type() { - TRACE_CTOR(draft_t, "value_t"); if (! args.empty()) parse_args(args); + TRACE_CTOR(draft_t, "value_t"); } - virtual ~draft_t() { + virtual ~draft_t() throw() { TRACE_DTOR(draft_t); } diff --git a/src/emacs.cc b/src/emacs.cc index 5048a348..41c67cc6 100644 --- a/src/emacs.cc +++ b/src/emacs.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/emacs.h b/src/emacs.h index 97292728..a018ce68 100644 --- a/src/emacs.h +++ b/src/emacs.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/error.cc b/src/error.cc index 88adfbdb..4a16f4e3 100644 --- a/src/error.cc +++ b/src/error.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/error.h b/src/error.h index b9960b03..86d9de76 100644 --- a/src/error.h +++ b/src/error.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -101,6 +101,12 @@ string source_context(const path& file, virtual ~name() throw() {} \ } +struct error_count { + std::size_t count; + explicit error_count(std::size_t _count) : count(_count) {} + const char * what() const { return ""; } +}; + } // namespace ledger #endif // _ERROR_H diff --git a/src/expr.cc b/src/expr.cc index b3d4abcd..25967207 100644 --- a/src/expr.cc +++ b/src/expr.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -37,6 +37,59 @@ 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<string>& original_string) { @@ -163,6 +216,65 @@ 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<expr_t::ptr_op_t>(); +} + +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; @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -58,53 +58,33 @@ public: typedef intrusive_ptr<op_t> ptr_op_t; typedef intrusive_ptr<const op_t> const_ptr_op_t; + enum check_expr_kind_t { + EXPR_GENERAL, + EXPR_ASSERTION, + EXPR_CHECK + }; + + typedef std::pair<expr_t, check_expr_kind_t> check_expr_pair; + typedef std::list<check_expr_pair> check_expr_list; + protected: ptr_op_t ptr; public: - expr_t() : base_type() { - TRACE_CTOR(expr_t, ""); - } - expr_t(const expr_t& other) - : base_type(other), ptr(other.ptr) { - TRACE_CTOR(expr_t, "copy"); - } - expr_t(ptr_op_t _ptr, scope_t * _context = NULL) - : base_type(_context), ptr(_ptr) { - TRACE_CTOR(expr_t, "const ptr_op_t&, scope_t *"); - } + 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) - : base_type() { - TRACE_CTOR(expr_t, "string, parse_flags_t"); - if (! _str.empty()) - parse(_str, flags); - } - expr_t(std::istream& in, const parse_flags_t& flags = PARSE_DEFAULT) - : base_type() { - TRACE_CTOR(expr_t, "std::istream&, parse_flags_t"); - parse(in, flags); - } + 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() { - TRACE_DTOR(expr_t); - } + virtual ~expr_t(); - expr_t& operator=(const expr_t& _expr) { - if (this != &_expr) { - base_type::operator=(_expr); - ptr = _expr.ptr; - } - return *this; - } + expr_t& operator=(const expr_t& _expr); - virtual operator bool() const throw() { - return ptr.get() != NULL; - } + virtual operator bool() const throw(); - ptr_op_t get_op() throw() { - return ptr; - } + ptr_op_t get_op() throw(); void parse(const string& str, const parse_flags_t& flags = PARSE_DEFAULT) { std::istringstream stream(str); @@ -147,21 +127,73 @@ private: inline bool is_expr(const value_t& val) { return val.is_any() && val.as_any().type() == typeid(expr_t::ptr_op_t); } -inline expr_t::ptr_op_t as_expr(const value_t& val) { - VERIFY(val.is_any()); - return val.as_any<expr_t::ptr_op_t>(); -} -inline void set_expr(value_t& val, expr_t::ptr_op_t op) { - val.set_any(op); -} -inline value_t expr_value(expr_t::ptr_op_t op) { - value_t temp; - temp.set_any(op); - return temp; -} -class call_scope_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<string> 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 diff --git a/src/exprbase.h b/src/exprbase.h index e0e2824f..bea25320 100644 --- a/src/exprbase.h +++ b/src/exprbase.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -113,7 +113,7 @@ public: return ! str.empty(); } - virtual string text() { + virtual string text() const throw() { return str; } void set_text(const string& txt) { @@ -163,7 +163,7 @@ public: } #endif // defined(DEBUG_ON) - DEBUG("expr.calc.when", "Compiling: " << str); + DEBUG("expr.compile", "Compiling: " << str); compile(scope); #if defined(DEBUG_ON) @@ -174,7 +174,7 @@ public: #endif // defined(DEBUG_ON) } - DEBUG("expr.calc.when", "Calculating: " << str); + DEBUG("expr.calc", "Calculating: " << str); return real_calc(scope); } diff --git a/src/filters.cc b/src/filters.cc index 331073eb..9589958c 100644 --- a/src/filters.cc +++ b/src/filters.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -37,6 +37,7 @@ #include "report.h" #include "compare.h" #include "pool.h" +#include "history.h" namespace ledger { @@ -262,6 +263,8 @@ void anonymize_posts::operator()(post_t& post) xact.payee = to_hex(message_digest); xact.note = none; + } else { + xact.journal = post.xact->journal; } std::list<string> account_names; @@ -337,9 +340,10 @@ namespace { const bool act_date_p = true, const value_t& total = value_t(), const bool direct_amount = false, - const bool mark_visited = false) + const bool mark_visited = false, + const bool bidir_link = true) { - post_t& post = temps.create_post(*xact, account); + 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 @@ -509,8 +513,8 @@ display_filter_posts::display_filter_posts(post_handler_ptr handler, display_total_expr(report.HANDLER(display_total_).expr), show_rounding(_show_rounding) { - TRACE_CTOR(display_filter_posts, "post_handler_ptr, report_t&, bool"); create_accounts(); + TRACE_CTOR(display_filter_posts, "post_handler_ptr, report_t&, bool"); } bool display_filter_posts::output_rounding(post_t& post) @@ -519,7 +523,8 @@ bool display_filter_posts::output_rounding(post_t& post) value_t new_display_total; if (show_rounding) { - new_display_total = display_total_expr.calc(bound_scope); + 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); @@ -536,7 +541,8 @@ bool display_filter_posts::output_rounding(post_t& post) return true; } - if (value_t repriced_amount = display_amount_expr.calc(bound_scope)) { + 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); @@ -561,7 +567,9 @@ bool display_filter_posts::output_rounding(post_t& post) /* date= */ date_t(), /* act_date_p= */ true, /* total= */ precise_display_total, - /* direct_amount= */ true); + /* direct_amount= */ true, + /* mark_visited= */ false, + /* bidir_link= */ false); } } if (show_rounding) @@ -590,13 +598,11 @@ changed_value_posts::changed_value_posts 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) { - TRACE_CTOR(changed_value_posts, - "post_handler_ptr, report_t&, bool, bool, display_filter_posts *"); - string gains_equity_account_name; if (report.HANDLED(unrealized_gains_)) gains_equity_account_name = report.HANDLER(unrealized_gains_).str(); @@ -616,14 +622,19 @@ changed_value_posts::changed_value_posts 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 (! for_accounts_report) - output_intermediate_prices(*last_post, report.terminus.date()); - output_revaluation(*last_post, 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<post_t>::flush(); @@ -688,6 +699,19 @@ void changed_value_posts::output_revaluation(post_t& post, const date_t& date) } } +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()(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) { @@ -754,36 +778,19 @@ void changed_value_posts::output_intermediate_prices(post_t& post, // fall through... case value_t::BALANCE: { - commodity_t::history_map all_prices; + price_map_t all_prices; foreach (const balance_t::amounts_map::value_type& amt_comm, - display_total.as_balance().amounts) { - if (optional<commodity_t::varied_history_t&> hist = - amt_comm.first->varied_history()) { - foreach - (const commodity_t::history_by_commodity_map::value_type& comm_hist, - hist->histories) { - foreach (const commodity_t::history_map::value_type& price, - comm_hist.second.prices) { - if (price.first.date() > post.value_date() && - price.first.date() < current) { - DEBUG("filters.revalued", post.value_date() << " < " - << price.first.date() << " < " << current); - DEBUG("filters.revalued", "inserting " - << price.second << " at " << price.first.date()); - all_prices.insert(price); - } - } - } - } - } + 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<const date_t, bool> date_map; date_map pricing_dates; - BOOST_REVERSE_FOREACH - (const commodity_t::history_map::value_type& price, all_prices) { + 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", @@ -808,7 +815,7 @@ void changed_value_posts::output_intermediate_prices(post_t& post, void changed_value_posts::operator()(post_t& post) { if (last_post) { - if (! for_accounts_report) + if (! for_accounts_report && ! historical_prices_only) output_intermediate_prices(*last_post, post.value_date()); output_revaluation(*last_post, post.value_date()); } @@ -836,10 +843,17 @@ void subtotal_posts::report_subtotal(const char * spec_fmt, 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(); @@ -879,15 +893,39 @@ void subtotal_posts::operator()(post_t& 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()) { - value_t temp; - post.add_to_value(temp, amount_expr); - std::pair<values_map::iterator, bool> result - = values.insert(values_pair(acct->fullname(), acct_value_t(acct, temp))); +#if defined(DEBUG_ON) + std::pair<values_map::iterator, bool> 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 defined(DEBUG_ON) assert(result.second); +#endif } else { - post.add_to_value((*i).second.value, amount_expr); + 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 @@ -904,55 +942,139 @@ void subtotal_posts::operator()(post_t& post) void interval_posts::report_subtotal(const date_interval_t& ival) { - if (last_post && ival) { - if (exact_periods) - subtotal_posts::report_subtotal(); - else - subtotal_posts::report_subtotal(NULL, ival); - } + if (exact_periods) + subtotal_posts::report_subtotal(); + else + subtotal_posts::report_subtotal(NULL, ival); +} - last_post = NULL; +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 (! interval.find_period(post.date())) - return; + // 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) { - if (last_interval && interval != last_interval) { - report_subtotal(last_interval); + all_posts.push_back(&post); + } + else if (interval.find_period(post.date())) { + item_handler<post_t>::operator()(post); + } +} - if (generate_empty_posts) { - for (++last_interval; interval != last_interval; ++last_interval) { - // 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 = last_interval.inclusive_end(); +void interval_posts::flush() +{ + if (! interval.duration) { + item_handler<post_t>::flush(); + return; + } - post_t& null_post = temps.create_post(null_xact, empty_account); - null_post.add_flags(POST_CALCULATED); - null_post.amount = 0L; + // Sort all the postings we saw by date ascending + std::stable_sort(all_posts.begin(), all_posts.end(), + sort_posts_by_date()); - last_post = &null_post; - subtotal_posts::operator()(null_post); + // Determine the beginning interval by using the earliest post + if (! interval.find_period(all_posts.front()->date())) + throw_(std::logic_error, _("Failed to find period for interval report")); - report_subtotal(last_interval); - } - assert(interval == last_interval); - } else { - last_interval = interval; - } + // 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<post_t *>::iterator i = all_posts.begin(); + i != all_posts.end(); ) { + post_t * post(*i); + + DEBUG("filters.interval", + "Considering post " << post->date() << " = " << post->amount); +#if defined(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 { - last_interval = interval; + 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; } - subtotal_posts::operator()(post); - } else { - item_handler<post_t>::operator()(post); } - last_post = &post; + // 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() @@ -971,40 +1093,47 @@ void posts_as_equity::report_subtotal() value_t total = 0L; foreach (values_map::value_type& pair, values) { - if (pair.second.value.is_balance()) { - foreach (const balance_t::amounts_map::value_type& amount_pair, - pair.second.value.as_balance().amounts) - handle_value(/* value= */ amount_pair.second, + value_t value(pair.second.value.strip_annotations(report.what_to_keep())); + if (! value.is_zero()) { + if (value.is_balance()) { + foreach (const balance_t::amounts_map::value_type& amount_pair, + value.as_balance_lval().amounts) { + if (! amount_pair.second.is_zero()) + handle_value(/* value= */ amount_pair.second, + /* 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); - } else { - handle_value(/* value= */ pair.second.value, - /* account= */ pair.second.account, - /* xact= */ &xact, - /* temps= */ temps, - /* handler= */ handler, - /* date= */ finish, - /* act_date_p= */ false); + } } - total += pair.second.value; + + if (! pair.second.is_virtual || pair.second.must_balance) + total += value; } values.clear(); - if (total.is_balance()) { - foreach (const balance_t::amounts_map::value_type& pair, - total.as_balance().amounts) { - post_t& balance_post = temps.create_post(xact, balance_account); - balance_post.amount = - pair.second; - (*handler)(balance_post); - } - } else { - post_t& balance_post = temps.create_post(xact, balance_account); - balance_post.amount = - total.to_amount(); - (*handler)(balance_post); + // 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()); } } @@ -1346,8 +1475,6 @@ inject_posts::inject_posts(post_handler_ptr handler, account_t * master) : item_handler<post_t>(handler) { - TRACE_CTOR(inject_posts, "post_handler_ptr, string, account_t *"); - scoped_array<char> buf(new char[tag_list.length() + 1]); std::strcpy(buf.get(), tag_list.c_str()); @@ -1364,6 +1491,8 @@ inject_posts::inject_posts(post_handler_ptr handler, 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) diff --git a/src/filters.h b/src/filters.h index e7a2eefa..1dad8852 100644 --- a/src/filters.h +++ b/src/filters.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -65,18 +65,18 @@ protected: value_to_posts_map posts_map; post_handler_ptr post_chain; report_t& report; - expr_t group_by_expr; + expr_t& group_by_expr; custom_flusher_t preflush_func; optional<custom_flusher_t> postflush_func; public: post_splitter(post_handler_ptr _post_chain, report_t& _report, - expr_t _group_by_expr) + 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"); - preflush_func = bind(&post_splitter::print_title, this, _1); } virtual ~post_splitter() { TRACE_DTOR(post_splitter); @@ -154,9 +154,7 @@ class pass_down_posts : public item_handler<post_t> public: pass_down_posts(post_handler_ptr handler, Iterator& iter) : item_handler<post_t>(handler) { - TRACE_CTOR(pass_down_posts, "post_handler_ptr, posts_iterator"); - - while (post_t * post = *iter++) { + while (post_t * post = *iter) { try { item_handler<post_t>::operator()(*post); } @@ -164,9 +162,12 @@ public: add_error_context(item_context(*post, _("While handling posting"))); throw; } + iter.increment(); } item_handler<post_t>::flush(); + + TRACE_CTOR(pass_down_posts, "post_handler_ptr, posts_iterator"); } virtual ~pass_down_posts() { @@ -372,6 +373,7 @@ public: } virtual ~anonymize_posts() { TRACE_DTOR(anonymize_posts); + handler.reset(); } void render_commodity(amount_t& amt); @@ -446,11 +448,12 @@ public: only_predicate(_only_predicate), count(0), last_xact(NULL), last_post(NULL), only_collapse_if_zero(_only_collapse_if_zero), report(_report) { - TRACE_CTOR(collapse_posts, "post_handler_ptr, ..."); create_accounts(); + TRACE_CTOR(collapse_posts, "post_handler_ptr, ..."); } virtual ~collapse_posts() { TRACE_DTOR(collapse_posts); + handler.reset(); } void create_accounts() { @@ -496,8 +499,7 @@ public: const bool _also_matching = false) : item_handler<post_t>(handler), also_matching(_also_matching) { - TRACE_CTOR(related_posts, - "post_handler_ptr, const bool"); + TRACE_CTOR(related_posts, "post_handler_ptr, const bool"); } virtual ~related_posts() throw() { TRACE_DTOR(related_posts); @@ -521,8 +523,8 @@ class display_filter_posts : public item_handler<post_t> // later in the chain. report_t& report; - expr_t display_amount_expr; - expr_t display_total_expr; + expr_t& display_amount_expr; + expr_t& display_total_expr; bool show_rounding; value_t last_display_total; temporaries_t temps; @@ -539,6 +541,7 @@ public: virtual ~display_filter_posts() { TRACE_DTOR(display_filter_posts); + handler.reset(); } void create_accounts() { @@ -569,9 +572,10 @@ class changed_value_posts : public item_handler<post_t> // later in the chain. report_t& report; - expr_t total_expr; - expr_t display_total_expr; + 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; @@ -595,6 +599,7 @@ public: virtual ~changed_value_posts() { TRACE_DTOR(changed_value_posts); + handler.reset(); } void create_accounts() { @@ -635,15 +640,22 @@ protected: public: account_t * account; value_t value; + bool is_virtual; + bool must_balance; - acct_value_t(account_t * a) : account(a) { - TRACE_CTOR(acct_value_t, "account_t *"); + 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) : account(a), value(v) { - TRACE_CTOR(acct_value_t, "account_t *, value_t&"); + 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) { + : account(av.account), value(av.value), is_virtual(av.is_virtual) { TRACE_CTOR(acct_value_t, "copy"); } ~acct_value_t() throw() { @@ -655,11 +667,11 @@ protected: typedef std::pair<string, acct_value_t> values_pair; protected: - expr_t& amount_expr; - values_map values; - optional<string> date_format; - temporaries_t temps; - std::list<post_t *> component_posts; + expr_t& amount_expr; + values_map values; + optional<string> date_format; + temporaries_t temps; + std::deque<post_t *> component_posts; public: subtotal_posts(post_handler_ptr handler, expr_t& _amount_expr, @@ -671,6 +683,7 @@ public: } virtual ~subtotal_posts() { TRACE_DTOR(subtotal_posts); + handler.reset(); } void report_subtotal(const char * spec_fmt = NULL, @@ -697,12 +710,12 @@ class interval_posts : public subtotal_posts { date_interval_t start_interval; date_interval_t interval; - date_interval_t last_interval; - post_t * last_post; account_t * empty_account; bool exact_periods; bool generate_empty_posts; + std::deque<post_t *> all_posts; + interval_posts(); public: @@ -713,12 +726,11 @@ public: bool _exact_periods = false, bool _generate_empty_posts = false) : subtotal_posts(_handler, amount_expr), start_interval(_interval), - interval(start_interval), last_post(NULL), - exact_periods(_exact_periods), + 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"); - create_accounts(); } virtual ~interval_posts() throw() { TRACE_DTOR(interval_posts); @@ -728,21 +740,27 @@ public: empty_account = &temps.create_account(_("<None>")); } - void report_subtotal(const date_interval_t& interval); + void report_subtotal(const date_interval_t& ival); - virtual void flush() { - if (last_post && interval.duration) { - if (interval.is_valid()) - report_subtotal(interval); - subtotal_posts::flush(); - } +#if defined(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; - last_interval = date_interval_t(); - last_post = NULL; + interval = start_interval; subtotal_posts::clear(); create_accounts(); @@ -751,6 +769,7 @@ public: class posts_as_equity : public subtotal_posts { + report_t& report; post_t * last_post; account_t * equity_account; account_t * balance_account; @@ -758,10 +777,11 @@ class posts_as_equity : public subtotal_posts posts_as_equity(); public: - posts_as_equity(post_handler_ptr _handler, expr_t& amount_expr) - : subtotal_posts(_handler, amount_expr) { - TRACE_CTOR(posts_as_equity, "post_handler_ptr, expr_t&"); + 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); @@ -844,6 +864,7 @@ public: } virtual ~transfer_details() { TRACE_DTOR(transfer_details); + handler.reset(); } virtual void operator()(post_t& post); @@ -903,6 +924,7 @@ public: virtual ~generate_posts() { TRACE_DTOR(generate_posts); + handler.reset(); } void add_period_xacts(period_xacts_list& period_xacts); @@ -990,6 +1012,7 @@ class inject_posts : public item_handler<post_t> virtual ~inject_posts() throw() { TRACE_DTOR(inject_posts); + handler.reset(); } virtual void operator()(post_t& post); diff --git a/src/flags.h b/src/flags.h index 09b7eec4..e2046c08 100644 --- a/src/flags.h +++ b/src/flags.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/format.cc b/src/format.cc index 93e7ea08..8c3cbc14 100644 --- a/src/format.cc +++ b/src/format.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -70,6 +70,27 @@ void format_t::element_t::dump(std::ostream& out) const } namespace { + struct format_mapping_t { + char letter; + const char * expr; + } single_letter_mappings[] = { + { 'd', "date" }, + { 'S', "filename" }, + { 'B', "beg_pos" }, + { 'b', "beg_line" }, + { 'E', "end_pos" }, + { 'e', "end_line" }, + { 'X', "cleared" }, + { 'Y', "xact.cleared" }, + { 'C', "code" }, + { 'P', "payee" }, + { 'a', "account.name" }, + { '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); @@ -92,12 +113,19 @@ namespace { } 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<format_t&>& tmpl) { - std::auto_ptr<element_t> result; + unique_ptr<element_t> result; element_t * current = NULL; @@ -172,122 +200,170 @@ format_t::element_t * format_t::parse_elements(const string& fmt, current->min_width = current->max_width; } - switch (*p) { - case '%': - current->type = element_t::STRING; - current->data = string("%"); - break; + 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<string::size_type>(ptr - beg); + string keyword(beg, 0, klen); + if (keyword == "min") + expr << (current->min_width > 0 ? + static_cast<int>(current->min_width) : -1); + else if (keyword == "max") + expr << (current->max_width > 0 ? + static_cast<int>(current->max_width) : -1); + else if (keyword == "left") + expr << (current->has_flags(ELEMENT_ALIGN_LEFT) ? "false" : "true"); +#if defined(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, _("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")); + 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")); + 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(); + 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) + 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")); + if (! tmpl_elem) + throw_(format_error, _("%$ reference to a non-existent prior field")); - *current = *tmpl_elem; - break; - } + *current = *tmpl_elem; + break; + } - case '(': - case '{': { - bool format_amount = *p == '{'; - if (format_amount) p++; + case '(': + case '{': { + bool format_amount = *p == '{'; - current->type = element_t::EXPR; - current->data = parse_single_expression(p, ! format_amount); + current->type = element_t::EXPR; + current->data = parse_single_expression(p); - // Wrap the subexpression in calls to justify and scrub - if (format_amount) { - if (! *p || *(p + 1) != '}') - throw_(format_error, _("Expected closing brace")); - else - p++; + // Wrap the subexpression in calls to justify and scrub + if (! format_amount) + break; expr_t::ptr_op_t op = boost::get<expr_t>(current->data).get_op(); - expr_t::ptr_op_t amount_op; - expr_t::ptr_op_t colorize_op; - if (op->kind == expr_t::op_t::O_CONS) { - amount_op = op->left(); - colorize_op = op->right(); - } else { - amount_op = 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); + } } - expr_t::ptr_op_t scrub_node(new expr_t::op_t(expr_t::op_t::IDENT)); - scrub_node->set_ident("scrub"); - - expr_t::ptr_op_t call1_node(new expr_t::op_t(expr_t::op_t::O_CALL)); - call1_node->set_left(scrub_node); - call1_node->set_right(amount_op); - - expr_t::ptr_op_t arg1_node(new expr_t::op_t(expr_t::op_t::VALUE)); - expr_t::ptr_op_t arg2_node(new expr_t::op_t(expr_t::op_t::VALUE)); - expr_t::ptr_op_t arg3_node(new expr_t::op_t(expr_t::op_t::VALUE)); - - arg1_node->set_value(current->min_width > 0 ? - long(current->min_width) : -1); - arg2_node->set_value(current->max_width > 0 ? - long(current->max_width) : -1); - arg3_node->set_value(! current->has_flags(ELEMENT_ALIGN_LEFT)); - current->min_width = 0; current->max_width = 0; - expr_t::ptr_op_t args1_node(new expr_t::op_t(expr_t::op_t::O_CONS)); - args1_node->set_left(arg2_node); - args1_node->set_right(arg3_node); - - expr_t::ptr_op_t args2_node(new expr_t::op_t(expr_t::op_t::O_CONS)); - args2_node->set_left(arg1_node); - args2_node->set_right(args1_node); - - expr_t::ptr_op_t args3_node(new expr_t::op_t(expr_t::op_t::O_CONS)); - args3_node->set_left(call1_node); - args3_node->set_right(args2_node); - - expr_t::ptr_op_t seq1_node(new expr_t::op_t(expr_t::op_t::O_SEQ)); - seq1_node->set_left(args3_node); - - expr_t::ptr_op_t justify_node(new expr_t::op_t(expr_t::op_t::IDENT)); - justify_node->set_ident("justify"); - - expr_t::ptr_op_t call2_node(new expr_t::op_t(expr_t::op_t::O_CALL)); - call2_node->set_left(justify_node); - call2_node->set_right(seq1_node); - string prev_expr = boost::get<expr_t>(current->data).text(); - if (colorize_op) { - expr_t::ptr_op_t ansify_if_node(new expr_t::op_t(expr_t::op_t::IDENT)); - ansify_if_node->set_ident("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); - args4_node->set_right(colorize_op); - - expr_t::ptr_op_t seq2_node(new expr_t::op_t(expr_t::op_t::O_SEQ)); - seq2_node->set_left(args4_node); + 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(ansify_if_node); - call3_node->set_right(seq2_node); + { + 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 { @@ -295,12 +371,12 @@ format_t::element_t * format_t::parse_elements(const string& fmt, } boost::get<expr_t>(current->data).set_text(prev_expr); + break; } - break; - } - default: - throw_(format_error, _("Unrecognized formatting character: %1") << *p); + default: + throw_(format_error, _("Unrecognized formatting character: %1") << *p); + } } } @@ -342,7 +418,6 @@ string format_t::real_calc(scope_t& scope) case element_t::EXPR: { expr_t& expr(boost::get<expr_t>(elem->data)); try { - expr.compile(scope); value_t value; @@ -524,6 +599,7 @@ string format_t::truncate(const unistring& ustr, index = 0; #endif std::size_t counter = lens.size(); + std::list<string>::iterator x = parts.begin(); for (std::list<std::size_t>::iterator i = lens.begin(); i != lens.end(); i++) { @@ -553,12 +629,21 @@ string format_t::truncate(const unistring& ustr, 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"); - overflow -= adjust; + 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"); diff --git a/src/format.h b/src/format.h index f30b8184..cc48bdda 100644 --- a/src/format.h +++ b/src/format.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -51,11 +51,11 @@ class unistring; DECLARE_EXCEPTION(format_error, std::runtime_error); -class format_t : public expr_base_t<string> +class format_t : public expr_base_t<string>, public noncopyable { typedef expr_base_t<string> base_type; - struct element_t : public supports_flags<> + struct element_t : public supports_flags<>, public noncopyable { #define ELEMENT_ALIGN_LEFT 0x01 @@ -74,9 +74,6 @@ class format_t : public expr_base_t<string> ~element_t() throw() { TRACE_DTOR(element_t); } - element_t(const element_t& elem) : supports_flags<>() { - *this = elem; - } element_t& operator=(const element_t& elem) { if (this != &elem) { @@ -128,9 +125,9 @@ public: } format_t(const string& _str, scope_t * context = NULL) : base_type(context) { - TRACE_CTOR(format_t, "const string&"); if (! _str.empty()) parse_format(_str); + TRACE_CTOR(format_t, "const string&"); } virtual ~format_t() { TRACE_DTOR(format_t); diff --git a/src/generate.cc b/src/generate.cc index 185e23e7..8769c99c 100644 --- a/src/generate.cc +++ b/src/generate.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -63,16 +63,15 @@ generate_posts_iterator::generate_posts_iterator 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) { - TRACE_CTOR(generate_posts_iterator, "bool"); - std::ostringstream next_date_buf; generate_date(next_date_buf); next_date = parse_date(next_date_buf.str()); - std::ostringstream next_eff_date_buf; - generate_date(next_eff_date_buf); - next_eff_date = parse_date(next_eff_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, @@ -326,8 +325,8 @@ void generate_posts_iterator::generate_xact(std::ostream& out) next_date += gregorian::days(six_gen()); if (truth_gen()) { out << '='; - out << format_date(next_eff_date, FMT_WRITTEN); - next_eff_date += gregorian::days(six_gen()); + out << format_date(next_aux_date, FMT_WRITTEN); + next_aux_date += gregorian::days(six_gen()); } out << ' '; @@ -360,9 +359,15 @@ void generate_posts_iterator::increment() DEBUG("generate.post", "The post we intend to parse:\n" << buf.str()); - std::istringstream in(buf.str()); try { - if (session.journal->parse(in, session) != 0) { + shared_ptr<std::istringstream> 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++; diff --git a/src/generate.h b/src/generate.h index 47abcd94..1b22004b 100644 --- a/src/generate.h +++ b/src/generate.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -57,7 +57,7 @@ class generate_posts_iterator std::size_t quantity; bool allow_invalid; date_t next_date; - date_t next_eff_date; + date_t next_aux_date; mt19937 rnd_gen; diff --git a/src/global.cc b/src/global.cc index c0698376..6dc8d150 100644 --- a/src/global.cc +++ b/src/global.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -47,8 +47,6 @@ static bool args_only = false; global_scope_t::global_scope_t(char ** envp) { - TRACE_CTOR(global_scope_t, ""); - epoch = CURRENT_TIME(); #if defined(HAVE_BOOST_PYTHON) @@ -68,8 +66,9 @@ global_scope_t::global_scope_t(char ** envp) // 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())); + 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: // @@ -88,6 +87,8 @@ global_scope_t::global_scope_t(char ** envp) } else { session().HANDLER(price_db_).off(); } + + TRACE_CTOR(global_scope_t, ""); } global_scope_t::~global_scope_t() @@ -112,9 +113,12 @@ void global_scope_t::read_init() if (exists(init_file)) { TRACE_START(init, 1, "Read initialization file"); - ifstream init(init_file); + parse_context_stack_t parsing_context; + parsing_context.push(init_file); + parsing_context.get_current().journal = session().journal.get(); + parsing_context.get_current().scope = &report(); - if (session().journal->read(init_file, NULL, &report()) > 0 || + if (session().journal->read(parsing_context) > 0 || session().journal->auto_xacts.size() > 0 || session().journal->period_xacts.size() > 0) { throw_(parse_error, _("Transactions found in initialization file '%1'") @@ -189,7 +193,7 @@ void global_scope_t::execute_command(strings_list args, bool at_repl) is_precommand = true; // If it is not a pre-command, then parse the user's ledger data at this - // time if not done alreday (i.e., if not at a REPL). Then patch up the + // time if not done already (i.e., if not at a REPL). Then patch up the // report options based on the command verb. if (! is_precommand) { @@ -268,6 +272,7 @@ void global_scope_t::report_options(report_t& report, std::ostream& out) HANDLER(trace_).report(out); HANDLER(verbose).report(out); HANDLER(verify).report(out); + HANDLER(verify_memory).report(out); out << std::endl << "[Session scope options]" << std::endl; report.session.report_options(out); @@ -311,6 +316,7 @@ option_t<global_scope_t> * global_scope_t::lookup_option(const char * p) case 'v': OPT_(verbose); else OPT(verify); + else OPT(verify_memory); else OPT(version); break; } @@ -448,29 +454,36 @@ void handle_debug_options(int argc, char * argv[]) if (std::strcmp(argv[i], "--args-only") == 0) { args_only = true; } + else if (std::strcmp(argv[i], "--verify-memory") == 0) { +#if defined(VERIFY_ON) + verify_enabled = true; + + _log_level = LOG_DEBUG; + _log_category = "memory\\.counts"; +#endif + } else if (std::strcmp(argv[i], "--verify") == 0) { #if defined(VERIFY_ON) - verify_enabled = true; // global in utils.h + verify_enabled = true; #endif } else if (std::strcmp(argv[i], "--verbose") == 0 || std::strcmp(argv[i], "-v") == 0) { #if defined(LOGGING_ON) - _log_level = LOG_INFO; // global in utils.h + _log_level = LOG_INFO; #endif } else if (i + 1 < argc && std::strcmp(argv[i], "--debug") == 0) { #if defined(DEBUG_ON) - _log_level = LOG_DEBUG; // global in utils.h - _log_category = argv[i + 1]; // global in utils.h + _log_level = LOG_DEBUG; + _log_category = argv[i + 1]; i++; #endif } else if (i + 1 < argc && std::strcmp(argv[i], "--trace") == 0) { #if defined(TRACING_ON) - _log_level = LOG_TRACE; // global in utils.h + _log_level = LOG_TRACE; try { - // global in utils.h _trace_level = boost::lexical_cast<uint8_t>(argv[i + 1]); } catch (const boost::bad_lexical_cast&) { diff --git a/src/global.h b/src/global.h index 6504230d..5786bb89 100644 --- a/src/global.h +++ b/src/global.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, John Wiegley. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -50,11 +50,17 @@ class global_scope_t : public noncopyable, public scope_t { shared_ptr<session_t> session_ptr; ptr_list<report_t> report_stack; + empty_scope_t empty_scope; public: global_scope_t(char ** envp); ~global_scope_t(); + void quick_close() { + if (! report_stack.empty()) + report_stack.front().quick_close(); + } + virtual string description() { return _("global scope"); } @@ -82,6 +88,7 @@ public: void pop_report() { assert(! report_stack.empty()); report_stack.pop_front(); + // There should always be the "default report" waiting on the stack. assert(! report_stack.empty()); scope_t::default_scope = &report(); @@ -113,7 +120,7 @@ public: out << "Ledger " << ledger::version << _(", the command-line accounting tool"); out << - _("\n\nCopyright (c) 2003-2010, John Wiegley. All rights reserved.\n\n\ + _("\n\nCopyright (c) 2003-2012, John Wiegley. All rights reserved.\n\n\ This program is made available under the terms of the BSD Public License.\n\ See LICENSE file included with the distribution for details and disclaimer."); out << std::endl; @@ -139,7 +146,6 @@ See LICENSE file included with the distribution for details and disclaimer."); OPTION__ (global_scope_t, init_file_, // -i - CTOR(global_scope_t, init_file_) { if (const char * home_var = std::getenv("HOME")) on(none, (path(home_var) / ".ledgerrc").string()); @@ -152,10 +158,11 @@ See LICENSE file included with the distribution for details and disclaimer."); OPTION(global_scope_t, trace_); OPTION(global_scope_t, verbose); OPTION(global_scope_t, verify); + OPTION(global_scope_t, verify_memory); OPTION_(global_scope_t, version, DO() { // -v parent->show_version_info(std::cout); - throw int(0); // exit immediately + throw error_count(0); // exit immediately }); }; diff --git a/src/history.cc b/src/history.cc new file mode 100644 index 00000000..d94ec647 --- /dev/null +++ b/src/history.cc @@ -0,0 +1,455 @@ +/* + * Copyright (c) 2003-2012, 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 <system.hh> + +#include "history.h" + +template <typename T> +struct f_max : public std::binary_function<T, T, bool> { + T operator()(const T& x, const T& y) const { + return std::max(x, y); + } +}; + +namespace ledger { + +template <typename EdgeWeightMap, + typename PricePointMap, + typename PriceRatioMap> +class recent_edge_weight +{ +public: + EdgeWeightMap weight; + PricePointMap price_point; + PriceRatioMap ratios; + + datetime_t reftime; + datetime_t oldest; + + recent_edge_weight() { } + recent_edge_weight(EdgeWeightMap _weight, + PricePointMap _price_point, + PriceRatioMap _ratios, + const datetime_t& _reftime, + const datetime_t& _oldest = datetime_t()) + : weight(_weight), price_point(_price_point), ratios(_ratios), + reftime(_reftime), oldest(_oldest) { } + + template <typename Edge> + bool operator()(const Edge& e) const + { +#if defined(DEBUG_ON) + DEBUG("history.find", " reftime = " << reftime); + if (! oldest.is_not_a_date_time()) { + DEBUG("history.find", " oldest = " << oldest); + } +#endif + + const price_map_t& prices(get(ratios, e)); + if (prices.empty()) { + DEBUG("history.find", " prices map is empty for this edge"); + return false; + } + + price_map_t::const_iterator low = prices.upper_bound(reftime); + if (low != prices.end() && low == prices.begin()) { + DEBUG("history.find", " don't use this edge"); + return false; + } else { + --low; + assert(((*low).first <= reftime)); + + if (! oldest.is_not_a_date_time() && (*low).first < oldest) { + DEBUG("history.find", " edge is out of range"); + return false; + } + + long secs = (reftime - (*low).first).total_seconds(); + assert(secs >= 0); + + put(weight, e, secs); + put(price_point, e, price_point_t((*low).first, (*low).second)); + + DEBUG("history.find", " using edge at price point " + << (*low).first << " " << (*low).second); + return true; + } + } +}; + +typedef filtered_graph + <commodity_history_t::Graph, + recent_edge_weight<commodity_history_t::EdgeWeightMap, + commodity_history_t::PricePointMap, + commodity_history_t::PriceRatioMap> > FGraph; + +typedef property_map<FGraph, vertex_name_t>::type FNameMap; +typedef property_map<FGraph, vertex_index_t>::type FIndexMap; +typedef iterator_property_map + <commodity_history_t::vertex_descriptor*, FIndexMap, + commodity_history_t::vertex_descriptor, + commodity_history_t::vertex_descriptor&> FPredecessorMap; +typedef iterator_property_map<long*, FIndexMap, long, long&> FDistanceMap; + +void commodity_history_t::add_commodity(commodity_t& comm) +{ + if (! comm.graph_index()) { + comm.set_graph_index(num_vertices(price_graph)); + add_vertex(/* vertex_name= */ &comm, price_graph); + } +} + +void commodity_history_t::add_price(const commodity_t& source, + const datetime_t& when, + const amount_t& price) +{ + assert(source != price.commodity()); + + vertex_descriptor sv = vertex(*source.graph_index(), price_graph); + vertex_descriptor tv = vertex(*price.commodity().graph_index(), price_graph); + + std::pair<edge_descriptor, bool> e1 = edge(sv, tv, price_graph); + if (! e1.second) + e1 = add_edge(sv, tv, price_graph); + + price_map_t& prices(get(ratiomap, e1.first)); + + std::pair<price_map_t::iterator, bool> result = + prices.insert(price_map_t::value_type(when, price)); + if (! result.second) { + // There is already an entry for this moment, so update it + (*result.first).second = price; + } +} + +void commodity_history_t::remove_price(const commodity_t& source, + const commodity_t& target, + const datetime_t& date) +{ + assert(source != target); + + vertex_descriptor sv = vertex(*source.graph_index(), price_graph); + vertex_descriptor tv = vertex(*target.graph_index(), price_graph); + + std::pair<Graph::edge_descriptor, bool> e1 = edge(sv, tv, price_graph); + if (e1.second) { + price_map_t& prices(get(ratiomap, e1.first)); + + // jww (2012-03-04): If it fails, should we give a warning? + prices.erase(date); + + if (prices.empty()) + remove_edge(e1.first, price_graph); + } +} + +void commodity_history_t::map_prices(function<void(datetime_t, + const amount_t&)> fn, + const commodity_t& source, + const datetime_t& moment, + const datetime_t& oldest, + bool bidirectionally) +{ + DEBUG("history.map", "Mapping prices for source commodity: " << source); + + vertex_descriptor sv = vertex(*source.graph_index(), price_graph); + + FGraph fg(price_graph, + recent_edge_weight<EdgeWeightMap, PricePointMap, PriceRatioMap> + (get(edge_weight, price_graph), pricemap, ratiomap, + moment, oldest)); + + FNameMap namemap(get(vertex_name, fg)); + + graph_traits<FGraph>::adjacency_iterator f_vi, f_vend; + for (tie(f_vi, f_vend) = adjacent_vertices(sv, fg); f_vi != f_vend; ++f_vi) { + std::pair<Graph::edge_descriptor, bool> edgePair = edge(sv, *f_vi, fg); + Graph::edge_descriptor edge = edgePair.first; + + const price_map_t& prices(get(ratiomap, edge)); + + foreach (const price_map_t::value_type& pair, prices) { + const datetime_t& when(pair.first); + + DEBUG("history.map", "Price " << pair.second << " on " << when); + + if ((oldest.is_not_a_date_time() || when >= oldest) && when <= moment) { + if (pair.second.commodity() == source) { + if (bidirectionally) { + amount_t price(pair.second); + price.in_place_invert(); + if (source == *get(namemap, sv)) + price.set_commodity(const_cast<commodity_t&>(*get(namemap, *f_vi))); + else + price.set_commodity(const_cast<commodity_t&>(*get(namemap, sv))); + DEBUG("history.map", "Inverted price is " << price); + DEBUG("history.map", "fn(" << when << ", " << price << ")"); + fn(when, price); + } + } else { + DEBUG("history.map", "fn(" << when << ", " << pair.second << ")"); + fn(when, pair.second); + } + } + } + } +} + +optional<price_point_t> +commodity_history_t::find_price(const commodity_t& source, + const datetime_t& moment, + const datetime_t& oldest) +{ + vertex_descriptor sv = vertex(*source.graph_index(), price_graph); + + FGraph fg(price_graph, + recent_edge_weight<EdgeWeightMap, PricePointMap, PriceRatioMap> + (get(edge_weight, price_graph), pricemap, ratiomap, + moment, oldest)); + + FNameMap namemap(get(vertex_name, fg)); + + DEBUG("history.find", "sv commodity = " << get(namemap, sv)->symbol()); +#if defined(DEBUG_ON) + if (source.has_flags(COMMODITY_PRIMARY)) + DEBUG("history.find", "sv commodity is primary"); +#endif + DEBUG("history.find", "tv commodity = none "); + + datetime_t most_recent = moment; + amount_t price; + + graph_traits<FGraph>::adjacency_iterator f_vi, f_vend; + for (tie(f_vi, f_vend) = adjacent_vertices(sv, fg); f_vi != f_vend; ++f_vi) { + std::pair<Graph::edge_descriptor, bool> edgePair = edge(sv, *f_vi, fg); + Graph::edge_descriptor edge = edgePair.first; + + DEBUG("history.find", "u commodity = " << get(namemap, sv)->symbol()); + DEBUG("history.find", "v commodity = " << get(namemap, *f_vi)->symbol()); + + const price_point_t& point(get(pricemap, edge)); + + if (price.is_null() || point.when > most_recent) { + most_recent = point.when; + price = point.price; + } + + DEBUG("history.find", "price was = " << price.unrounded()); + + if (price.commodity() == source) { + price.in_place_invert(); + if (source == *get(namemap, sv)) + price.set_commodity(const_cast<commodity_t&>(*get(namemap, *f_vi))); + else + price.set_commodity(const_cast<commodity_t&>(*get(namemap, sv))); + } + + DEBUG("history.find", "price is = " << price.unrounded()); + } + + if (price.is_null()) { + DEBUG("history.find", "there is no final price"); + return none; + } else { + DEBUG("history.find", "final price is = " << price.unrounded()); + return price_point_t(most_recent, price); + } +} + +optional<price_point_t> +commodity_history_t::find_price(const commodity_t& source, + const commodity_t& target, + const datetime_t& moment, + const datetime_t& oldest) +{ + assert(source != target); + + vertex_descriptor sv = vertex(*source.graph_index(), price_graph); + vertex_descriptor tv = vertex(*target.graph_index(), price_graph); + + FGraph fg(price_graph, + recent_edge_weight<EdgeWeightMap, PricePointMap, PriceRatioMap> + (get(edge_weight, price_graph), pricemap, ratiomap, + moment, oldest)); + + FNameMap namemap(get(vertex_name, fg)); + + DEBUG("history.find", "sv commodity = " << get(namemap, sv)->symbol()); + DEBUG("history.find", "tv commodity = " << get(namemap, tv)->symbol()); + + std::size_t vector_len(num_vertices(fg)); + std::vector<vertex_descriptor> predecessors(vector_len); + std::vector<long> distances(vector_len); + + FPredecessorMap predecessorMap(&predecessors[0]); + FDistanceMap distanceMap(&distances[0]); + + dijkstra_shortest_paths(fg, /* start= */ sv, + predecessor_map(predecessorMap) + .distance_map(distanceMap) + .distance_combine(f_max<long>())); + + // Extract the shortest path and performance the calculations + datetime_t least_recent = moment; + amount_t price; + + const commodity_t * last_target = ⌖ + +#if defined(REVERSE_PREDECESSOR_MAP) + typedef tuple<const commodity_t *, const commodity_t *, + const price_point_t *> results_tuple; + std::vector<results_tuple> results; + bool results_reversed = false; +#endif + + vertex_descriptor v = tv; + for (vertex_descriptor u = predecessorMap[v]; + u != v; + v = u, u = predecessorMap[v]) + { + std::pair<Graph::edge_descriptor, bool> edgePair_uv = edge(u, v, fg); + std::pair<Graph::edge_descriptor, bool> edgePair_vu = edge(v, u, fg); + + Graph::edge_descriptor edge_uv = edgePair_uv.first; + Graph::edge_descriptor edge_vu = edgePair_vu.first; + + const price_point_t& point_uv(get(pricemap, edge_uv)); + const price_point_t& point_vu(get(pricemap, edge_vu)); + + const price_point_t& point(point_vu.when > point_uv.when ? + point_vu : point_uv); + + const commodity_t * u_comm = get(namemap, u); + const commodity_t * v_comm = get(namemap, v); + +#if defined(REVERSE_PREDECESSOR_MAP) + if (v == tv && u_comm != last_target && v_comm != last_target) + results_reversed = true; + + results.push_back(results_tuple(u_comm, v_comm, &point)); + } + + if (results_reversed) + std::reverse(results.begin(), results.end()); + + foreach (const results_tuple& edge, results) { + const commodity_t * u_comm = edge.get<0>(); + const commodity_t * v_comm = edge.get<1>(); + const price_point_t& point(*edge.get<2>()); +#else + assert(u_comm == last_target || v_comm == last_target); +#endif + + bool first_run = false; + if (price.is_null()) { + least_recent = point.when; + first_run = true; + } + else if (point.when < least_recent) { + least_recent = point.when; + } + + DEBUG("history.find", "u commodity = " << u_comm->symbol()); + DEBUG("history.find", "v commodity = " << v_comm->symbol()); + DEBUG("history.find", "last target = " << last_target->symbol()); + + // Determine which direction we are converting in + amount_t pprice(point.price); + DEBUG("history.find", "pprice = " << pprice.unrounded()); + + if (! first_run) { + DEBUG("history.find", "price was = " << price.unrounded()); + if (pprice.commodity_ptr() != last_target) + price *= pprice.inverted(); + else + price *= pprice; + } + else if (pprice.commodity_ptr() != last_target) { + price = pprice.inverted(); + } + else { + price = pprice; + } + DEBUG("history.find", "price is = " << price.unrounded()); + + if (last_target == v_comm) + last_target = u_comm; + else + last_target = v_comm; + + DEBUG("history.find", "last target now = " << last_target->symbol()); + } + + if (price.is_null()) { + DEBUG("history.find", "there is no final price"); + return none; + } else { + price.set_commodity(const_cast<commodity_t&>(target)); + DEBUG("history.find", "final price is = " << price.unrounded()); + + return price_point_t(least_recent, price); + } +} + +template <class Name> +class label_writer { +public: + label_writer(Name _name) : name(_name) { + TRACE_CTOR(label_writer<Name>, "Name"); + } + ~label_writer() throw() { + TRACE_DTOR(label_writer<Name>); + } + + template <class VertexOrEdge> + void operator()(std::ostream& out, const VertexOrEdge& v) const { + out << "[label=\"" << name[v]->symbol() << "\"]"; + } + +private: + Name name; +}; + +void commodity_history_t::print_map(std::ostream& out, const datetime_t& moment) +{ + if (moment.is_not_a_date_time()) { + write_graphviz(out, price_graph, + label_writer<NameMap>(get(vertex_name, price_graph))); + } else { + FGraph fg(price_graph, + recent_edge_weight<EdgeWeightMap, PricePointMap, PriceRatioMap> + (get(edge_weight, price_graph), pricemap, ratiomap, moment)); + write_graphviz(out, fg, label_writer<FNameMap>(get(vertex_name, fg))); + } +} + +} // namespace ledger diff --git a/src/history.h b/src/history.h new file mode 100644 index 00000000..af0d90f9 --- /dev/null +++ b/src/history.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2003-2012, 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 history.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Types for managing commodity historys + * + * Long. + */ +#ifndef _HISTORY_H +#define _HISTORY_H + +#include "amount.h" +#include "commodity.h" + +namespace boost { + enum edge_price_point_t { edge_price_point }; + enum edge_price_ratio_t { edge_price_ratio }; + BOOST_INSTALL_PROPERTY(edge, price_point); + BOOST_INSTALL_PROPERTY(edge, price_ratio); +} + +namespace ledger { + +typedef std::map<datetime_t, amount_t> price_map_t; + +class commodity_history_t : public noncopyable +{ +public: + typedef adjacency_list + <vecS, // Store all edges in a vector + vecS, // Store all vertices in a vector + undirectedS, // Relations are both ways + + // All vertices are commodities + property<vertex_name_t, const commodity_t *, + property<vertex_index_t, std::size_t> >, + + // All edges are weights computed as the absolute difference between + // the reference time of a search and a known price point. A + // filtered_graph is used to select the recent price point to the + // reference time before performing the search. + property<edge_weight_t, long, + property<edge_price_ratio_t, price_map_t, + property<edge_price_point_t, price_point_t> > >, + + // Graph itself has a std::string name + property<graph_name_t, std::string> + > Graph; + + Graph price_graph; + + typedef graph_traits<Graph>::vertex_descriptor vertex_descriptor; + typedef graph_traits<Graph>::edge_descriptor edge_descriptor; + + typedef property_map<Graph, vertex_name_t>::type NameMap; + typedef property_map<Graph, edge_weight_t>::type EdgeWeightMap; + typedef property_map<Graph, edge_price_point_t>::type PricePointMap; + typedef property_map<Graph, edge_price_ratio_t>::type PriceRatioMap; + + PricePointMap pricemap; + PriceRatioMap ratiomap; + + commodity_history_t() + : pricemap(get(edge_price_point, price_graph)), + ratiomap(get(edge_price_ratio, price_graph)) {} + + void add_commodity(commodity_t& comm); + + void add_price(const commodity_t& source, + const datetime_t& when, + const amount_t& price); + void remove_price(const commodity_t& source, + const commodity_t& target, + const datetime_t& date); + + void map_prices(function<void(datetime_t, const amount_t&)> fn, + const commodity_t& source, + const datetime_t& moment, + const datetime_t& _oldest = datetime_t(), + bool bidirectionally = false); + + optional<price_point_t> + find_price(const commodity_t& source, + const datetime_t& moment, + const datetime_t& oldest = datetime_t()); + + optional<price_point_t> + find_price(const commodity_t& source, + const commodity_t& target, + const datetime_t& moment, + const datetime_t& oldest = datetime_t()); + + void print_map(std::ostream& out, const datetime_t& moment = datetime_t()); +}; + +} // namespace ledger + +#endif // _HISTORY_H diff --git a/src/item.cc b/src/item.cc index 056aa04c..17d46652 100644 --- a/src/item.cc +++ b/src/item.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -35,7 +35,7 @@ namespace ledger { -bool item_t::use_effective_date = false; +bool item_t::use_aux_date = false; bool item_t::has_tag(const string& tag, bool) const { @@ -72,7 +72,7 @@ bool item_t::has_tag(const mask_t& tag_mask, return false; } - optional<value_t> item_t::get_tag(const string& tag, bool) const +optional<value_t> item_t::get_tag(const string& tag, bool) const { DEBUG("item.meta", "Getting item tag: " << tag); if (metadata) { @@ -103,6 +103,16 @@ optional<value_t> item_t::get_tag(const mask_t& tag_mask, return none; } +namespace { + struct CaseInsensitiveKeyCompare + : public std::binary_function<string, string, bool> + { + bool operator()(const string& s1, const string& s2) const { + return boost::algorithm::ilexicographical_compare(s1, s2); + } + }; +} + item_t::string_map::iterator item_t::set_tag(const string& tag, const optional<value_t>& value, @@ -111,15 +121,14 @@ item_t::set_tag(const string& tag, assert(! tag.empty()); if (! metadata) - metadata = string_map(); + metadata = string_map(CaseInsensitiveKeyCompare()); DEBUG("item.meta", "Setting tag '" << tag << "' to value '" << (value ? *value : string_value("<none>")) << "'"); optional<value_t> data = value; - if (data && - (data->is_null() || - (data->is_string() && data->as_string().empty()))) + if (data && (data->is_null() || + (data->is_string() && data->as_string().empty()))) data = none; string_map::iterator i = metadata->find(tag); @@ -150,7 +159,7 @@ void item_t::parse_tags(const char * p, if (char * pp = std::strchr(buf, '=')) { *pp++ = '\0'; - _date_eff = parse_date(pp); + _date_aux = parse_date(pp); } if (buf[0]) _date = parse_date(buf); @@ -172,19 +181,7 @@ void item_t::parse_tags(const char * p, q = std::strtok(NULL, " \t")) { const string::size_type len = std::strlen(q); if (len < 2) continue; - if (! tag.empty()) { - string_map::iterator i; - string field(p + (q - buf.get())); - if (by_value) { - bind_scope_t bound_scope(scope, *this); - i = set_tag(tag, expr_t(field).calc(bound_scope), overwrite_existing); - } else { - i = set_tag(tag, string_value(field), overwrite_existing); - } - (*i).second.second = true; - break; - } - else if (q[0] == ':' && q[len - 1] == ':') { // a series of tags + if (q[0] == ':' && q[len - 1] == ':') { // a series of tags for (char * r = std::strtok(q + 1, ":"); r; r = std::strtok(NULL, ":")) { @@ -199,6 +196,18 @@ void item_t::parse_tags(const char * p, index = 2; } tag = string(q, len - index); + + string_map::iterator i; + string field(p + len + index); + trim(field); + if (by_value) { + bind_scope_t bound_scope(scope, *this); + i = set_tag(tag, expr_t(field).calc(bound_scope), overwrite_existing); + } else { + i = set_tag(tag, string_value(field), overwrite_existing); + } + (*i).second.second = true; + break; } first = false; } @@ -239,12 +248,12 @@ namespace { value_t get_date(item_t& item) { return item.date(); } - value_t get_actual_date(item_t& item) { - return item.actual_date(); + value_t get_primary_date(item_t& item) { + return item.primary_date(); } - value_t get_effective_date(item_t& item) { - if (optional<date_t> effective = item.effective_date()) - return *effective; + value_t get_aux_date(item_t& item) { + if (optional<date_t> aux_date = item.aux_date()) + return *aux_date; return NULL_VALUE; } value_t get_note(item_t& item) { @@ -338,7 +347,10 @@ namespace { } value_t get_seq(item_t& item) { - return item.pos ? long(item.pos->sequence) : 0L; + return long(item.seq()); + } + value_t get_id(item_t& item) { + return string_value(item.id()); } value_t get_addr(item_t& item) { @@ -386,6 +398,13 @@ value_t get_comment(item_t& item) } } +void item_t::define(const symbol_t::kind_t, const string& name, + expr_t::ptr_op_t def) +{ + bind_scope_t bound_scope(*scope_t::default_scope, *this); + set_tag(name, def->calc(bound_scope)); +} + expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, const string& name) { @@ -397,9 +416,11 @@ expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, if (name == "actual") return WRAP_FUNCTOR(get_wrapper<&get_actual>); else if (name == "actual_date") - return WRAP_FUNCTOR(get_wrapper<&get_actual_date>); + return WRAP_FUNCTOR(get_wrapper<&get_primary_date>); else if (name == "addr") return WRAP_FUNCTOR(get_wrapper<&get_addr>); + else if (name == "aux_date") + return WRAP_FUNCTOR(get_wrapper<&get_aux_date>); break; case 'b': @@ -429,7 +450,7 @@ expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, else if (name == "end_pos") return WRAP_FUNCTOR(get_wrapper<&get_end_pos>); else if (name == "effective_date") - return WRAP_FUNCTOR(get_wrapper<&get_effective_date>); + return WRAP_FUNCTOR(get_wrapper<&get_aux_date>); break; case 'f': @@ -447,6 +468,8 @@ expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, case 'i': if (name == "is_account") return WRAP_FUNCTOR(get_wrapper<&ignore>); + else if (name == "id") + return WRAP_FUNCTOR(get_wrapper<&get_id>); break; case 'm': @@ -464,10 +487,12 @@ expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(get_wrapper<&get_pending>); else if (name == "parent") return WRAP_FUNCTOR(get_wrapper<&ignore>); + else if (name == "primary_date") + return WRAP_FUNCTOR(get_wrapper<&get_primary_date>); break; case 's': - if (name == "status") + if (name == "status" || name == "state") return WRAP_FUNCTOR(get_wrapper<&get_status>); else if (name == "seq") return WRAP_FUNCTOR(get_wrapper<&get_seq>); @@ -481,6 +506,8 @@ expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, case 'u': if (name == "uncleared") return WRAP_FUNCTOR(get_wrapper<&get_uncleared>); + else if (name == "uuid") + return WRAP_FUNCTOR(get_wrapper<&get_id>); break; case 'v': @@ -532,7 +559,7 @@ string item_context(const item_t& item, const string& desc) if (! (len > 0)) return empty_string; - assert(len < 8192); + assert(len < 1024 * 1024); std::ostringstream out; @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -60,8 +60,8 @@ struct position_t TRACE_CTOR(position_t, ""); } position_t(const position_t& pos) { - TRACE_CTOR(position_t, "copy"); *this = pos; + TRACE_CTOR(position_t, "copy"); } ~position_t() throw() { TRACE_DTOR(position_t); @@ -100,18 +100,20 @@ private: class item_t : public supports_flags<uint_least16_t>, public scope_t { public: -#define ITEM_NORMAL 0x00 // no flags at all, a basic posting -#define ITEM_GENERATED 0x01 // posting was not found in a journal -#define ITEM_TEMP 0x02 // posting is a managed temporary +#define ITEM_NORMAL 0x00 // no flags at all, a basic posting +#define ITEM_GENERATED 0x01 // posting was not found in a journal +#define ITEM_TEMP 0x02 // posting is a managed temporary +#define ITEM_NOTE_ON_NEXT_LINE 0x04 // did we see a note on the next line? enum state_t { UNCLEARED = 0, CLEARED, PENDING }; typedef std::pair<optional<value_t>, bool> tag_data_t; - typedef std::map<string, tag_data_t> string_map; + typedef std::map<string, tag_data_t, + function<bool(string, string)> > string_map; state_t _state; optional<date_t> _date; - optional<date_t> _date_eff; + optional<date_t> _date_aux; optional<string> note; optional<position_t> pos; optional<string_map> metadata; @@ -123,20 +125,20 @@ public: } item_t(const item_t& item) : supports_flags<uint_least16_t>(), scope_t() { - TRACE_CTOR(item_t, "copy"); copy_details(item); + TRACE_CTOR(item_t, "copy"); } virtual ~item_t() { TRACE_DTOR(item_t); } - void copy_details(const item_t& item) + virtual void copy_details(const item_t& item) { set_flags(item.flags()); set_state(item.state()); _date = item._date; - _date_eff = item._date_eff; + _date_aux = item._date_aux; note = item.note; pos = item.pos; metadata = item.metadata; @@ -149,6 +151,19 @@ public: return ! (*this == xact); } + string id() const { + if (optional<value_t> ref = get_tag(_("UUID"))) { + return ref->to_string(); + } else { + std::ostringstream buf; + buf << seq(); + return buf.str(); + } + } + std::size_t seq() const { + return pos ? pos->sequence : 0L; + } + virtual bool has_tag(const string& tag, bool inherit = true) const; virtual bool has_tag(const mask_t& tag_mask, @@ -173,7 +188,7 @@ public: scope_t& scope, bool overwrite_existing = true); - static bool use_effective_date; + static bool use_aux_date; virtual bool has_date() const { return _date; @@ -181,17 +196,17 @@ public: virtual date_t date() const { assert(_date); - if (use_effective_date) - if (optional<date_t> effective = effective_date()) - return *effective; + if (use_aux_date) + if (optional<date_t> aux = aux_date()) + return *aux; return *_date; } - virtual date_t actual_date() const { + virtual date_t primary_date() const { assert(_date); return *_date; } - virtual optional<date_t> effective_date() const { - return _date_eff; + virtual optional<date_t> aux_date() const { + return _date_aux; } void set_state(state_t new_state) { @@ -201,6 +216,8 @@ public: return _state; } + virtual void define(const symbol_t::kind_t, const string&, + expr_t::ptr_op_t); virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, const string& name); @@ -218,7 +235,7 @@ private: ar & boost::serialization::base_object<scope_t>(*this); ar & _state; ar & _date; - ar & _date_eff; + ar & _date_aux; ar & note; ar & pos; ar & metadata; diff --git a/src/iterators.cc b/src/iterators.cc index b398646e..7cc1291a 100644 --- a/src/iterators.cc +++ b/src/iterators.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -75,6 +75,65 @@ void journal_posts_iterator::increment() } } +namespace { + struct create_price_xact + { + journal_t& journal; + account_t * account; + temporaries_t& temps; + xacts_list& xact_temps; + + std::map<string, xact_t *> xacts_by_commodity; + + create_price_xact(journal_t& _journal, account_t * _account, + temporaries_t& _temps, xacts_list& _xact_temps) + : journal(_journal), account(_account), temps(_temps), + xact_temps(_xact_temps) { + TRACE_CTOR(create_price_xact, + "journal_t&, account_t *, temporaries_t&, xacts_list&"); + } + ~create_price_xact() throw() { + TRACE_DTOR(create_price_xact); + } + + void operator()(datetime_t& date, const amount_t& price) { + xact_t * xact; + string symbol = price.commodity().symbol(); + + std::map<string, xact_t *>::iterator i = + xacts_by_commodity.find(symbol); + if (i != xacts_by_commodity.end()) { + xact = (*i).second; + } else { + xact = &temps.create_xact(); + xact_temps.push_back(xact); + xact->payee = symbol; + xact->_date = date.date(); + xacts_by_commodity.insert + (std::pair<string, xact_t *>(symbol, xact)); + xact->journal = &journal; + } + + bool post_already_exists = false; + + foreach (post_t * post, xact->posts) { + if (post->date() == date.date() && post->amount == price) { + post_already_exists = true; + break; + } + } + + if (! post_already_exists) { + post_t& temp = temps.create_post(*xact, account); + temp._date = date.date(); + temp.amount = price; + + temp.xdata().datetime = date; + } + } + }; +} + void posts_commodities_iterator::reset(journal_t& journal) { journal_posts.reset(journal); @@ -85,57 +144,13 @@ void posts_commodities_iterator::reset(journal_t& journal) commodity_t& comm(post->amount.commodity()); if (comm.flags() & COMMODITY_NOMARKET) continue; - commodities.insert(&comm); + commodities.insert(&comm.referent()); } - std::map<string, xact_t *> xacts_by_commodity; - - foreach (commodity_t * comm, commodities) { - if (optional<commodity_t::varied_history_t&> history = - comm->varied_history()) { - account_t * account = journal.master->find_account(comm->symbol()); - - foreach (commodity_t::history_by_commodity_map::value_type& pair, - history->histories) { - foreach (commodity_t::history_map::value_type& hpair, - pair.second.prices) { - xact_t * xact; - string symbol = hpair.second.commodity().symbol(); - - std::map<string, xact_t *>::iterator i = - xacts_by_commodity.find(symbol); - if (i != xacts_by_commodity.end()) { - xact = (*i).second; - } else { - xact = &temps.create_xact(); - xact_temps.push_back(xact); - xact->payee = symbol; - xact->_date = hpair.first.date(); - xacts_by_commodity.insert - (std::pair<string, xact_t *>(symbol, xact)); - } - - bool post_already_exists = false; - - foreach (post_t * post, xact->posts) { - if (post->_date == hpair.first.date() && - post->amount == hpair.second) { - post_already_exists = true; - break; - } - } - - if (! post_already_exists) { - post_t& temp = temps.create_post(*xact, account); - temp._date = hpair.first.date(); - temp.amount = hpair.second; - - temp.xdata().datetime = hpair.first; - } - } - } - } - } + foreach (commodity_t * comm, commodities) + comm->map_prices + (create_price_xact(journal, journal.master->find_account(comm->symbol()), + temps, xact_temps)); xacts.reset(xact_temps.begin(), xact_temps.end()); diff --git a/src/iterators.h b/src/iterators.h index 93782400..53814666 100644 --- a/src/iterators.h +++ b/src/iterators.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -58,7 +58,15 @@ class iterator_facade_base typedef Value node_base; public: - iterator_facade_base() : m_node(NULL) {} + iterator_facade_base() : m_node(NULL) { + TRACE_CTOR(iterator_facade_base, ""); + } + iterator_facade_base(const iterator_facade_base& i) : m_node(i.m_node) { + TRACE_CTOR(iterator_facade_base, "copy"); + } + ~iterator_facade_base() throw() { + TRACE_DTOR(iterator_facade_base); + } explicit iterator_facade_base(node_base p) : m_node(p) {} @@ -87,12 +95,24 @@ class xact_posts_iterator bool posts_uninitialized; public: - xact_posts_iterator() : posts_uninitialized(true) {} + xact_posts_iterator() : posts_uninitialized(true) { + TRACE_CTOR(xact_posts_iterator, ""); + } xact_posts_iterator(xact_t& xact) : posts_uninitialized(true) { reset(xact); + TRACE_CTOR(xact_posts_iterator, "xact_t&"); + } + xact_posts_iterator(const xact_posts_iterator& i) + : iterator_facade_base<xact_posts_iterator, post_t *, + boost::forward_traversal_tag>(i), + posts_i(i.posts_i), posts_end(i.posts_end), + posts_uninitialized(i.posts_uninitialized) { + TRACE_CTOR(xact_posts_iterator, "copy"); + } + ~xact_posts_iterator() throw() { + TRACE_DTOR(xact_posts_iterator); } - ~xact_posts_iterator() throw() {} void reset(xact_t& xact) { posts_i = xact.posts.begin(); @@ -121,15 +141,28 @@ public: bool xacts_uninitialized; - xacts_iterator() : xacts_uninitialized(true) {} + xacts_iterator() : xacts_uninitialized(true) { + TRACE_CTOR(xacts_iterator, ""); + } xacts_iterator(journal_t& journal) : xacts_uninitialized(false) { reset(journal); + TRACE_CTOR(xacts_iterator, "journal_t&"); } xacts_iterator(xacts_list::iterator beg, xacts_list::iterator end) : xacts_uninitialized(false) { reset(beg, end); + TRACE_CTOR(xacts_iterator, "xacts_list::iterator, xacts_list::iterator"); + } + xacts_iterator(const xacts_iterator& i) + : iterator_facade_base<xacts_iterator, xact_t *, + boost::forward_traversal_tag>(i), + xacts_i(i.xacts_i), xacts_end(i.xacts_end), + xacts_uninitialized(i.xacts_uninitialized) { + TRACE_CTOR(xacts_iterator, "copy"); + } + ~xacts_iterator() throw() { + TRACE_DTOR(xacts_iterator); } - ~xacts_iterator() throw() {} void reset(journal_t& journal); @@ -150,11 +183,22 @@ class journal_posts_iterator xact_posts_iterator posts; public: - journal_posts_iterator() {} + journal_posts_iterator() { + TRACE_CTOR(journal_posts_iterator, ""); + } journal_posts_iterator(journal_t& journal) { reset(journal); + TRACE_CTOR(journal_posts_iterator, "journal_t&"); + } + journal_posts_iterator(const journal_posts_iterator& i) + : iterator_facade_base<journal_posts_iterator, post_t *, + boost::forward_traversal_tag>(i), + xacts(i.xacts), posts(i.posts) { + TRACE_CTOR(journal_posts_iterator, "copy"); + } + ~journal_posts_iterator() throw() { + TRACE_DTOR(journal_posts_iterator); } - ~journal_posts_iterator() throw() {} void reset(journal_t& journal); @@ -169,15 +213,27 @@ protected: journal_posts_iterator journal_posts; xacts_iterator xacts; xact_posts_iterator posts; - temporaries_t temps; xacts_list xact_temps; + temporaries_t temps; public: - posts_commodities_iterator() {} + posts_commodities_iterator() { + TRACE_CTOR(posts_commodities_iterator, ""); + } posts_commodities_iterator(journal_t& journal) { reset(journal); + TRACE_CTOR(posts_commodities_iterator, "journal_t&"); + } + posts_commodities_iterator(const posts_commodities_iterator& i) + : iterator_facade_base<posts_commodities_iterator, post_t *, + boost::forward_traversal_tag>(i), + journal_posts(i.journal_posts), xacts(i.xacts), posts(i.posts), + xact_temps(i.xact_temps), temps(i.temps) { + TRACE_CTOR(posts_commodities_iterator, "copy"); + } + ~posts_commodities_iterator() throw() { + TRACE_DTOR(posts_commodities_iterator); } - ~posts_commodities_iterator() throw() {} void reset(journal_t& journal); @@ -192,12 +248,23 @@ class basic_accounts_iterator std::list<accounts_map::const_iterator> accounts_end; public: - basic_accounts_iterator() {} + basic_accounts_iterator() { + TRACE_CTOR(basic_accounts_iterator, ""); + } basic_accounts_iterator(account_t& account) { push_back(account); increment(); + TRACE_CTOR(basic_accounts_iterator, "account_t&"); + } + basic_accounts_iterator(const basic_accounts_iterator& i) + : iterator_facade_base<basic_accounts_iterator, account_t *, + boost::forward_traversal_tag>(i), + accounts_i(i.accounts_i), accounts_end(i.accounts_end) { + TRACE_CTOR(basic_accounts_iterator, "copy"); + } + ~basic_accounts_iterator() throw() { + TRACE_DTOR(basic_accounts_iterator); } - ~basic_accounts_iterator() throw() {} void increment(); @@ -227,8 +294,20 @@ public: : sort_cmp(_sort_cmp), flatten_all(_flatten_all) { push_back(account); increment(); + TRACE_CTOR(sorted_accounts_iterator, "account_t&, expr_t, bool"); + } + sorted_accounts_iterator(const sorted_accounts_iterator& i) + : iterator_facade_base<sorted_accounts_iterator, account_t *, + boost::forward_traversal_tag>(i), + sort_cmp(i.sort_cmp), flatten_all(i.flatten_all), + accounts_list(i.accounts_list), + sorted_accounts_i(i.sorted_accounts_i), + sorted_accounts_end(i.sorted_accounts_end) { + TRACE_CTOR(sorted_accounts_iterator, "copy"); + } + ~sorted_accounts_iterator() throw() { + TRACE_DTOR(sorted_accounts_iterator); } - ~sorted_accounts_iterator() throw() {} void increment(); diff --git a/src/journal.cc b/src/journal.cc index 0691954f..49d86ff0 100644 --- a/src/journal.cc +++ b/src/journal.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -32,33 +32,37 @@ #include <system.hh> #include "journal.h" +#include "context.h" #include "amount.h" #include "commodity.h" #include "pool.h" #include "xact.h" +#include "post.h" #include "account.h" namespace ledger { journal_t::journal_t() { - TRACE_CTOR(journal_t, ""); initialize(); + TRACE_CTOR(journal_t, ""); } +#if 0 journal_t::journal_t(const path& pathname) { - TRACE_CTOR(journal_t, "path"); initialize(); read(pathname); + TRACE_CTOR(journal_t, "path"); } journal_t::journal_t(const string& str) { - TRACE_CTOR(journal_t, "string"); initialize(); read(str); + TRACE_CTOR(journal_t, "string"); } +#endif journal_t::~journal_t() { @@ -80,9 +84,18 @@ journal_t::~journal_t() void journal_t::initialize() { - master = new account_t; - bucket = NULL; - was_loaded = false; + master = new account_t; + bucket = NULL; + fixed_accounts = false; + fixed_payees = false; + fixed_commodities = false; + fixed_metadata = false; + current_context = NULL; + was_loaded = false; + force_checking = false; + check_payees = false; + day_break = false; + checking_style = CHECK_PERMISSIVE; } void journal_t::add_account(account_t * acct) @@ -105,6 +118,201 @@ account_t * journal_t::find_account_re(const string& regexp) return master->find_account_re(regexp); } +account_t * journal_t::register_account(const string& name, post_t * post, + account_t * master_account) +{ + account_t * result = NULL; + + // If there any account aliases, substitute before creating an account + // object. + if (account_aliases.size() > 0) { + accounts_map::const_iterator i = account_aliases.find(name); + if (i != account_aliases.end()) + result = (*i).second; + } + + // Create the account object and associate it with the journal; this + // is registering the account. + if (! result) + result = master_account->find_account(name); + + // If the account name being registered is "Unknown", check whether + // the payee indicates an account that should be used. + if (result->name == _("Unknown")) { + foreach (account_mapping_t& value, payees_for_unknown_accounts) { + if (value.first.match(post->xact->payee)) { + result = value.second; + break; + } + } + } + + // Now that we have an account, make certain that the account is + // "known", if the user has requested validation of that fact. + if (checking_style == CHECK_WARNING || checking_style == CHECK_ERROR) { + if (! result->has_flags(ACCOUNT_KNOWN)) { + if (! post) { + if (force_checking) + fixed_accounts = true; + result->add_flags(ACCOUNT_KNOWN); + } + else if (! fixed_accounts && post->_state != item_t::UNCLEARED) { + result->add_flags(ACCOUNT_KNOWN); + } + else if (checking_style == CHECK_WARNING) { + current_context->warning(STR(_("Unknown account '%1'") + << result->fullname())); + } + else if (checking_style == CHECK_ERROR) { + throw_(parse_error, _("Unknown account '%1'") << result->fullname()); + } + } + } + + return result; +} + +string journal_t::register_payee(const string& name, xact_t * xact) +{ + string payee; + + if (check_payees && + (checking_style == CHECK_WARNING || checking_style == CHECK_ERROR)) { + std::set<string>::iterator i = known_payees.find(name); + + if (i == known_payees.end()) { + if (! xact) { + if (force_checking) + fixed_payees = true; + known_payees.insert(name); + } + else if (! fixed_payees && xact->_state != item_t::UNCLEARED) { + known_payees.insert(name); + } + else if (checking_style == CHECK_WARNING) { + current_context->warning(STR(_("Unknown payee '%1'") << name)); + } + else if (checking_style == CHECK_ERROR) { + throw_(parse_error, _("Unknown payee '%1'") << name); + } + } + } + + foreach (payee_mapping_t& value, payee_mappings) { + if (value.first.match(name)) { + payee = value.second; + break; + } + } + + return payee.empty() ? name : payee; +} + +void journal_t::register_commodity(commodity_t& comm, + variant<int, xact_t *, post_t *> context) +{ + if (checking_style == CHECK_WARNING || checking_style == CHECK_ERROR) { + if (! comm.has_flags(COMMODITY_KNOWN)) { + if (context.which() == 0) { + if (force_checking) + fixed_commodities = true; + comm.add_flags(COMMODITY_KNOWN); + } + else if (! fixed_commodities && + ((context.which() == 1 && + boost::get<xact_t *>(context)->_state != item_t::UNCLEARED) || + (context.which() == 2 && + boost::get<post_t *>(context)->_state != item_t::UNCLEARED))) { + comm.add_flags(COMMODITY_KNOWN); + } + else if (checking_style == CHECK_WARNING) { + current_context->warning(STR(_("Unknown commodity '%1'") << comm)); + } + else if (checking_style == CHECK_ERROR) { + throw_(parse_error, _("Unknown commodity '%1'") << comm); + } + } + } +} + +void journal_t::register_metadata(const string& key, const value_t& value, + variant<int, xact_t *, post_t *> context) +{ + if (checking_style == CHECK_WARNING || checking_style == CHECK_ERROR) { + std::set<string>::iterator i = known_tags.find(key); + + if (i == known_tags.end()) { + if (context.which() == 0) { + if (force_checking) + fixed_metadata = true; + known_tags.insert(key); + } + else if (! fixed_metadata && + ((context.which() == 1 && + boost::get<xact_t *>(context)->_state != item_t::UNCLEARED) || + (context.which() == 2 && + boost::get<post_t *>(context)->_state != item_t::UNCLEARED))) { + known_tags.insert(key); + } + else if (checking_style == CHECK_WARNING) { + current_context->warning(STR(_("Unknown metadata tag '%1'") << key)); + } + else if (checking_style == CHECK_ERROR) { + throw_(parse_error, _("Unknown metadata tag '%1'") << key); + } + } + } + + if (! value.is_null()) { + std::pair<tag_check_exprs_map::iterator, + tag_check_exprs_map::iterator> range = + tag_check_exprs.equal_range(key); + + for (tag_check_exprs_map::iterator i = range.first; + i != range.second; + ++i) { + bind_scope_t bound_scope + (*current_context->scope, + context.which() == 1 ? + static_cast<scope_t&>(*boost::get<xact_t *>(context)) : + static_cast<scope_t&>(*boost::get<post_t *>(context))); + value_scope_t val_scope(bound_scope, value); + + if (! (*i).second.first.calc(val_scope).to_boolean()) { + if ((*i).second.second == expr_t::EXPR_ASSERTION) + throw_(parse_error, + _("Metadata assertion failed for (%1: %2): %3") + << key << value << (*i).second.first); + else + current_context->warning + (STR(_("Metadata check failed for (%1: %2): %3") + << key << value << (*i).second.first)); + } + } + } +} + +namespace { + void check_all_metadata(journal_t& journal, + variant<int, xact_t *, post_t *> context) + { + xact_t * xact = context.which() == 1 ? boost::get<xact_t *>(context) : NULL; + post_t * post = context.which() == 2 ? boost::get<post_t *>(context) : NULL; + + if ((xact || post) && xact ? xact->metadata : post->metadata) { + foreach (const item_t::string_map::value_type& pair, + xact ? *xact->metadata : *post->metadata) { + const string& key(pair.first); + + if (optional<value_t> value = pair.second.first) + journal.register_metadata(key, *value, context); + else + journal.register_metadata(key, NULL_VALUE, context); + } + } + } +} + bool journal_t::add_xact(xact_t * xact) { xact->journal = this; @@ -115,6 +323,29 @@ bool journal_t::add_xact(xact_t * xact) } extend_xact(xact); + check_all_metadata(*this, xact); + + foreach (post_t * post, xact->posts) { + extend_post(*post, *this); + check_all_metadata(*this, post); + } + + // If a transaction with this UUID has already been seen, simply do + // not add this one to the journal. However, all automated checks + // will have been performed by extend_xact, so asserts can still be + // applied to it. + if (optional<value_t> ref = xact->get_tag(_("UUID"))) { + std::pair<checksum_map_t::iterator, bool> result + = checksum_map.insert(checksum_map_t::value_type(ref->to_string(), xact)); + if (! result.second) { + // jww (2012-02-27): Confirm that the xact in + // (*result.first).second is exact match in its significant + // details to xact. + xact->journal = NULL; + return false; + } + } + xacts.push_back(xact); return true; @@ -123,7 +354,7 @@ bool journal_t::add_xact(xact_t * xact) void journal_t::extend_xact(xact_base_t * xact) { foreach (auto_xact_t * auto_xact, auto_xacts) - auto_xact->extend_xact(*xact); + auto_xact->extend_xact(*xact, *current_context); } bool journal_t::remove_xact(xact_t * xact) @@ -144,28 +375,36 @@ bool journal_t::remove_xact(xact_t * xact) return true; } -std::size_t journal_t::read(std::istream& in, - const path& pathname, - account_t * master_alt, - scope_t * scope) +std::size_t journal_t::read(parse_context_stack_t& context) { std::size_t count = 0; try { - if (! scope) - scope = scope_t::default_scope; + parse_context_t& current(context.get_current()); + current_context = ¤t; + + current.count = 0; + if (! current.scope) + current.scope = scope_t::default_scope; - if (! scope) + if (! current.scope) throw_(std::runtime_error, _("No default scope in which to read journal file '%1'") - << pathname); + << current.pathname); - value_t strict = expr_t("strict").calc(*scope); + if (! current.master) + current.master = master; - count = parse(in, *scope, master_alt ? master_alt : master, - &pathname, strict.to_boolean()); + count = read_textual(context); + if (count > 0) { + if (! current.pathname.empty()) + sources.push_back(fileinfo_t(current.pathname)); + else + sources.push_back(fileinfo_t()); + } } catch (...) { clear_xdata(); + current_context = NULL; throw; } @@ -177,23 +416,6 @@ std::size_t journal_t::read(std::istream& in, return count; } -std::size_t journal_t::read(const path& pathname, - account_t * master_account, - scope_t * scope) -{ - path filename = resolve_path(pathname); - - if (! exists(filename)) - throw_(std::runtime_error, - _("Cannot read journal file '%1'") << filename); - - ifstream stream(filename); - std::size_t count = read(stream, filename, master_account, scope); - if (count > 0) - sources.push_back(fileinfo_t(filename)); - return count; -} - bool journal_t::has_xdata() { foreach (xact_t * xact, xacts) diff --git a/src/journal.h b/src/journal.h index ca6b6e4f..a7a84447 100644 --- a/src/journal.h +++ b/src/journal.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -45,6 +45,7 @@ #include "utils.h" #include "times.h" #include "mask.h" +#include "expr.h" namespace ledger { @@ -52,17 +53,22 @@ class xact_base_t; class xact_t; class auto_xact_t; class period_xact_t; +class post_t; class account_t; -class scope_t; - -typedef std::list<xact_t *> xacts_list; -typedef std::list<auto_xact_t *> auto_xacts_list; -typedef std::list<period_xact_t *> period_xacts_list; - -typedef std::pair<mask_t, string> payee_mapping_t; -typedef std::list<payee_mapping_t> payee_mappings_t; -typedef std::pair<mask_t, account_t *> account_mapping_t; -typedef std::list<account_mapping_t> account_mappings_t; +class parse_context_t; +class parse_context_stack_t; + +typedef std::list<xact_t *> xacts_list; +typedef std::list<auto_xact_t *> auto_xacts_list; +typedef std::list<period_xact_t *> period_xacts_list; +typedef std::pair<mask_t, string> payee_mapping_t; +typedef std::list<payee_mapping_t> payee_mappings_t; +typedef std::pair<mask_t, account_t *> account_mapping_t; +typedef std::list<account_mapping_t> account_mappings_t; +typedef std::map<const string, account_t *> accounts_map; +typedef std::map<string, xact_t *> checksum_map_t; +typedef std::multimap<string, + expr_t::check_expr_pair> tag_check_exprs_map; class journal_t : public noncopyable { @@ -79,9 +85,9 @@ public: } fileinfo_t(const path& _filename) : filename(_filename), from_stream(false) { - TRACE_CTOR(journal_t::fileinfo_t, "const path&"); size = file_size(*filename); modtime = posix_time::from_time_t(last_write_time(*filename)); + TRACE_CTOR(journal_t::fileinfo_t, "const path&"); } fileinfo_t(const fileinfo_t& info) : filename(info.filename), size(info.size), @@ -115,13 +121,36 @@ public: auto_xacts_list auto_xacts; period_xacts_list period_xacts; std::list<fileinfo_t> sources; + std::set<string> known_payees; + std::set<string> known_tags; + bool fixed_accounts; + bool fixed_payees; + bool fixed_commodities; + bool fixed_metadata; + bool was_loaded; + bool force_checking; + bool check_payees; + bool day_break; payee_mappings_t payee_mappings; account_mappings_t account_mappings; - bool was_loaded; + accounts_map account_aliases; + account_mappings_t payees_for_unknown_accounts; + checksum_map_t checksum_map; + tag_check_exprs_map tag_check_exprs; + optional<expr_t> value_expr; + parse_context_t * current_context; + + enum checking_style_t { + CHECK_PERMISSIVE, + CHECK_WARNING, + CHECK_ERROR + } checking_style; journal_t(); +#if 0 journal_t(const path& pathname); journal_t(const string& str); +#endif ~journal_t(); void initialize(); @@ -133,13 +162,19 @@ public: return sources.end(); } - // These four methods are delegated to the current session, since all - // accounts processed are gathered together at the session level. void add_account(account_t * acct); bool remove_account(account_t * acct); account_t * find_account(const string& name, bool auto_create = true); account_t * find_account_re(const string& regexp); + account_t * register_account(const string& name, post_t * post, + account_t * master = NULL); + string register_payee(const string& name, xact_t * xact); + void register_commodity(commodity_t& comm, + variant<int, xact_t *, post_t *> context); + void register_metadata(const string& key, const value_t& value, + variant<int, xact_t *, post_t *> context); + bool add_xact(xact_t * xact); void extend_xact(xact_base_t * xact); bool remove_xact(xact_t * xact); @@ -163,25 +198,16 @@ public: return period_xacts.end(); } - std::size_t read(std::istream& in, - const path& pathname, - account_t * master = NULL, - scope_t * scope = NULL); - std::size_t read(const path& pathname, - account_t * master = NULL, - scope_t * scope = NULL); - - std::size_t parse(std::istream& in, - scope_t& session_scope, - account_t * master = NULL, - const path * original_file = NULL, - bool strict = false); + std::size_t read(parse_context_stack_t& context); bool has_xdata(); void clear_xdata(); bool valid() const; +private: + std::size_t read_textual(parse_context_stack_t& context); + #if defined(HAVE_BOOST_SERIALIZATION) private: /** Serialization. */ @@ -198,6 +224,7 @@ private: ar & sources; ar & payee_mappings; ar & account_mappings; + ar & checksum_map; } #endif // HAVE_BOOST_SERIALIZATION }; diff --git a/src/lookup.cc b/src/lookup.cc index 452727d6..6f0c33ea 100644 --- a/src/lookup.cc +++ b/src/lookup.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -186,12 +186,14 @@ lookup_probable_account(const string& ident, if (in_order_match && pos - index < 3) addend++; +#if 0 #if !defined(HAVE_BOOST_REGEX_UNICODE) if (pos == 0 || (pos > 0 && !std::isalnum(value_key[pos - 1]))) addend++; #else // jww (2010-03-07): Not yet implemented #endif +#endif last_match_pos = pos; } diff --git a/src/lookup.h b/src/lookup.h index 8e83b84e..ba64b0b5 100644 --- a/src/lookup.h +++ b/src/lookup.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/main.cc b/src/main.cc index 820f920d..0130d5c6 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -58,6 +58,7 @@ int main(int argc, char * argv[], char * envp[]) // --verbose ; turns on logging // --debug CATEGORY ; turns on debug logging // --trace LEVEL ; turns on trace logging + // --memory ; turns on memory usage tracing handle_debug_options(argc, argv); #if defined(VERIFY_ON) IF_VERIFY() initialize_memory_tracing(); @@ -80,13 +81,12 @@ int main(int argc, char * argv[], char * envp[]) ::textdomain("ledger"); #endif - std::auto_ptr<global_scope_t> global_scope; + global_scope_t * global_scope = NULL; try { // Create the session object, which maintains nearly all state relating to // this invocation of Ledger; and register all known journal parsers. - global_scope.reset(new global_scope_t(envp)); - + global_scope = new global_scope_t(envp); global_scope->session().set_flush_on_next_data_file(true); // Construct an STL-style argument list from the process command arguments @@ -95,8 +95,7 @@ int main(int argc, char * argv[], char * envp[]) args.push_back(argv[i]); // Look for options and a command verb in the command-line arguments - bind_scope_t bound_scope(*global_scope.get(), global_scope->report()); - + bind_scope_t bound_scope(*global_scope, global_scope->report()); args = global_scope->read_command_arguments(bound_scope, args); if (global_scope->HANDLED(script_)) { @@ -112,8 +111,8 @@ int main(int argc, char * argv[], char * envp[]) char * p = skip_ws(line); if (*p && *p != '#') - status = global_scope->execute_command_wrapper(split_arguments(p), - true); + status = + global_scope->execute_command_wrapper(split_arguments(p), true); } } else if (! args.empty()) { @@ -187,30 +186,35 @@ int main(int argc, char * argv[], char * envp[]) } } catch (const std::exception& err) { - if (global_scope.get()) + if (global_scope) global_scope->report_error(err); else std::cerr << "Exception during initialization: " << err.what() << std::endl; } - catch (int _status) { - status = _status; // used for a "quick" exit, and is used only - // if help text (such as --help) was displayed + catch (const error_count& errors) { + // used for a "quick" exit, and is used only if help text (such as + // --help) was displayed + status = static_cast<int>(errors.count); } // If memory verification is being performed (which can be very slow), clean // up everything by closing the session and deleting the session object, and // then shutting down the memory tracing subsystem. Otherwise, let it all // leak because we're about to exit anyway. +#if defined(VERIFY_ON) IF_VERIFY() { - global_scope.reset(); + checked_delete(global_scope); INFO("Ledger ended (Boost/libstdc++ may still hold memory)"); #if defined(VERIFY_ON) shutdown_memory_tracing(); #endif - } else { - INFO("Ledger ended"); + } else +#endif + { + global_scope->quick_close(); + INFO("Ledger ended"); // let global_scope leak! } // Return the final status to the operating system, either 1 for error or 0 diff --git a/src/mask.cc b/src/mask.cc index 52907cfe..afb68ca0 100644 --- a/src/mask.cc +++ b/src/mask.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -37,8 +37,8 @@ namespace ledger { mask_t::mask_t(const string& pat) : expr() { - TRACE_CTOR(mask_t, "const string&"); *this = pat; + TRACE_CTOR(mask_t, "const string&"); } mask_t& mask_t::operator=(const string& pat) @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -38,31 +38,41 @@ namespace ledger { -namespace { - value_t split_cons_expr(expr_t::ptr_op_t op) - { - if (op->kind == expr_t::op_t::O_CONS) { - value_t seq; - seq.push_back(expr_value(op->left())); - - expr_t::ptr_op_t next = op->right(); - while (next) { - expr_t::ptr_op_t value_op; - if (next->kind == expr_t::op_t::O_CONS) { - value_op = next->left(); - next = next->right(); - } else { - value_op = next; - next = NULL; - } - seq.push_back(expr_value(value_op)); +void intrusive_ptr_add_ref(const expr_t::op_t * op) +{ + op->acquire(); +} + +void intrusive_ptr_release(const expr_t::op_t * op) +{ + op->release(); +} + +value_t split_cons_expr(expr_t::ptr_op_t op) +{ + if (op->kind == expr_t::op_t::O_CONS) { + value_t seq; + seq.push_back(expr_value(op->left())); + + expr_t::ptr_op_t next = op->right(); + while (next) { + expr_t::ptr_op_t value_op; + if (next->kind == expr_t::op_t::O_CONS) { + value_op = next->left(); + next = next->has_right() ? next->right() : NULL; + } else { + value_op = next; + next = NULL; } - return seq; - } else { - return expr_value(op); + seq.push_back(expr_value(value_op)); } + return seq; + } else { + return expr_value(op); } +} +namespace { inline void check_type_context(scope_t& scope, value_t& result) { if (scope.type_required() && @@ -76,12 +86,31 @@ namespace { } } -expr_t::ptr_op_t expr_t::op_t::compile(scope_t& scope, const int depth) +expr_t::ptr_op_t expr_t::op_t::compile(scope_t& scope, const int depth, + scope_t * param_scope) { - if (is_ident()) { - DEBUG("expr.compile", "lookup: " << as_ident()); + scope_t * scope_ptr = &scope; + unique_ptr<scope_t> bound_scope; + expr_t::ptr_op_t result; + +#if defined(DEBUG_ON) + if (SHOW_DEBUG("expr.compile")) { + for (int i = 0; i < depth; i++) + ledger::_log_buffer << '.'; + DEBUG("expr.compile", ""); + } +#endif + + assert(kind < LAST); - if (ptr_op_t def = scope.lookup(symbol_t::FUNCTION, as_ident())) { + if (is_ident()) { + DEBUG("expr.compile", "Lookup: " << as_ident() << " in " << scope_ptr); + ptr_op_t def; + if (param_scope) + def = param_scope->lookup(symbol_t::FUNCTION, as_ident()); + if (! def) + def = scope_ptr->lookup(symbol_t::FUNCTION, as_ident()); + if (def) { // Identifier references are first looked up at the point of // definition, and then at the point of every use if they could // not be found there. @@ -91,70 +120,144 @@ expr_t::ptr_op_t expr_t::op_t::compile(scope_t& scope, const int depth) def->dump(*_log_stream, 0); } #endif // defined(DEBUG_ON) - return copy(def); + result = copy(def); } else if (left()) { - return copy(); + result = copy(); + } + else { + result = this; } - return this; } - - if (kind < TERMINALS) - return this; - - if (kind == O_DEFINE) { + else if (is_scope()) { + shared_ptr<scope_t> subscope(new symbol_scope_t(*scope_t::empty_scope)); + set_scope(subscope); + bound_scope.reset(new bind_scope_t(*scope_ptr, *subscope.get())); + scope_ptr = bound_scope.get(); + } + else if (kind < TERMINALS) { + result = this; + } + else if (kind == O_DEFINE) { switch (left()->kind) { - case IDENT: - scope.define(symbol_t::FUNCTION, left()->as_ident(), right()); + case IDENT: { + ptr_op_t node(right()->compile(*scope_ptr, depth + 1, param_scope)); + + DEBUG("expr.compile", + "Defining " << left()->as_ident() << " in " << scope_ptr); + scope_ptr->define(symbol_t::FUNCTION, left()->as_ident(), node); break; + } + case O_CALL: if (left()->left()->is_ident()) { ptr_op_t node(new op_t(op_t::O_LAMBDA)); node->set_left(left()->right()); node->set_right(right()); - scope.define(symbol_t::FUNCTION, left()->left()->as_ident(), node); - } else { - throw_(compile_error, _("Invalid function definition")); + node = node->compile(*scope_ptr, depth + 1, param_scope); + + DEBUG("expr.compile", + "Defining " << left()->left()->as_ident() << " in " << scope_ptr); + scope_ptr->define(symbol_t::FUNCTION, left()->left()->as_ident(), node); + break; } - break; + // fall through... + default: throw_(compile_error, _("Invalid function definition")); } - return wrap_value(value_t()); + result = wrap_value(NULL_VALUE); + } + else if (kind == O_LAMBDA) { + symbol_scope_t params(param_scope ? *param_scope : *scope_t::empty_scope); + + for (ptr_op_t sym = left(); + sym; + sym = sym->has_right() ? sym->right() : NULL) { + ptr_op_t varname = sym->kind == O_CONS ? sym->left() : sym; + + if (! varname->is_ident()) { + std::ostringstream buf; + varname->dump(buf, 0); + throw_(calc_error, + _("Invalid function or lambda parameter: %1") << buf.str()); + } else { + DEBUG("expr.compile", + "Defining function parameter " << varname->as_ident()); + params.define(symbol_t::FUNCTION, varname->as_ident(), + new op_t(PLUG)); + } + } + + ptr_op_t rhs(right()->compile(*scope_ptr, depth + 1, ¶ms)); + if (rhs == right()) + result = this; + else + result = copy(left(), rhs); } - ptr_op_t lhs(left()->compile(scope, depth)); - ptr_op_t rhs(kind > UNARY_OPERATORS && has_right() ? - (kind == O_LOOKUP ? right() : - right()->compile(scope, depth)) : NULL); + if (! result) { + if (! left()) + throw_(calc_error, _("Syntax error")); + + ptr_op_t lhs(left()->compile(*scope_ptr, depth + 1, param_scope)); + ptr_op_t rhs(kind > UNARY_OPERATORS && has_right() ? + (kind == O_LOOKUP ? right() : + right()->compile(*scope_ptr, depth + 1, param_scope)) : NULL); + + if (lhs == left() && (! rhs || rhs == right())) { + result = this; + } else { + ptr_op_t intermediate(copy(lhs, rhs)); + + // Reduce constants immediately if possible + if ((! lhs || lhs->is_value()) && (! rhs || rhs->is_value())) + result = wrap_value(intermediate->calc(*scope_ptr, NULL, depth + 1)); + else + result = intermediate; + } + } - if (lhs == left() && (! rhs || rhs == right())) - return this; +#if defined(DEBUG_ON) + if (SHOW_DEBUG("expr.compile")) { + for (int i = 0; i < depth; i++) + ledger::_log_buffer << '.'; + DEBUG("expr.compile", ""); + } +#endif - ptr_op_t intermediate(copy(lhs, rhs)); + return result; +} - // Reduce constants immediately if possible - if ((! lhs || lhs->is_value()) && (! rhs || rhs->is_value())) - return wrap_value(intermediate->calc(scope, NULL, depth)); +namespace { + expr_t::ptr_op_t lookup_ident(expr_t::ptr_op_t op, scope_t& scope) + { + expr_t::ptr_op_t def = op->left(); - return intermediate; + // If no definition was pre-compiled for this identifier, look it up + // in the current scope. + if (! def || def->kind == expr_t::op_t::PLUG) { + DEBUG("scope.symbols", "Looking for IDENT '" << op->as_ident() << "'"); + def = scope.lookup(symbol_t::FUNCTION, op->as_ident()); + } + if (! def) + throw_(calc_error, _("Unknown identifier '%1'") << op->as_ident()); + return def; + } } value_t expr_t::op_t::calc(scope_t& scope, ptr_op_t * locus, const int depth) { -#if defined(DEBUG_ON) - bool skip_debug = false; -#endif try { value_t result; #if defined(DEBUG_ON) - if (! skip_debug && SHOW_DEBUG("expr.calc")) { + if (SHOW_DEBUG("expr.calc")) { for (int i = 0; i < depth; i++) ledger::_log_buffer << '.'; - ledger::_log_buffer << op_context(this) << " => ..."; + ledger::_log_buffer << op_context(this) << " => ..."; DEBUG("expr.calc", ""); } #endif @@ -165,77 +268,38 @@ value_t expr_t::op_t::calc(scope_t& scope, ptr_op_t * locus, const int depth) break; case O_DEFINE: - //result = left()->calc(scope, locus, depth + 1); result = NULL_VALUE; break; - case IDENT: { - ptr_op_t definition = left(); - if (! definition) { - // If no definition was pre-compiled for this identifier, look it - // up in the current scope. - definition = scope.lookup(symbol_t::FUNCTION, as_ident()); + case IDENT: + if (ptr_op_t definition = lookup_ident(this, scope)) { + // Evaluating an identifier is the same as calling its definition + // directly + result = definition->calc(scope, locus, depth + 1); + check_type_context(scope, result); } - if (! definition) - throw_(calc_error, _("Unknown identifier '%1'") << as_ident()); - - // Evaluating an identifier is the same as calling its definition - // directly, so we create an empty call_scope_t to reflect the scope for - // this implicit call. - call_scope_t call_args(scope, locus, depth); - result = definition->compile(call_args, depth + 1) - ->calc(call_args, locus, depth + 1); - check_type_context(scope, result); break; - } case FUNCTION: { - // Evaluating a FUNCTION is the same as calling it directly; this happens - // when certain functions-that-look-like-variables (such as "amount") are - // resolved. - call_scope_t call_args(scope, locus, depth); + // Evaluating a FUNCTION is the same as calling it directly; this + // happens when certain functions-that-look-like-variables (such as + // "amount") are resolved. + call_scope_t call_args(scope, locus, depth + 1); result = as_function()(call_args); check_type_context(scope, result); -#if defined(DEBUG_ON) - skip_debug = true; -#endif break; } - case O_LAMBDA: { - call_scope_t& call_args(downcast<call_scope_t>(scope)); - std::size_t args_count(call_args.size()); - std::size_t args_index(0); - symbol_scope_t call_scope(call_args); - ptr_op_t sym(left()); - - for (; sym; sym = sym->has_right() ? sym->right() : NULL) { - ptr_op_t varname = sym; - if (sym->kind == O_CONS) - varname = sym->left(); - - if (! varname->is_ident()) { - throw_(calc_error, _("Invalid function definition")); - } - else if (args_index == args_count) { - call_scope.define(symbol_t::FUNCTION, varname->as_ident(), - wrap_value(NULL_VALUE)); - } - else { - DEBUG("expr.compile", - "Defining function parameter " << varname->as_ident()); - call_scope.define(symbol_t::FUNCTION, varname->as_ident(), - wrap_value(call_args[args_index++])); - } + case SCOPE: + assert(! is_scope_unset()); + if (is_scope_unset()) { + symbol_scope_t subscope(scope); + result = left()->calc(subscope, locus, depth + 1); + } else { + bind_scope_t bound_scope(scope, *as_scope()); + result = left()->calc(bound_scope, locus, depth + 1); } - - if (args_index < args_count) - throw_(calc_error, - _("Too few arguments in function call (saw %1)") << args_count); - - result = right()->calc(call_scope, locus, depth + 1); break; - } case O_LOOKUP: { context_scope_t context_scope(scope, value_t::SCOPE); @@ -252,43 +316,14 @@ value_t expr_t::op_t::calc(scope_t& scope, ptr_op_t * locus, const int depth) break; } - case O_CALL: { - call_scope_t call_args(scope, locus, depth); - if (has_right()) - call_args.set_args(split_cons_expr(right())); - - ptr_op_t func = left(); - const string& name(func->as_ident()); - - func = func->left(); - if (! func) - func = scope.lookup(symbol_t::FUNCTION, name); - if (! func) - throw_(calc_error, _("Calling unknown function '%1'") << name); - -#if defined(DEBUG_ON) - if (! skip_debug && SHOW_DEBUG("expr.calc")) { - for (int i = 0; i < depth; i++) - ledger::_log_buffer << '.'; - ledger::_log_buffer << " args: "; - if (call_args.args.is_sequence()) { - foreach (value_t& arg, call_args) - ledger::_log_buffer << arg << " "; - } else { - ledger::_log_buffer << call_args.args[0] << " "; - } - DEBUG("expr.calc", ""); - } -#endif - - if (func->is_function()) - result = func->as_function()(call_args); - else - result = func->calc(call_args, locus, depth + 1); - + case O_CALL: + result = calc_call(scope, locus, depth); check_type_context(scope, result); break; - } + + case O_LAMBDA: + result = expr_value(this); + break; case O_MATCH: result = (right()->calc(scope, locus, depth + 1).as_mask() @@ -358,7 +393,6 @@ value_t expr_t::op_t::calc(scope_t& scope, ptr_op_t * locus, const int depth) case O_QUERY: assert(right()); assert(right()->kind == O_COLON); - if (value_t temp = left()->calc(scope, locus, depth + 1)) result = right()->left()->calc(scope, locus, depth + 1); else @@ -370,63 +404,19 @@ value_t expr_t::op_t::calc(scope_t& scope, ptr_op_t * locus, const int depth) break; case O_CONS: - result = left()->calc(scope, locus, depth + 1); - DEBUG("op.cons", "car = " << result); - - if (has_right()) { - value_t temp; - temp.push_back(result); - - ptr_op_t next = right(); - while (next) { - ptr_op_t value_op; - if (next->kind == O_CONS) { - value_op = next->left(); - next = next->right(); - } else { - value_op = next; - next = NULL; - } - temp.push_back(value_op->calc(scope, locus, depth + 1)); - DEBUG("op.cons", "temp now = " << temp); - } - result = temp; - } + result = calc_cons(scope, locus, depth); break; - case O_SEQ: { - symbol_scope_t seq_scope(scope); - - // An O_SEQ is very similar to an O_CONS except that only the last result - // value in the series is kept. O_CONS builds up a list. - // - // Another feature of O_SEQ is that it pushes a new symbol scope onto the - // stack. - result = left()->calc(seq_scope, locus, depth + 1); - - if (has_right()) { - ptr_op_t next = right(); - while (next) { - ptr_op_t value_op; - if (next->kind == O_SEQ) { - value_op = next->left(); - next = next->right(); - } else { - value_op = next; - next = NULL; - } - result = value_op->calc(seq_scope, locus, depth + 1); - } - } + case O_SEQ: + result = calc_seq(scope, locus, depth); break; - } default: throw_(calc_error, _("Unexpected expr node '%1'") << op_context(this)); } #if defined(DEBUG_ON) - if (! skip_debug && SHOW_DEBUG("expr.calc")) { + if (SHOW_DEBUG("expr.calc")) { for (int i = 0; i < depth; i++) ledger::_log_buffer << '.'; ledger::_log_buffer << op_context(this) << " => "; @@ -446,6 +436,184 @@ value_t expr_t::op_t::calc(scope_t& scope, ptr_op_t * locus, const int depth) } namespace { + expr_t::ptr_op_t find_definition(expr_t::ptr_op_t op, scope_t& scope, + expr_t::ptr_op_t * locus, const int depth, + int recursion_depth = 0) + { + // If the object we are apply call notation to is a FUNCTION value + // or a O_LAMBDA expression, then this is the object we want to + // call. + if (op->is_function() || op->kind == expr_t::op_t::O_LAMBDA) + return op; + + if (recursion_depth > 256) + throw_(value_error, _("Function recursion_depth too deep (> 256)")); + + // If it's an identifier, look up its definition and see if it's a + // function. + if (op->is_ident()) + return find_definition(lookup_ident(op, scope), scope, + locus, depth, recursion_depth + 1); + + // Value objects might be callable if they contain an expression. + if (op->is_value()) { + value_t def(op->as_value()); + if (is_expr(def)) + return find_definition(as_expr(def), scope, locus, depth, + recursion_depth + 1); + else + throw_(value_error, _("Cannot call %1 as a function") << def.label()); + } + + // Resolve ordinary expressions. + return find_definition(expr_t::op_t::wrap_value(op->calc(scope, locus, + depth + 1)), + scope, locus, depth + 1, recursion_depth + 1); + } + + value_t call_lambda(expr_t::ptr_op_t func, scope_t& scope, + call_scope_t& call_args, expr_t::ptr_op_t * locus, + const int depth) + { + std::size_t args_index(0); + std::size_t args_count(call_args.size()); + + symbol_scope_t args_scope(*scope_t::empty_scope); + + for (expr_t::ptr_op_t sym = func->left(); + sym; + sym = sym->has_right() ? sym->right() : NULL) { + expr_t::ptr_op_t varname = + sym->kind == expr_t::op_t::O_CONS ? sym->left() : sym; + if (! varname->is_ident()) { + throw_(calc_error, _("Invalid function definition")); + } + else if (args_index == args_count) { + DEBUG("expr.calc", "Defining function argument as null: " + << varname->as_ident()); + args_scope.define(symbol_t::FUNCTION, varname->as_ident(), + expr_t::op_t::wrap_value(NULL_VALUE)); + } + else { + DEBUG("expr.calc", "Defining function argument from call_args: " + << varname->as_ident()); + args_scope.define(symbol_t::FUNCTION, varname->as_ident(), + expr_t::op_t::wrap_value(call_args[args_index++])); + } + } + + if (args_index < args_count) + throw_(calc_error, + _("Too few arguments in function call (saw %1, wanted %2)") + << args_count << args_index); + + if (func->right()->is_scope()) { + bind_scope_t outer_scope(scope, *func->right()->as_scope()); + bind_scope_t bound_scope(outer_scope, args_scope); + + return func->right()->left()->calc(bound_scope, locus, depth + 1); + } else { + return func->right()->calc(args_scope, locus, depth + 1); + } + } +} + + +value_t expr_t::op_t::call(const value_t& args, scope_t& scope, + ptr_op_t * locus, const int depth) +{ + call_scope_t call_args(scope, locus, depth + 1); + call_args.set_args(args); + + if (is_function()) + return as_function()(call_args); + else if (kind == O_LAMBDA) + return call_lambda(this, scope, call_args, locus, depth); + else + return find_definition(this, scope, locus, depth) + ->calc(call_args, locus, depth); +} + +value_t expr_t::op_t::calc_call(scope_t& scope, ptr_op_t * locus, + const int depth) +{ + ptr_op_t func = left(); + string name = func->is_ident() ? func->as_ident() : "<value expr>"; + + func = find_definition(func, scope, locus, depth); + + call_scope_t call_args(scope, locus, depth + 1); + if (has_right()) + call_args.set_args(split_cons_expr(right())); + + try { + if (func->is_function()) { + return func->as_function()(call_args); + } else { + assert(func->kind == O_LAMBDA); + return call_lambda(func, scope, call_args, locus, depth); + } + } + catch (const std::exception&) { + add_error_context(_("While calling function '%1 %2':" << name + << call_args.args)); + throw; + } +} + +value_t expr_t::op_t::calc_cons(scope_t& scope, ptr_op_t * locus, + const int depth) +{ + value_t result = left()->calc(scope, locus, depth + 1); + if (has_right()) { + value_t temp; + temp.push_back(result); + + ptr_op_t next = right(); + while (next) { + ptr_op_t value_op; + if (next->kind == O_CONS) { + value_op = next->left(); + next = next->has_right() ? next->right() : NULL; + } else { + value_op = next; + next = NULL; + } + temp.push_back(value_op->calc(scope, locus, depth + 1)); + } + result = temp; + } + return result; +} + +value_t expr_t::op_t::calc_seq(scope_t& scope, ptr_op_t * locus, + const int depth) +{ + // An O_SEQ is very similar to an O_CONS except that only the last + // result value in the series is kept. O_CONS builds up a list. + // + // Another feature of O_SEQ is that it pushes a new symbol scope onto + // the stack. We evaluate the left side here to catch any + // side-effects, such as definitions in the case of 'x = 1; x'. + value_t result = left()->calc(scope, locus, depth + 1); + if (has_right()) { + ptr_op_t next = right(); + while (next) { + ptr_op_t value_op; + if (next->kind == O_SEQ) { + value_op = next->left(); + next = next->right(); + } else { + value_op = next; + next = NULL; + } + result = value_op->calc(scope, locus, depth + 1); + } + } + return result; +} + +namespace { bool print_cons(std::ostream& out, const expr_t::const_ptr_op_t op, const expr_t::op_t::context_t& context) { @@ -476,9 +644,8 @@ namespace { if (op->has_right()) { out << "; "; - - if (op->right()->kind == expr_t::op_t::O_CONS) - found = print_cons(out, op->right(), context); + if (op->right()->kind == expr_t::op_t::O_SEQ) + found = print_seq(out, op->right(), context); else if (op->right()->print(out, context)) found = true; } @@ -515,6 +682,11 @@ bool expr_t::op_t::print(std::ostream& out, const context_t& context) const out << "<FUNCTION>"; break; + case SCOPE: + if (left() && left()->print(out, context)) + found = true; + break; + case O_NOT: out << "! "; if (left() && left()->print(out, context)) @@ -625,7 +797,6 @@ bool expr_t::op_t::print(std::ostream& out, const context_t& context) const case O_CONS: found = print_cons(out, this, context); break; - case O_SEQ: found = print_seq(out, this, context); break; @@ -713,6 +884,10 @@ void expr_t::op_t::dump(std::ostream& out, const int depth) const out << " "; switch (kind) { + case PLUG: + out << "PLUG"; + break; + case VALUE: out << "VALUE: "; as_value().dump(out); @@ -726,6 +901,14 @@ void expr_t::op_t::dump(std::ostream& out, const int depth) const out << "FUNCTION"; break; + case SCOPE: + out << "SCOPE: "; + if (is_scope_unset()) + out << "null"; + else + out << as_scope().get(); + break; + case O_DEFINE: out << "O_DEFINE"; break; case O_LOOKUP: out << "O_LOOKUP"; break; case O_LAMBDA: out << "O_LAMBDA"; break; @@ -765,7 +948,7 @@ void expr_t::op_t::dump(std::ostream& out, const int depth) const // An identifier is a special non-terminal, in that its left() can // hold the compiled definition of the identifier. - if (kind > TERMINALS || is_ident()) { + if (kind > TERMINALS || is_scope() || is_ident()) { if (left()) { left()->dump(out, depth + 1); if (kind > UNARY_OPERATORS && has_right()) @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -58,21 +58,25 @@ private: mutable short refc; ptr_op_t left_; - variant<ptr_op_t, // used by all binary operators + variant<boost::blank, + ptr_op_t, // used by all binary operators value_t, // used by constant VALUE string, // used by constant IDENT - expr_t::func_t // used by terminal FUNCTION + expr_t::func_t, // used by terminal FUNCTION + shared_ptr<scope_t> // used by terminal SCOPE > data; public: enum kind_t { // Constants + PLUG, VALUE, IDENT, CONSTANTS, FUNCTION, + SCOPE, TERMINALS, @@ -173,7 +177,7 @@ public: return kind == FUNCTION; } expr_t::func_t& as_function_lval() { - assert(kind == FUNCTION); + assert(is_function()); return boost::get<expr_t::func_t>(data); } const expr_t::func_t& as_function() const { @@ -183,21 +187,41 @@ public: data = val; } + bool is_scope() const { + return kind == SCOPE; + } + bool is_scope_unset() const { + return data.which() == 0; + } + shared_ptr<scope_t> as_scope_lval() { + assert(is_scope()); + return boost::get<shared_ptr<scope_t> >(data); + } + const shared_ptr<scope_t> as_scope() const { + return const_cast<op_t *>(this)->as_scope_lval(); + } + void set_scope(shared_ptr<scope_t> val) { + data = val; + } + + // These three functions must use 'kind == IDENT' rather than + // 'is_ident()', because they are called before the `data' member gets + // set, which is_ident() tests. ptr_op_t& left() { - assert(kind > TERMINALS || kind == IDENT); + assert(kind > TERMINALS || kind == IDENT || is_scope()); return left_; } const ptr_op_t& left() const { - assert(kind > TERMINALS || kind == IDENT); + assert(kind > TERMINALS || kind == IDENT || is_scope()); return left_; } void set_left(const ptr_op_t& expr) { - assert(kind > TERMINALS || kind == IDENT); + assert(kind > TERMINALS || kind == IDENT || is_scope()); left_ = expr; } ptr_op_t& as_op_lval() { - assert(kind > TERMINALS || kind == IDENT); + assert(kind > TERMINALS || is_ident()); return boost::get<ptr_op_t>(data); } const ptr_op_t& as_op() const { @@ -219,7 +243,7 @@ public: bool has_right() const { if (kind < TERMINALS) return false; - return as_op(); + return data.which() != 0 && as_op(); } private: @@ -237,12 +261,8 @@ private: checked_delete(this); } - friend inline void intrusive_ptr_add_ref(const op_t * op) { - op->acquire(); - } - friend inline void intrusive_ptr_release(const op_t * op) { - op->release(); - } + friend void intrusive_ptr_add_ref(const op_t * op); + friend void intrusive_ptr_release(const op_t * op); ptr_op_t copy(ptr_op_t _left = NULL, ptr_op_t _right = NULL) const { ptr_op_t node(new_node(kind, _left, _right)); @@ -255,10 +275,14 @@ public: static ptr_op_t new_node(kind_t _kind, ptr_op_t _left = NULL, ptr_op_t _right = NULL); - ptr_op_t compile(scope_t& scope, const int depth = 0); + ptr_op_t compile(scope_t& scope, const int depth = 0, + scope_t * param_scope = NULL); value_t calc(scope_t& scope, ptr_op_t * locus = NULL, const int depth = 0); + value_t call(const value_t& args, scope_t& scope, + ptr_op_t * locus = NULL, const int depth = 0); + struct context_t { ptr_op_t expr_op; @@ -284,6 +308,12 @@ public: static ptr_op_t wrap_value(const value_t& val); static ptr_op_t wrap_functor(expr_t::func_t fobj); + static ptr_op_t wrap_scope(shared_ptr<scope_t> sobj); + +private: + value_t calc_call(scope_t& scope, ptr_op_t * locus, const int depth); + value_t calc_cons(scope_t& scope, ptr_op_t * locus, const int depth); + value_t calc_seq(scope_t& scope, ptr_op_t * locus, const int depth); #if defined(HAVE_BOOST_SERIALIZATION) private: @@ -295,13 +325,13 @@ private: void serialize(Archive& ar, const unsigned int /* version */) { ar & refc; ar & kind; - if (Archive::is_loading::value || ! left_ || left_->kind != FUNCTION) { + if (Archive::is_loading::value || ! left_ || ! left_->is_function()) { ar & left_; } else { ptr_op_t temp_op; ar & temp_op; } - if (Archive::is_loading::value || kind == VALUE || kind == IDENT || + if (Archive::is_loading::value || is_value() || is_ident() || (kind > UNARY_OPERATORS && (! has_right() || ! right()->is_function()))) { ar & data; @@ -341,6 +371,8 @@ expr_t::op_t::wrap_functor(expr_t::func_t fobj) { string op_context(const expr_t::ptr_op_t op, const expr_t::ptr_op_t locus = NULL); +value_t split_cons_expr(expr_t::ptr_op_t op); + } // namespace ledger #endif // _OP_H diff --git a/src/option.cc b/src/option.cc index 2843c775..418980bd 100644 --- a/src/option.cc +++ b/src/option.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -89,7 +89,6 @@ namespace { catch (const std::exception&) { if (name[0] == '-') add_error_context(_("While parsing option '%1'") << name); - else add_error_context(_("While parsing environent variable '%1'") << name); throw; diff --git a/src/option.h b/src/option.h index 8f89d081..772f2b01 100644 --- a/src/option.h +++ b/src/option.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -61,16 +61,16 @@ protected: option_t& operator=(const option_t&); public: - T * parent; - value_t value; - bool wants_arg; + T * parent; + string value; + bool wants_arg; option_t(const char * _name, const char _ch = '\0') : name(_name), name_len(std::strlen(name)), ch(_ch), handled(false), parent(NULL), value(), wants_arg(name[name_len - 1] == '_') { - TRACE_CTOR(option_t, "const char *, const char"); DEBUG("option.names", "Option: " << name); + TRACE_CTOR(option_t, "const char *, const char"); } option_t(const option_t& other) : name(other.name), @@ -94,7 +94,8 @@ public: out << std::right << desc(); if (wants_arg) { out << " = "; - value.print(out, 42); + out.width(42); + out << value; } else { out.width(45); out << ' '; @@ -123,43 +124,48 @@ public: return handled; } - string& str() { + string str() const { assert(handled); - if (! value) + if (value.empty()) throw_(std::runtime_error, _("No argument provided for %1") << desc()); - return value.as_string_lval(); + return value; } - string str() const { - assert(handled); - if (! value) - throw_(std::runtime_error, _("No argument provided for %1") << desc()); - return value.as_string(); + void on(const char * whence) { + on(string(whence)); } + void on(const optional<string>& whence) { + handler_thunk(whence); - void on_only(const optional<string>& whence) { handled = true; source = whence; } - void on(const optional<string>& whence, const string& str) { - on_with(whence, string_value(str)); + + void on(const char * whence, const string& str) { + on(string(whence), str); } - virtual void on_with(const optional<string>& whence, - const value_t& val) { + void on(const optional<string>& whence, const string& str) { + string before = value; + + handler_thunk(whence, str); + + if (value == before) + value = str; + handled = true; - value = val; source = whence; } void off() { handled = false; - value = value_t(); + value = ""; source = none; } - virtual void handler_thunk(call_scope_t&) {} + virtual void handler_thunk(const optional<string>&) {} + virtual void handler_thunk(const optional<string>&, const string&) {} - virtual void handler(call_scope_t& args) { + value_t handler(call_scope_t& args) { if (wants_arg) { if (args.size() < 2) throw_(std::runtime_error, _("No argument provided for %1") << desc()); @@ -167,7 +173,7 @@ public: throw_(std::runtime_error, _("To many arguments provided for %1") << desc()); else if (! args[0].is_string()) throw_(std::runtime_error, _("Context argument for %1 not a string") << desc()); - on_with(args.get<string>(0), args[1]); + on(args.get<string>(0), args.get<string>(1)); } else if (args.size() < 1) { throw_(std::runtime_error, _("No argument provided for %1") << desc()); @@ -176,27 +182,18 @@ public: throw_(std::runtime_error, _("Context argument for %1 not a string") << desc()); } else { - on_only(args.get<string>(0)); + on(args.get<string>(0)); } - - handler_thunk(args); - } - - virtual value_t handler_wrapper(call_scope_t& args) { - handler(args); return true; } virtual value_t operator()(call_scope_t& args) { if (! args.empty()) { args.push_front(string_value("?expr")); - return handler_wrapper(args); + return handler(args); } else if (wants_arg) { - if (handled) - return value; - else - return NULL_VALUE; + return string_value(value); } else { return handled; @@ -213,17 +210,18 @@ public: name ## option_t() : option_t<type>(#name), base #define DECL1(type, name, vartype, var, value) \ vartype var ; \ - name ## option_t() : option_t<type>(#name), var(value) + name ## option_t() : option_t<type>(#name), var value -#define DO() virtual void handler_thunk(call_scope_t&) -#define DO_(var) virtual void handler_thunk(call_scope_t& var) +#define DO() virtual void handler_thunk(const optional<string>& whence) +#define DO_(var) virtual void handler_thunk(const optional<string>& whence, \ + const string& var) #define END(name) name ## handler #define COPY_OPT(name, other) name ## handler(other.name ## handler) #define MAKE_OPT_HANDLER(type, x) \ - expr_t::op_t::wrap_functor(bind(&option_t<type>::handler_wrapper, x, _1)) + expr_t::op_t::wrap_functor(bind(&option_t<type>::handler, x, _1)) #define MAKE_OPT_FUNCTOR(type, x) \ expr_t::op_t::wrap_functor(bind(&option_t<type>::operator(), x, _1)) @@ -284,6 +282,10 @@ inline bool is_eq(const char * p, const char * n) { } \ END(name) +#define OTHER(name) \ + parent->HANDLER(name).parent = parent; \ + parent->HANDLER(name) + bool process_option(const string& whence, const string& name, scope_t& scope, const char * arg, const string& varname); @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -45,8 +45,6 @@ posts_to_org_table::posts_to_org_table(report_t& _report, const optional<string>& _prepend_format) : report(_report), last_xact(NULL), last_post(NULL) { - TRACE_CTOR(posts_to_org_table, "report&, optional<string>"); - first_line_format.parse_format ("|%(format_date(date))" "|%(code)" @@ -79,6 +77,8 @@ posts_to_org_table::posts_to_org_table(report_t& _report, if (_prepend_format) prepend_format.parse_format(*_prepend_format); + + TRACE_CTOR(posts_to_org_table, "report&, optional<string>"); } void posts_to_org_table::flush() @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/output.cc b/src/output.cc index b26881a3..742000bd 100644 --- a/src/output.cc +++ b/src/output.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -47,8 +47,6 @@ format_posts::format_posts(report_t& _report, : report(_report), prepend_width(_prepend_width), last_xact(NULL), last_post(NULL), first_report_title(true) { - TRACE_CTOR(format_posts, "report&, const string&, bool"); - const char * f = format.c_str(); if (const char * p = std::strstr(f, "%/")) { @@ -70,6 +68,8 @@ format_posts::format_posts(report_t& _report, if (_prepend_format) prepend_format.parse_format(*_prepend_format); + + TRACE_CTOR(format_posts, "report&, const string&, bool"); } void format_posts::flush() @@ -131,8 +131,6 @@ format_accounts::format_accounts(report_t& _report, : report(_report), prepend_width(_prepend_width), disp_pred(), first_report_title(true) { - TRACE_CTOR(format_accounts, "report&, const string&"); - const char * f = format.c_str(); if (const char * p = std::strstr(f, "%/")) { @@ -154,6 +152,8 @@ format_accounts::format_accounts(report_t& _report, if (_prepend_format) prepend_format.parse_format(*_prepend_format); + + TRACE_CTOR(format_accounts, "report&, const string&"); } std::size_t format_accounts::post_account(account_t& account, const bool flat) diff --git a/src/output.h b/src/output.h index ac3925c4..281f69b6 100644 --- a/src/output.h +++ b/src/output.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/parser.cc b/src/parser.cc index a18fa552..360ac93d 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -54,20 +54,6 @@ expr_t::parser_t::parse_value_term(std::istream& in, node = new op_t(op_t::IDENT); node->set_ident(ident); - - // An identifier followed by ( represents a function call - tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); - if (tok.kind == token_t::LPAREN) { - op_t::kind_t kind = op_t::O_CALL; - ptr_op_t call_node(new op_t(kind)); - call_node->set_left(node); - node = call_node; - - push_token(tok); // let the parser see it again - node->set_right(parse_value_expr(in, tflags.plus_flags(PARSE_SINGLE))); - } else { - push_token(tok); - } break; } @@ -85,8 +71,9 @@ expr_t::parser_t::parse_value_term(std::istream& in, return node; } + expr_t::ptr_op_t -expr_t::parser_t::parse_dot_expr(std::istream& in, +expr_t::parser_t::parse_call_expr(std::istream& in, const parse_flags_t& tflags) const { ptr_op_t node(parse_value_term(in, tflags)); @@ -94,11 +81,36 @@ expr_t::parser_t::parse_dot_expr(std::istream& in, if (node && ! tflags.has_flags(PARSE_SINGLE)) { while (true) { token_t& tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + if (tok.kind == token_t::LPAREN) { + ptr_op_t prev(node); + node = new op_t(op_t::O_CALL); + node->set_left(prev); + push_token(tok); // let the parser see the '(' again + node->set_right(parse_value_expr(in, tflags.plus_flags(PARSE_SINGLE))); + } else { + push_token(tok); + break; + } + } + } + + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_dot_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_call_expr(in, tflags)); + + if (node && ! tflags.has_flags(PARSE_SINGLE)) { + while (true) { + token_t& tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); if (tok.kind == token_t::DOT) { ptr_op_t prev(node); node = new op_t(op_t::O_LOOKUP); node->set_left(prev); - node->set_right(parse_value_term(in, tflags)); + node->set_right(parse_call_expr(in, tflags)); if (! node->right()) throw_(parse_error, _("%1 operator not followed by argument") << tok.symbol); @@ -434,7 +446,6 @@ expr_t::parser_t::parse_comma_expr(std::istream& in, ptr_op_t prev(node); node = new op_t(op_t::O_CONS); node->set_left(prev); - next = node; } @@ -471,7 +482,9 @@ expr_t::parser_t::parse_lambda_expr(std::istream& in, ptr_op_t prev(node); node = new op_t(op_t::O_LAMBDA); node->set_left(prev); - node->set_right(parse_querycolon_expr(in, tflags)); + ptr_op_t scope(new op_t(op_t::SCOPE)); + scope->set_left(parse_querycolon_expr(in, tflags)); + node->set_right(scope); } else { push_token(tok); } @@ -493,7 +506,9 @@ expr_t::parser_t::parse_assign_expr(std::istream& in, ptr_op_t prev(node); node = new op_t(op_t::O_DEFINE); node->set_left(prev); - node->set_right(parse_lambda_expr(in, tflags)); + ptr_op_t scope(new op_t(op_t::SCOPE)); + scope->set_left(parse_lambda_expr(in, tflags)); + node->set_right(scope); } else { push_token(tok); } @@ -509,24 +524,20 @@ expr_t::parser_t::parse_value_expr(std::istream& in, ptr_op_t node(parse_assign_expr(in, tflags)); if (node && ! tflags.has_flags(PARSE_SINGLE)) { - ptr_op_t next; + ptr_op_t chain; while (true) { token_t& tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); - if (tok.kind == token_t::SEMI) { - if (! next) { - ptr_op_t prev(node); - node = new op_t(op_t::O_SEQ); - node->set_left(prev); - - next = node; + ptr_op_t seq(new op_t(op_t::O_SEQ)); + if (! chain) { + seq->set_left(node); + node = seq; + } else { + seq->set_left(chain->right()); + chain->set_right(seq); } - - ptr_op_t chain(new op_t(op_t::O_SEQ)); - chain->set_left(parse_assign_expr(in, tflags)); - - next->set_right(chain); - next = chain; + seq->set_right(parse_assign_expr(in, tflags)); + chain = seq; } else { push_token(tok); break; diff --git a/src/parser.h b/src/parser.h index 09e12d95..db16a919 100644 --- a/src/parser.h +++ b/src/parser.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -81,6 +81,8 @@ class expr_t::parser_t : public noncopyable ptr_op_t parse_value_term(std::istream& in, const parse_flags_t& flags) const; + ptr_op_t parse_call_expr(std::istream& in, + const parse_flags_t& flags) const; ptr_op_t parse_dot_expr(std::istream& in, const parse_flags_t& flags) const; ptr_op_t parse_unary_expr(std::istream& in, diff --git a/src/pool.cc b/src/pool.cc index 65edbd6a..61b5bef2 100644 --- a/src/pool.cc +++ b/src/pool.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -35,6 +35,7 @@ #include "commodity.h" #include "annotate.h" #include "pool.h" +#include "history.h" #include "quotes.h" namespace ledger { @@ -46,16 +47,16 @@ commodity_pool_t::commodity_pool_t() quote_leeway(86400), get_quotes(false), get_commodity_quote(commodity_quote_from_script) { - TRACE_CTOR(commodity_pool_t, ""); null_commodity = create(""); null_commodity->add_flags(COMMODITY_BUILTIN | COMMODITY_NOMARKET); + TRACE_CTOR(commodity_pool_t, ""); } commodity_t * commodity_pool_t::create(const string& symbol) { shared_ptr<commodity_t::base_t> base_commodity(new commodity_t::base_t(symbol)); - std::auto_ptr<commodity_t> commodity(new commodity_t(this, base_commodity)); + shared_ptr<commodity_t> commodity(new commodity_t(this, base_commodity)); DEBUG("pool.commodities", "Creating base commodity " << symbol); @@ -66,25 +67,19 @@ commodity_t * commodity_pool_t::create(const string& symbol) *commodity->qualified_symbol += "\""; } - DEBUG("pool.commodities", - "Creating commodity '" << commodity->symbol() << "'"); + DEBUG("pool.commodities", "Creating commodity '" << symbol << "'"); - std::pair<commodities_map::iterator, bool> result - = commodities.insert(commodities_map::value_type(commodity->mapping_key(), - commodity.get())); +#if defined(DEBUG_ON) + std::pair<commodities_map::iterator, bool> result = +#endif + commodities.insert(commodities_map::value_type(symbol, commodity)); +#if defined(DEBUG_ON) assert(result.second); +#endif - return commodity.release(); -} - -commodity_t * commodity_pool_t::find_or_create(const string& symbol) -{ - DEBUG("pool.commodities", "Find-or-create commodity " << symbol); + commodity_price_history.add_commodity(*commodity.get()); - commodity_t * commodity = find(symbol); - if (commodity) - return commodity; - return create(symbol); + return commodity.get(); } commodity_t * commodity_pool_t::find(const string& symbol) @@ -93,91 +88,111 @@ commodity_t * commodity_pool_t::find(const string& symbol) commodities_map::const_iterator i = commodities.find(symbol); if (i != commodities.end()) - return (*i).second; + return (*i).second.get(); return NULL; } -commodity_t * -commodity_pool_t::create(const string& symbol, const annotation_t& details) +commodity_t * commodity_pool_t::find_or_create(const string& symbol) { - commodity_t * new_comm = create(symbol); - if (! new_comm) - return NULL; - - if (details) - return find_or_create(*new_comm, details); - else - return new_comm; + DEBUG("pool.commodities", "Find-or-create commodity " << symbol); + if (commodity_t * commodity = find(symbol)) + return commodity; + return create(symbol); } -string commodity_pool_t::make_qualified_name(const commodity_t& comm, - const annotation_t& details) +commodity_t * commodity_pool_t::alias(const string& name, commodity_t& referent) { - assert(details); + commodities_map::const_iterator i = commodities.find(referent.base_symbol()); + assert(i != commodities.end()); - if (details.price && details.price->sign() < 0) - throw_(amount_error, _("A commodity's price may not be negative")); + std::pair<commodities_map::iterator, bool> result + = commodities.insert(commodities_map::value_type(name, (*i).second)); + assert(result.second); - std::ostringstream name; - comm.print(name); - details.print(name, comm.pool().keep_base); + return (*result.first).second.get(); +} -#if defined(DEBUG_ON) - if (comm.qualified_symbol) - DEBUG("pool.commodities", "make_qualified_name for " - << *comm.qualified_symbol << std::endl << details); -#endif - DEBUG("pool.commodities", "qualified_name is " << name.str()); +commodity_t * +commodity_pool_t::create(const string& symbol, const annotation_t& details) +{ + DEBUG("pool.commodities", "commodity_pool_t::create[ann] " + << "symbol " << symbol << std::endl << details); - return name.str(); + if (details) + return create(*find_or_create(symbol), details); + else + return create(symbol); } commodity_t * commodity_pool_t::find(const string& symbol, const annotation_t& details) { - commodity_t * comm = find(symbol); - if (! comm) + DEBUG("pool.commodities", "commodity_pool_t::find[ann] " + << "symbol " << symbol << std::endl << details); + + annotated_commodities_map::const_iterator i = + annotated_commodities.find + (annotated_commodities_map::key_type(symbol, details)); + if (i != annotated_commodities.end()) { + DEBUG("pool.commodities", "commodity_pool_t::find[ann] found " + << "symbol " << (*i).second->base_symbol() << std::endl + << as_annotated_commodity(*(*i).second.get()).details); + return (*i).second.get(); + } else { return NULL; + } +} - if (details) { - string name = make_qualified_name(*comm, details); +commodity_t * +commodity_pool_t::find_or_create(const string& symbol, + const annotation_t& details) +{ + DEBUG("pool.commodities", "commodity_pool_t::find_or_create[ann] " + << "symbol " << symbol << std::endl << details); - if (commodity_t * ann_comm = find(name)) { + if (details) { + if (commodity_t * ann_comm = find(symbol, details)) { assert(ann_comm->annotated && as_annotated_commodity(*ann_comm).details); return ann_comm; + } else { + return create(symbol, details); } - return NULL; } else { - return comm; + return find_or_create(symbol); } } commodity_t * -commodity_pool_t::find_or_create(const string& symbol, - const annotation_t& details) +commodity_pool_t::find_or_create(commodity_t& comm, const annotation_t& details) { - commodity_t * comm = find_or_create(symbol); - if (! comm) - return NULL; + DEBUG("pool.commodities", "commodity_pool_t::find_or_create[ann:comm] " + << "symbol " << comm.base_symbol() << std::endl << details); - if (details) - return find_or_create(*comm, details); - else - return comm; + if (details) { + if (commodity_t * ann_comm = find(comm.base_symbol(), details)) { + assert(ann_comm->annotated && as_annotated_commodity(*ann_comm).details); + return ann_comm; + } else { + return create(comm, details); + } + } else { + return &comm; + } } -commodity_t * +annotated_commodity_t * commodity_pool_t::create(commodity_t& comm, - const annotation_t& details, - const string& mapping_key) + const annotation_t& details) { + DEBUG("pool.commodities", "commodity_pool_t::create[ann:comm] " + << "symbol " << comm.base_symbol() << std::endl << details); + assert(comm); assert(! comm.has_annotation()); assert(details); - assert(! mapping_key.empty()); - unique_ptr<commodity_t> commodity - (new annotated_commodity_t(&comm, details)); + shared_ptr<annotated_commodity_t> + commodity(new annotated_commodity_t(&comm, details)); comm.add_flags(COMMODITY_SAW_ANNOTATED); if (details.price) { @@ -187,39 +202,21 @@ commodity_pool_t::create(commodity_t& comm, comm.add_flags(COMMODITY_SAW_ANN_PRICE_FLOAT); } - commodity->qualified_symbol = comm.symbol(); - assert(! commodity->qualified_symbol->empty()); - DEBUG("pool.commodities", "Creating annotated commodity " - << "symbol " << commodity->symbol() - << " key " << mapping_key << std::endl << details); + << "symbol " << commodity->base_symbol() + << std::endl << details); - // Add the fully annotated name to the map, so that this symbol may - // quickly be found again. - commodity->mapping_key_ = mapping_key; - - std::pair<commodities_map::iterator, bool> result - = commodities.insert(commodities_map::value_type(mapping_key, - commodity.get())); +#if defined(DEBUG_ON) + std::pair<annotated_commodities_map::iterator, bool> result = +#endif + annotated_commodities.insert(annotated_commodities_map::value_type + (annotated_commodities_map::key_type + (comm.base_symbol(), details), commodity)); +#if defined(DEBUG_ON) assert(result.second); +#endif - return commodity.release(); -} - -commodity_t * commodity_pool_t::find_or_create(commodity_t& comm, - const annotation_t& details) -{ - assert(comm); - assert(details); - - string name = make_qualified_name(comm, details); - assert(! name.empty()); - - if (commodity_t * ann_comm = find(name)) { - assert(ann_comm->annotated && as_annotated_commodity(*ann_comm).details); - return ann_comm; - } - return create(comm, details, name); + return commodity.get(); } void commodity_pool_t::exchange(commodity_t& commodity, @@ -240,6 +237,7 @@ cost_breakdown_t commodity_pool_t::exchange(const amount_t& amount, const amount_t& cost, const bool is_per_unit, + const bool add_price, const optional<datetime_t>& moment, const optional<string>& tag) { @@ -261,6 +259,9 @@ commodity_pool_t::exchange(const amount_t& amount, amount_t per_unit_cost = (is_per_unit || amount.is_realzero()) ? cost.abs() : (cost / amount).abs(); + if (! cost.has_commodity()) + per_unit_cost.clear_commodity(); + DEBUG("commodity.prices.add", "exchange: per-unit-cost = " << per_unit_cost); // Do not record commodity exchanges where amount's commodity has a @@ -269,8 +270,10 @@ commodity_pool_t::exchange(const amount_t& amount, if (! per_unit_cost.is_realzero() && (current_annotation == NULL || ! (current_annotation->price && - current_annotation->has_flags(ANNOTATION_PRICE_FIXATED)))) + current_annotation->has_flags(ANNOTATION_PRICE_FIXATED))) && + commodity.referent() != per_unit_cost.commodity().referent()) { exchange(commodity, per_unit_cost, moment ? *moment : CURRENT_TIME()); + } cost_breakdown_t breakdown; breakdown.final_cost = ! is_per_unit ? cost : cost * amount.abs(); @@ -382,76 +385,4 @@ commodity_pool_t::parse_price_expression(const std::string& str, return NULL; } -void commodity_pool_t::print_pricemap(std::ostream& out, - const keep_details_t& keep, - const optional<datetime_t>& moment) -{ - typedef std::map<commodity_t *, commodity_t *> comm_map_t; - - comm_map_t comm_map; - - foreach (const commodities_map::value_type& comm_pair, commodities) { - commodity_t * comm(&comm_pair.second->strip_annotations(keep)); - comm_map.insert(comm_map_t::value_type(comm, NULL)); - } - - out << "digraph commodities {\n"; - - foreach (const comm_map_t::value_type& comm_pair, comm_map) { - commodity_t * comm(comm_pair.first); - if (comm->has_flags(COMMODITY_BUILTIN)) - continue; - - out << " "; - if (commodity_t::symbol_needs_quotes(comm->symbol())) - out << comm->symbol() << ";\n"; - else - out << "\"" << comm->symbol() << "\";\n"; - - if (! comm->has_flags(COMMODITY_NOMARKET) && - (! commodity_pool_t::current_pool->default_commodity || - comm != commodity_pool_t::current_pool->default_commodity)) { - if (optional<commodity_t::varied_history_t&> vhist = - comm->varied_history()) { - foreach (const commodity_t::history_by_commodity_map::value_type& pair, - vhist->histories) { - datetime_t most_recent; - amount_t most_recent_amt; - foreach (const commodity_t::history_map::value_type& inner_pair, - pair.second.prices) { - if ((most_recent.is_not_a_date_time() || - inner_pair.first > most_recent) && - (! moment || inner_pair.first <= moment)) { - most_recent = inner_pair.first; - most_recent_amt = inner_pair.second; - } - } - - if (! most_recent.is_not_a_date_time()) { - out << " "; - if (commodity_t::symbol_needs_quotes(comm->symbol())) - out << comm->symbol(); - else - out << "\"" << comm->symbol() << "\""; - - out << " -> "; - - if (commodity_t::symbol_needs_quotes(pair.first->symbol())) - out << pair.first->symbol(); - else - out << "\"" << pair.first->symbol() << "\""; - - out << " [label=\"" - << most_recent_amt.number() << "\\n" - << format_date(most_recent.date(), FMT_WRITTEN) - << "\" fontcolor=\"#008e28\"];\n"; - } - } - } - } - } - - out << "}\n"; -} - } // namespace ledger @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -46,6 +46,9 @@ #ifndef _POOL_H #define _POOL_H +#include "history.h" +#include "annotate.h" + namespace ledger { struct cost_breakdown_t @@ -64,50 +67,47 @@ public: * explicitly by calling the create methods of commodity_pool_t, or * implicitly by parsing a commoditized amount. */ - typedef std::map<string, commodity_t *> commodities_map; - - commodities_map commodities; - commodity_t * null_commodity; - commodity_t * default_commodity; - - bool keep_base; // --base + typedef std::map<string, shared_ptr<commodity_t> > commodities_map; + typedef std::map<std::pair<string, annotation_t>, + shared_ptr<annotated_commodity_t> > annotated_commodities_map; - optional<path> price_db; // --price-db= - long quote_leeway; // --leeway= - bool get_quotes; // --download + commodities_map commodities; + annotated_commodities_map annotated_commodities; + commodity_history_t commodity_price_history; + commodity_t * null_commodity; + commodity_t * default_commodity; - static shared_ptr<commodity_pool_t> current_pool; + bool keep_base; // --base + optional<path> price_db; // --price-db= + long quote_leeway; // --leeway= + bool get_quotes; // --download function<optional<price_point_t> - (commodity_t& commodity, const optional<commodity_t&>& in_terms_of)> + (commodity_t& commodity, const commodity_t * in_terms_of)> get_commodity_quote; - explicit commodity_pool_t(); + static shared_ptr<commodity_pool_t> current_pool; + explicit commodity_pool_t(); virtual ~commodity_pool_t() { TRACE_DTOR(commodity_pool_t); - foreach (commodities_map::value_type& pair, commodities) - checked_delete(pair.second); } - string make_qualified_name(const commodity_t& comm, - const annotation_t& details); - commodity_t * create(const string& symbol); commodity_t * find(const string& name); commodity_t * find_or_create(const string& symbol); + commodity_t * alias(const string& name, commodity_t& referent); - commodity_t * create(const string& symbol, const annotation_t& details); - commodity_t * find(const string& symbol, const annotation_t& details); + commodity_t * create(const string& symbol, + const annotation_t& details); + commodity_t * find(const string& symbol, + const annotation_t& details); commodity_t * find_or_create(const string& symbol, const annotation_t& details); + commodity_t * find_or_create(commodity_t& comm, const annotation_t& details); - commodity_t * create(commodity_t& comm, - const annotation_t& details, - const string& mapping_key); - - commodity_t * find_or_create(commodity_t& comm, - const annotation_t& details); + annotated_commodity_t * create(commodity_t& comm, + const annotation_t& details); // Exchange one commodity for another, while recording the factored price. @@ -118,6 +118,7 @@ public: cost_breakdown_t exchange(const amount_t& amount, const amount_t& cost, const bool is_per_unit = false, + const bool add_price = true, const optional<datetime_t>& moment = none, const optional<string>& tag = none); @@ -131,12 +132,6 @@ public: const bool add_prices = true, const optional<datetime_t>& moment = none); - // Output the commodity price map for a given date as a DOT file - - void print_pricemap(std::ostream& out, - const keep_details_t& keep, - const optional<datetime_t>& moment = none); - #if defined(HAVE_BOOST_SERIALIZATION) private: /** Serialization. */ @@ -147,6 +142,7 @@ private: void serialize(Archive& ar, const unsigned int /* version */) { ar & current_pool; ar & commodities; + ar & annotated_commodities; ar & null_commodity; ar & default_commodity; ar & keep_base; diff --git a/src/post.cc b/src/post.cc index b40e31f0..0564eaca 100644 --- a/src/post.cc +++ b/src/post.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -36,6 +36,7 @@ #include "account.h" #include "journal.h" #include "format.h" +#include "pool.h" namespace ledger { @@ -48,9 +49,9 @@ bool post_t::has_tag(const string& tag, bool inherit) const return false; } -bool post_t::has_tag(const mask_t& tag_mask, +bool post_t::has_tag(const mask_t& tag_mask, const optional<mask_t>& value_mask, - bool inherit) const + bool inherit) const { if (item_t::has_tag(tag_mask, value_mask)) return true; @@ -68,9 +69,9 @@ optional<value_t> post_t::get_tag(const string& tag, bool inherit) const return none; } -optional<value_t> post_t::get_tag(const mask_t& tag_mask, +optional<value_t> post_t::get_tag(const mask_t& tag_mask, const optional<mask_t>& value_mask, - bool inherit) const + bool inherit) const { if (optional<value_t> value = item_t::get_tag(tag_mask, value_mask)) return value; @@ -91,21 +92,15 @@ date_t post_t::date() const if (xdata_ && is_valid(xdata_->date)) return xdata_->date; - if (item_t::use_effective_date) { - if (_date_eff) - return *_date_eff; - else if (xact && xact->_date_eff) - return *xact->_date_eff; + if (item_t::use_aux_date) { + if (optional<date_t> aux = aux_date()) + return *aux; } - if (! _date) { - assert(xact); - return xact->date(); - } - return *_date; + return primary_date(); } -date_t post_t::actual_date() const +date_t post_t::primary_date() const { if (xdata_ && is_valid(xdata_->date)) return xdata_->date; @@ -117,11 +112,11 @@ date_t post_t::actual_date() const return *_date; } -optional<date_t> post_t::effective_date() const +optional<date_t> post_t::aux_date() const { - optional<date_t> date = item_t::effective_date(); + optional<date_t> date = item_t::aux_date(); if (! date && xact) - return xact->effective_date(); + return xact->aux_date(); return date; } @@ -173,7 +168,8 @@ namespace { return string_value(post.payee()); } - value_t get_note(post_t& post) { + value_t get_note(post_t& post) + { if (post.note || post.xact->note) { string note = post.note ? *post.note : empty_string; note += post.xact->note ? *post.xact->note : empty_string; @@ -186,14 +182,9 @@ namespace { value_t get_magnitude(post_t& post) { return post.xact->magnitude(); } - value_t get_idstring(post_t& post) { - return string_value(post.xact->idstring()); - } - value_t get_id(post_t& post) { - return string_value(post.xact->id()); - } - value_t get_amount(post_t& post) { + value_t get_amount(post_t& post) + { if (post.has_xdata() && post.xdata().has_flags(POST_EXT_COMPOUND)) return post.xdata().compound_value; else if (post.amount.is_null()) @@ -220,7 +211,8 @@ namespace { } } - value_t get_commodity_is_primary(post_t& post) { + value_t get_commodity_is_primary(post_t& post) + { if (post.has_xdata() && post.xdata().has_flags(POST_EXT_COMPOUND)) return post.xdata().compound_value.to_amount() @@ -245,6 +237,15 @@ namespace { return post.amount; } + value_t get_price(post_t& post) { + if (post.amount.is_null()) + return 0L; + if (post.amount.has_annotation() && post.amount.annotation().price) + return *post.amount.price(); + else + return get_cost(post); + } + value_t get_total(post_t& post) { if (post.xdata_ && ! post.xdata_->total.is_null()) return post.xdata_->total; @@ -347,7 +348,14 @@ namespace { return post.date(); } value_t get_datetime(post_t& post) { - return post.xdata().datetime; + return (! post.xdata().datetime.is_not_a_date_time() ? + post.xdata().datetime : datetime_t(post.date())); + } + value_t get_checkin(post_t& post) { + return post.checkin ? *post.checkin : NULL_VALUE; + } + value_t get_checkout(post_t& post) { + return post.checkout ? *post.checkout : NULL_VALUE; } template <value_t (*Func)(post_t&)> @@ -440,6 +448,10 @@ expr_t::ptr_op_t post_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(get_wrapper<&get_is_calculated>); else if (name == "commodity") return WRAP_FUNCTOR(&get_commodity); + else if (name == "checkin") + return WRAP_FUNCTOR(get_wrapper<&get_checkin>); + else if (name == "checkout") + return WRAP_FUNCTOR(get_wrapper<&get_checkout>); break; case 'd': @@ -459,10 +471,6 @@ expr_t::ptr_op_t post_t::lookup(const symbol_t::kind_t kind, case 'i': if (name == "index") return WRAP_FUNCTOR(get_wrapper<&get_count>); - else if (name == "id") - return WRAP_FUNCTOR(get_wrapper<&get_id>); - else if (name == "idstring") - return WRAP_FUNCTOR(get_wrapper<&get_idstring>); break; case 'm': @@ -484,6 +492,8 @@ expr_t::ptr_op_t post_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(get_wrapper<&get_payee>); else if (name == "primary") return WRAP_FUNCTOR(get_wrapper<&get_commodity_is_primary>); + else if (name == "price") + return WRAP_FUNCTOR(get_wrapper<&get_price>); else if (name == "parent") return WRAP_FUNCTOR(get_wrapper<&get_xact>); break; @@ -616,7 +626,8 @@ bool post_t::valid() const void post_t::add_to_value(value_t& value, const optional<expr_t&>& expr) const { if (xdata_ && xdata_->has_flags(POST_EXT_COMPOUND)) { - add_or_set_value(value, xdata_->compound_value); + if (! xdata_->compound_value.is_null()) + add_or_set_value(value, xdata_->compound_value); } else if (expr) { bind_scope_t bound_scope(*expr->get_context(), @@ -647,6 +658,44 @@ void post_t::set_reported_account(account_t * acct) acct->xdata().reported_posts.push_back(this); } +void extend_post(post_t& post, journal_t& journal) +{ + commodity_t& comm(post.amount.commodity()); + + annotation_t * details = + (comm.has_annotation() ? + &as_annotated_commodity(comm).details : NULL); + + if (! details || ! details->value_expr) { + optional<expr_t> value_expr; + + if (optional<value_t> data = post.get_tag(_("Value"))) + value_expr = expr_t(data->to_string()); + + if (! value_expr) + value_expr = post.account->value_expr; + + if (! value_expr) + value_expr = post.amount.commodity().value_expr(); + + if (! value_expr) + value_expr = journal.value_expr; + + if (value_expr) { + if (! details) { + annotation_t new_details; + new_details.value_expr = value_expr; + + commodity_t * new_comm = + commodity_pool_t::current_pool->find_or_create(comm, new_details); + post.amount.set_commodity(*new_comm); + } else { + details->value_expr = value_expr; + } + } + } +} + void to_xml(std::ostream& out, const post_t& post) { push_xml x(out, "posting", true); @@ -667,9 +716,9 @@ void to_xml(std::ostream& out, const post_t& post) push_xml y(out, "date"); to_xml(out, *post._date, false); } - if (post._date_eff) { - push_xml y(out, "effective-date"); - to_xml(out, *post._date_eff, false); + if (post._date_aux) { + push_xml y(out, "aux-date"); + to_xml(out, *post._date_aux, false); } if (post.account) { @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -58,20 +58,22 @@ public: #define POST_COST_CALCULATED 0x0080 // posting's cost was calculated #define POST_COST_IN_FULL 0x0100 // cost specified using @@ #define POST_COST_FIXATED 0x0200 // cost is fixed using = indicator -#define POST_ANONYMIZED 0x0400 // a temporary, anonymous posting +#define POST_COST_VIRTUAL 0x0400 // cost is virtualized: (@) +#define POST_ANONYMIZED 0x0800 // a temporary, anonymous posting - xact_t * xact; // only set for posts of regular xacts - account_t * account; + xact_t * xact; // only set for posts of regular xacts + account_t * account; - amount_t amount; // can be null until finalization - optional<expr_t> amount_expr; - optional<amount_t> cost; - optional<amount_t> assigned_amount; + amount_t amount; // can be null until finalization + optional<expr_t> amount_expr; + optional<amount_t> cost; + optional<amount_t> assigned_amount; + optional<datetime_t> checkin; + optional<datetime_t> checkout; post_t(account_t * _account = NULL, flags_t _flags = ITEM_NORMAL) - : item_t(_flags), - xact(NULL), account(_account) + : item_t(_flags), xact(NULL), account(_account) { TRACE_CTOR(post_t, "account_t *, flags_t"); } @@ -79,8 +81,7 @@ public: const amount_t& _amount, flags_t _flags = ITEM_NORMAL, const optional<string>& _note = none) - : item_t(_flags, _note), - xact(NULL), account(_account), amount(_amount) + : item_t(_flags, _note), xact(NULL), account(_account), amount(_amount) { TRACE_CTOR(post_t, "account_t *, const amount_t&, flags_t, const optional<string>&"); } @@ -91,8 +92,11 @@ public: amount(post.amount), cost(post.cost), assigned_amount(post.assigned_amount), + checkin(post.checkin), + checkout(post.checkout), xdata_(post.xdata_) { + copy_details(post); TRACE_CTOR(post_t, "copy"); } virtual ~post_t() { @@ -116,15 +120,15 @@ public: bool inherit = true) const; virtual optional<value_t> get_tag(const string& tag, - bool inherit = true) const; + bool inherit = true) const; virtual optional<value_t> get_tag(const mask_t& tag_mask, const optional<mask_t>& value_mask = none, - bool inherit = true) const; + bool inherit = true) const; virtual date_t value_date() const; virtual date_t date() const; - virtual date_t actual_date() const; - virtual optional<date_t> effective_date() const; + virtual date_t primary_date() const; + virtual optional<date_t> aux_date() const; string payee() const; @@ -140,6 +144,12 @@ public: std::size_t xact_id() const; std::size_t account_id() const; + virtual void copy_details(const item_t& item) { + const post_t& post(dynamic_cast<const post_t&>(item)); + xdata_ = post.xdata_; + item_t::copy_details(item); + } + bool valid() const; struct xdata_t : public supports_flags<uint_least16_t> @@ -230,7 +240,7 @@ public: { bool operator()(const post_t * left, const post_t * right) const { gregorian::date_duration duration = - left->actual_date() - right->actual_date(); + left->primary_date() - right->primary_date(); if (duration.days() == 0) { return ((left->pos ? left->pos->sequence : 0) < (right->pos ? right->pos->sequence : 0)); @@ -259,6 +269,9 @@ private: #endif // HAVE_BOOST_SERIALIZATION }; +class journal_t; +void extend_post(post_t& post, journal_t& journal); + void to_xml(std::ostream& out, const post_t& post); } // namespace ledger diff --git a/src/precmd.cc b/src/precmd.cc index 663b638d..fe0836bc 100644 --- a/src/precmd.cc +++ b/src/precmd.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -43,22 +43,6 @@ namespace ledger { namespace { - string join_args(call_scope_t& args) - { - std::ostringstream buf; - bool first = true; - - for (std::size_t i = 0; i < args.size(); i++) { - if (first) - first = false; - else - buf << ' '; - buf << args[i]; - } - - return buf.str(); - } - post_t * get_sample_xact(report_t& report) { { @@ -83,8 +67,14 @@ namespace { out << _("--- Context is first posting of the following transaction ---") << std::endl << str << std::endl; { - std::istringstream in(str); - report.session.journal->parse(in, report.session); + shared_ptr<std::istringstream> in(new std::istringstream(str)); + + parse_context_stack_t parsing_context; + parsing_context.push(in); + parsing_context.get_current().journal = report.session.journal.get(); + parsing_context.get_current().scope = &report.session; + + report.session.journal->read(parsing_context); report.session.journal->clear_xdata(); } } diff --git a/src/precmd.h b/src/precmd.h index 277933c3..1c52d8a7 100644 --- a/src/precmd.h +++ b/src/precmd.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/predicate.h b/src/predicate.h index 673f1d5d..7d58dc2f 100644 --- a/src/predicate.h +++ b/src/predicate.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/print.cc b/src/print.cc index b7f72bf0..79d83161 100644 --- a/src/print.cc +++ b/src/print.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -41,14 +41,47 @@ namespace ledger { namespace { + bool post_has_simple_amount(const post_t& post) + { + // Is the amount the result of a computation, i.e., it wasn't + // explicit specified by the user? + if (post.has_flags(POST_CALCULATED)) + return false; + + // Is the amount still empty? This shouldn't be true by this point, + // but we check anyway for safety. + if (post.amount.is_null()) + return false; + + // Is the amount a complex expression. If so, the first 'if' should + // have triggered. + if (post.amount_expr) + return false; + + // Is there a balance assignment? If so, don't elide the amount as + // that can change the semantics. + if (post.assigned_amount) + return false; + + // Does it have an explicitly specified cost (i.e., one that wasn't + // calculated for the user)? If so, don't elide the amount! + if (post.cost && ! post.has_flags(POST_COST_CALCULATED)) + return false; + + return true; + } + void print_note(std::ostream& out, const string& note, + const bool note_on_next_line, const std::size_t columns, const std::size_t prior_width) { - // The 4 is for four leading spaces at the beginning of the posting, and - // the 3 is for two spaces and a semi-colon before the note. - if (columns > 0 && note.length() > columns - (prior_width + 3)) + // The 3 is for two spaces and a semi-colon before the note. + if (note_on_next_line || + (columns > 0 && + (columns <= prior_width + 3 || + note.length() > columns - (prior_width + 3)))) out << "\n ;"; else out << " ;"; @@ -79,11 +112,11 @@ namespace { std::ostringstream buf; - buf << format_date(item_t::use_effective_date ? - xact.date() : xact.actual_date(), + buf << format_date(item_t::use_aux_date ? + xact.date() : xact.primary_date(), format_type, format); - if (! item_t::use_effective_date && xact.effective_date()) - buf << '=' << format_date(*xact.effective_date(), + if (! item_t::use_aux_date && xact.aux_date()) + buf << '=' << format_date(*xact.aux_date(), format_type, format); buf << ' '; @@ -100,10 +133,11 @@ namespace { std::size_t columns = (report.HANDLED(columns_) ? - static_cast<std::size_t>(report.HANDLER(columns_).value.to_long()) : 80); + lexical_cast<std::size_t>(report.HANDLER(columns_).str()) : 80); if (xact.note) - print_note(out, *xact.note, columns, unistring(leader).length()); + print_note(out, *xact.note, xact.has_flags(ITEM_NOTE_ON_NEXT_LINE), + columns, unistring(leader).length()); out << '\n'; if (xact.metadata) { @@ -119,7 +153,12 @@ namespace { } } + std::size_t count = xact.posts.size(); + std::size_t index = 0; + foreach (post_t * post, xact.posts) { + index++; + if (! report.HANDLED(generated) && (post->has_flags(ITEM_TEMP | ITEM_GENERATED) && ! post->has_flags(POST_ANONYMIZED))) @@ -152,8 +191,8 @@ namespace { unistring name(pbuf.str()); std::size_t account_width = - (report.HANDLER(account_width_).specified ? - static_cast<std::size_t>(report.HANDLER(account_width_).value.to_long()) : 36); + (report.HANDLED(account_width_) ? + lexical_cast<std::size_t>(report.HANDLER(account_width_).str()) : 36); if (account_width < name.length()) account_width = name.length(); @@ -163,25 +202,32 @@ namespace { std::string::size_type slip = (static_cast<std::string::size_type>(account_width) - static_cast<std::string::size_type>(name.length())); - if (slip > 0) { - out.width(static_cast<std::streamsize>(slip)); - out << ' '; - } - - std::ostringstream amtbuf; string amt; if (post->amount_expr) { amt = post->amount_expr->text(); - } else { - int amount_width = - (report.HANDLER(amount_width_).specified ? - report.HANDLER(amount_width_).value.to_int() : 12); + } + else if (count == 2 && index == 2 && + post_has_simple_amount(*post) && + post_has_simple_amount(*(*xact.posts.begin())) && + ((*xact.posts.begin())->amount.commodity() == + post->amount.commodity())) { + // If there are two postings and they both simple amount, and + // they are both of the same commodity, don't bother printing + // the second amount as it's always just an inverse of the + // first. + } + else { + std::size_t amount_width = + (report.HANDLED(amount_width_) ? + lexical_cast<std::size_t>(report.HANDLER(amount_width_).str()) : + 12); std::ostringstream amt_str; - value_t(post->amount).print(amt_str, amount_width, -1, - AMOUNT_PRINT_RIGHT_JUSTIFY | - AMOUNT_PRINT_NO_COMPUTED_ANNOTATIONS); + value_t(post->amount).print(amt_str, static_cast<int>(amount_width), + -1, AMOUNT_PRINT_RIGHT_JUSTIFY | + (report.HANDLED(generated) ? 0 : + AMOUNT_PRINT_NO_COMPUTED_ANNOTATIONS)); amt = amt_str.str(); } @@ -191,6 +237,7 @@ namespace { (static_cast<std::string::size_type>(amt.length()) - static_cast<std::string::size_type>(trimmed_amt.length())); + std::ostringstream amtbuf; if (slip + amt_slip < 2) amtbuf << string(2 - (slip + amt_slip), ' '); amtbuf << amt; @@ -208,15 +255,22 @@ namespace { amtbuf << " = " << *post->assigned_amount; string trailer = amtbuf.str(); - out << trailer; - - account_width += unistring(trailer).length(); + if (! trailer.empty()) { + if (slip > 0) { + out.width(static_cast<std::streamsize>(slip)); + out << ' '; + } + out << trailer; + + account_width += unistring(trailer).length(); + } } else { out << pbuf.str(); } if (post->note) - print_note(out, *post->note, columns, 4 + account_width); + print_note(out, *post->note, post->has_flags(ITEM_NOTE_ON_NEXT_LINE), + columns, 4 + account_width); out << '\n'; } } diff --git a/src/print.h b/src/print.h index 527f1912..42bfc8b6 100644 --- a/src/print.h +++ b/src/print.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/pstream.h b/src/pstream.h index 8134495d..6e38158a 100644 --- a/src/pstream.h +++ b/src/pstream.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, John Wiegley. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -61,9 +61,14 @@ class ptristream : public std::istream if (*ptr && len == 0) len = std::strlen(ptr); - setg(ptr, // beginning of putback area - ptr, // read position + setg(ptr, // beginning of putback area + ptr, // read position ptr+len); // end position + + TRACE_CTOR(ptrinbuf, "char *, std::size_t"); + } + ~ptrinbuf() throw() { + TRACE_DTOR(ptrinbuf); } protected: diff --git a/src/py_account.cc b/src/py_account.cc index 5ef86871..64a7ae54 100644 --- a/src/py_account.cc +++ b/src/py_account.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/py_amount.cc b/src/py_amount.cc index 9ce4a02d..0aa8fee8 100644 --- a/src/py_amount.cc +++ b/src/py_amount.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -48,14 +48,19 @@ namespace { return amount.value(CURRENT_TIME()); } boost::optional<amount_t> py_value_1(const amount_t& amount, - commodity_t& in_terms_of) { + const commodity_t * in_terms_of) { return amount.value(CURRENT_TIME(), in_terms_of); } boost::optional<amount_t> py_value_2(const amount_t& amount, - commodity_t& in_terms_of, - datetime_t& moment) { + const commodity_t * in_terms_of, + const datetime_t& moment) { return amount.value(moment, in_terms_of); } + boost::optional<amount_t> py_value_2d(const amount_t& amount, + const commodity_t * in_terms_of, + const date_t& moment) { + return amount.value(datetime_t(moment), in_terms_of); + } void py_parse_2(amount_t& amount, object in, unsigned char flags) { if (PyFile_Check(in.ptr())) { @@ -238,6 +243,7 @@ internal precision.")) .def("value", py_value_0) .def("value", py_value_1, args("in_terms_of")) .def("value", py_value_2, args("in_terms_of", "moment")) + .def("value", py_value_2d, args("in_terms_of", "moment")) .def("price", &amount_t::price) @@ -263,10 +269,11 @@ internal precision.")) .add_property("commodity", make_function(&amount_t::commodity, - return_value_policy<reference_existing_object>()), + return_internal_reference<>()), make_function(&amount_t::set_commodity, with_custodian_and_ward<1, 2>())) .def("has_commodity", &amount_t::has_commodity) + .def("with_commodity", &amount_t::with_commodity) .def("clear_commodity", &amount_t::clear_commodity) .def("number", &amount_t::number) diff --git a/src/py_balance.cc b/src/py_balance.cc index 0140a625..2ae546f1 100644 --- a/src/py_balance.cc +++ b/src/py_balance.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -48,12 +48,12 @@ namespace { return balance.value(CURRENT_TIME()); } boost::optional<balance_t> py_value_1(const balance_t& balance, - commodity_t& in_terms_of) { + const commodity_t * in_terms_of) { return balance.value(CURRENT_TIME(), in_terms_of); } boost::optional<balance_t> py_value_2(const balance_t& balance, - commodity_t& in_terms_of, - datetime_t& moment) { + const commodity_t * in_terms_of, + const datetime_t& moment) { return balance.value(moment, in_terms_of); } @@ -201,8 +201,6 @@ void export_balance() .def("value", py_value_1, args("in_terms_of")) .def("value", py_value_2, args("in_terms_of", "moment")) - .def("price", &balance_t::price) - .def("__nonzero__", &balance_t::is_nonzero) .def("is_nonzero", &balance_t::is_nonzero) .def("is_zero", &balance_t::is_zero) diff --git a/src/py_commodity.cc b/src/py_commodity.cc index 6d8a29b3..b283efcc 100644 --- a/src/py_commodity.cc +++ b/src/py_commodity.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -96,14 +96,15 @@ namespace { pool.exchange(commodity, per_unit_cost, moment); } - cost_breakdown_t py_exchange_5(commodity_pool_t& pool, + cost_breakdown_t py_exchange_7(commodity_pool_t& pool, const amount_t& amount, const amount_t& cost, const bool is_per_unit, + const bool add_prices, const boost::optional<datetime_t>& moment, const boost::optional<string>& tag) { - return pool.exchange(amount, cost, is_per_unit, moment, tag); + return pool.exchange(amount, cost, is_per_unit, add_prices, moment, tag); } commodity_t * py_pool_getitem(commodity_pool_t& pool, const string& symbol) @@ -113,9 +114,9 @@ namespace { if (i == pool.commodities.end()) { PyErr_SetString(PyExc_ValueError, (string("Could not find commodity ") + symbol).c_str()); - throw boost::python::error_already_set(); + throw_error_already_set(); } - return (*i).second; + return (*i).second.get(); } python::list py_pool_keys(commodity_pool_t& pool) { @@ -168,13 +169,15 @@ namespace { py_pool_commodities_values_begin(commodity_pool_t& pool) { return make_transform_iterator (pool.commodities.begin(), - bind(&commodity_pool_t::commodities_map::value_type::second, _1)); + bind(&shared_ptr<commodity_t>::get, + bind(&commodity_pool_t::commodities_map::value_type::second, _1))); } commodities_map_seconds_iterator py_pool_commodities_values_end(commodity_pool_t& pool) { return make_transform_iterator (pool.commodities.end(), - bind(&commodity_pool_t::commodities_map::value_type::second, _1)); + bind(&shared_ptr<commodity_t>::get, + bind(&commodity_pool_t::commodities_map::value_type::second, _1))); } void py_add_price_2(commodity_t& commodity, @@ -255,8 +258,10 @@ void export_commodity() make_getter(&commodity_pool_t::keep_base), make_setter(&commodity_pool_t::keep_base)) .add_property("price_db", - make_getter(&commodity_pool_t::price_db), - make_setter(&commodity_pool_t::price_db)) + make_getter(&commodity_pool_t::price_db, + return_value_policy<return_by_value>()), + make_setter(&commodity_pool_t::price_db, + return_value_policy<return_by_value>())) .add_property("quote_leeway", make_getter(&commodity_pool_t::quote_leeway), make_setter(&commodity_pool_t::quote_leeway)) @@ -267,44 +272,38 @@ void export_commodity() make_getter(&commodity_pool_t::get_commodity_quote), make_setter(&commodity_pool_t::get_commodity_quote)) - .def("make_qualified_name", &commodity_pool_t::make_qualified_name) + .def("create", py_create_1, return_internal_reference<>()) + .def("create", py_create_2, return_internal_reference<>()) - .def("create", py_create_1, - return_value_policy<reference_existing_object>()) - .def("create", py_create_2, - return_value_policy<reference_existing_object>()) + .def("find_or_create", py_find_or_create_1, return_internal_reference<>()) + .def("find_or_create", py_find_or_create_2, return_internal_reference<>()) - .def("find_or_create", py_find_or_create_1, - return_value_policy<reference_existing_object>()) - .def("find_or_create", py_find_or_create_2, - return_value_policy<reference_existing_object>()) - - .def("find", py_find_1, return_value_policy<reference_existing_object>()) - .def("find", py_find_2, return_value_policy<reference_existing_object>()) + .def("find", py_find_1, return_internal_reference<>()) + .def("find", py_find_2, return_internal_reference<>()) .def("exchange", py_exchange_2, with_custodian_and_ward<1, 2>()) .def("exchange", py_exchange_3, with_custodian_and_ward<1, 2>()) - .def("exchange", py_exchange_5) + .def("exchange", py_exchange_7) .def("parse_price_directive", &commodity_pool_t::parse_price_directive) .def("parse_price_expression", &commodity_pool_t::parse_price_expression, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .def("__getitem__", py_pool_getitem, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .def("keys", py_pool_keys) .def("has_key", py_pool_contains) .def("__contains__", py_pool_contains) .def("__iter__", - python::range<return_value_policy<reference_existing_object> > + python::range<return_internal_reference<> > (py_pool_commodities_begin, py_pool_commodities_end)) .def("iteritems", - python::range<return_value_policy<reference_existing_object> > + python::range<return_internal_reference<> > (py_pool_commodities_begin, py_pool_commodities_end)) .def("iterkeys", python::range<>(py_pool_commodities_keys_begin, py_pool_commodities_keys_end)) .def("itervalues", - python::range<return_value_policy<reference_existing_object> > + python::range<return_internal_reference<> > (py_pool_commodities_values_begin, py_pool_commodities_values_end)) ; @@ -349,21 +348,20 @@ void export_commodity() .add_property("referent", make_function(py_commodity_referent, - return_value_policy<reference_existing_object>())) + return_internal_reference<>())) .def("has_annotation", &commodity_t::has_annotation) .def("strip_annotations", py_strip_annotations_0, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .def("strip_annotations", py_strip_annotations_1, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .def("write_annotations", &commodity_t::write_annotations) .def("pool", &commodity_t::pool, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .add_property("base_symbol", &commodity_t::base_symbol) .add_property("symbol", &commodity_t::symbol) - .add_property("mapping_key", &commodity_t::mapping_key) .add_property("name", &commodity_t::name, &commodity_t::set_name) .add_property("note", &commodity_t::note, &commodity_t::set_note) @@ -394,11 +392,15 @@ void export_commodity() .add_property("price", py_price, py_set_price) .add_property("date", - make_getter(&annotation_t::date), - make_setter(&annotation_t::date)) + make_getter(&annotation_t::date, + return_value_policy<return_by_value>()), + make_setter(&annotation_t::date, + return_value_policy<return_by_value>())) .add_property("tag", - make_getter(&annotation_t::tag), - make_setter(&annotation_t::tag)) + make_getter(&annotation_t::tag, + return_value_policy<return_by_value>()), + make_setter(&annotation_t::tag, + return_value_policy<return_by_value>())) .def("__nonzero__", &annotation_t::operator bool) @@ -441,12 +443,12 @@ void export_commodity() .add_property("referent", make_function(py_annotated_commodity_referent, - return_value_policy<reference_existing_object>())) + return_internal_reference<>())) .def("strip_annotations", py_strip_ann_annotations_0, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .def("strip_annotations", py_strip_ann_annotations_1, - return_value_policy<reference_existing_object>()) + return_internal_reference<>()) .def("write_annotations", &annotated_commodity_t::write_annotations) ; } diff --git a/src/py_expr.cc b/src/py_expr.cc index 027125e2..dd9df1f5 100644 --- a/src/py_expr.cc +++ b/src/py_expr.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/py_format.cc b/src/py_format.cc index fc2103c7..482eaf5b 100644 --- a/src/py_format.cc +++ b/src/py_format.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/py_item.cc b/src/py_item.cc index 51d9e50c..893ddcfa 100644 --- a/src/py_item.cc +++ b/src/py_item.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -120,14 +120,20 @@ void export_item() #endif .add_property("note", - make_getter(&item_t::note), - make_setter(&item_t::note)) + make_getter(&item_t::note, + return_value_policy<return_by_value>()), + make_setter(&item_t::note, + return_value_policy<return_by_value>())) .add_property("pos", - make_getter(&item_t::pos), - make_setter(&item_t::pos)) + make_getter(&item_t::pos, + return_value_policy<return_by_value>()), + make_setter(&item_t::pos, + return_value_policy<return_by_value>())) .add_property("metadata", - make_getter(&item_t::metadata), - make_setter(&item_t::metadata)) + make_getter(&item_t::metadata, + return_value_policy<return_by_value>()), + make_setter(&item_t::metadata, + return_value_policy<return_by_value>())) .def("copy_details", &item_t::copy_details) @@ -149,13 +155,13 @@ void export_item() .def("parse_tags", &item_t::parse_tags) .def("append_note", &item_t::append_note) - .add_static_property("use_effective_date", - make_getter(&item_t::use_effective_date), - make_setter(&item_t::use_effective_date)) + .add_static_property("use_aux_date", + make_getter(&item_t::use_aux_date), + make_setter(&item_t::use_aux_date)) .add_property("date", &item_t::date, make_setter(&item_t::_date)) - .add_property("effective_date", &item_t::effective_date, - make_setter(&item_t::_date_eff)) + .add_property("aux_date", &item_t::aux_date, + make_setter(&item_t::_date_aux)) .add_property("state", &item_t::state, &item_t::set_state) diff --git a/src/py_journal.cc b/src/py_journal.cc index bd781225..50a52be9 100644 --- a/src/py_journal.cc +++ b/src/py_journal.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -135,51 +135,57 @@ namespace { return journal.find_account(name, auto_create); } +#if 0 std::size_t py_read(journal_t& journal, const string& pathname) { - return journal.read(pathname); + return journal.read(context_stack); } +#endif struct collector_wrapper { - journal_t& journal; - report_t report; - collect_posts * posts_collector; - post_handler_ptr chain; + journal_t& journal; + report_t report; + + post_handler_ptr posts_collector; collector_wrapper(journal_t& _journal, report_t& base) : journal(_journal), report(base), - posts_collector(new collect_posts) {} + posts_collector(new collect_posts) { + TRACE_CTOR(collector_wrapper, "journal_t&, report_t&"); + } ~collector_wrapper() { + TRACE_DTOR(collector_wrapper); journal.clear_xdata(); } std::size_t length() const { - return posts_collector->length(); + return dynamic_cast<collect_posts *>(posts_collector.get())->length(); } std::vector<post_t *>::iterator begin() { - return posts_collector->begin(); + return dynamic_cast<collect_posts *>(posts_collector.get())->begin(); } std::vector<post_t *>::iterator end() { - return posts_collector->end(); + return dynamic_cast<collect_posts *>(posts_collector.get())->end(); } }; - shared_ptr<collector_wrapper> - py_collect(journal_t& journal, const string& query) + shared_ptr<collector_wrapper> py_query(journal_t& journal, + const string& query) { if (journal.has_xdata()) { PyErr_SetString(PyExc_RuntimeError, - _("Cannot have multiple journal collections open at once")); + _("Cannot have more than one active journal query")); throw_error_already_set(); } report_t& current_report(downcast<report_t>(*scope_t::default_scope)); - shared_ptr<collector_wrapper> coll(new collector_wrapper(journal, - current_report)); - unique_ptr<journal_t> save_journal(current_report.session.journal.release()); - current_report.session.journal.reset(&journal); + shared_ptr<collector_wrapper> + coll(new collector_wrapper(journal, current_report)); + + unique_ptr<journal_t> save_journal(coll->report.session.journal.release()); + coll->report.session.journal.reset(&coll->journal); try { strings_list remaining = @@ -189,60 +195,48 @@ namespace { value_t args; foreach (const string& arg, remaining) args.push_back(string_value(arg)); - coll->report.parse_query_args(args, "@Journal.collect"); + coll->report.parse_query_args(args, "@Journal.query"); - journal_posts_iterator walker(coll->journal); - coll->chain = - chain_post_handlers(post_handler_ptr(coll->posts_collector), - coll->report); - pass_down_posts<journal_posts_iterator>(coll->chain, walker); + coll->report.posts_report(coll->posts_collector); } catch (...) { - current_report.session.journal.release(); - current_report.session.journal.reset(save_journal.release()); + coll->report.session.journal.release(); + coll->report.session.journal.reset(save_journal.release()); throw; } - current_report.session.journal.release(); - current_report.session.journal.reset(save_journal.release()); + coll->report.session.journal.release(); + coll->report.session.journal.reset(save_journal.release()); return coll; } post_t * posts_getitem(collector_wrapper& collector, long i) { - post_t * post = - collector.posts_collector->posts[static_cast<std::string::size_type>(i)]; - std::cerr << typeid(post).name() << std::endl; - std::cerr << typeid(*post).name() << std::endl; - std::cerr << typeid(post->account).name() << std::endl; - std::cerr << typeid(*post->account).name() << std::endl; - return post; + return dynamic_cast<collect_posts *>(collector.posts_collector.get()) + ->posts[static_cast<std::size_t>(i)]; } } // unnamed namespace +#define EXC_TRANSLATOR(type) \ + void exc_translate_ ## type(const type& err) { \ + PyErr_SetString(PyExc_RuntimeError, err.what()); \ + } + +EXC_TRANSLATOR(parse_error) +EXC_TRANSLATOR(error_count) + void export_journal() { class_< item_handler<post_t>, shared_ptr<item_handler<post_t> >, boost::noncopyable >("PostHandler") ; - class_< collect_posts, bases<item_handler<post_t> >, - shared_ptr<collect_posts>, boost::noncopyable >("PostCollector") - .def("__len__", &collect_posts::length) - .def("__iter__", python::range<return_internal_reference<1, - with_custodian_and_ward_postcall<1, 0> > > - (&collect_posts::begin, &collect_posts::end)) - ; - class_< collector_wrapper, shared_ptr<collector_wrapper>, boost::noncopyable >("PostCollectorWrapper", no_init) .def("__len__", &collector_wrapper::length) - .def("__getitem__", posts_getitem, return_internal_reference<1, - with_custodian_and_ward_postcall<0, 1> >()) - .def("__iter__", - python::range<return_value_policy<reference_existing_object, - with_custodian_and_ward_postcall<0, 1> > > + .def("__getitem__", posts_getitem, return_internal_reference<>()) + .def("__iter__", python::range<return_internal_reference<> > (&collector_wrapper::begin, &collector_wrapper::end)) ; @@ -264,9 +258,10 @@ void export_journal() ; class_< journal_t, boost::noncopyable > ("Journal") +#if 0 .def(init<path>()) .def(init<string>()) - +#endif .add_property("master", make_getter(&journal_t::master, return_internal_reference<1, @@ -283,13 +278,13 @@ void export_journal() .def("find_account", py_find_account_1, return_internal_reference<1, - with_custodian_and_ward_postcall<0, 1> >()) + with_custodian_and_ward_postcall<1, 0> >()) .def("find_account", py_find_account_2, return_internal_reference<1, - with_custodian_and_ward_postcall<0, 1> >()) + with_custodian_and_ward_postcall<1, 0> >()) .def("find_account_re", &journal_t::find_account_re, return_internal_reference<1, - with_custodian_and_ward_postcall<0, 1> >()) + with_custodian_and_ward_postcall<1, 0> >()) .def("add_xact", &journal_t::add_xact) .def("remove_xact", &journal_t::remove_xact) @@ -298,7 +293,7 @@ void export_journal() #if 0 .def("__getitem__", xacts_getitem, return_internal_reference<1, - with_custodian_and_ward_postcall<0, 1> >()) + with_custodian_and_ward_postcall<1, 0> >()) #endif .def("__iter__", python::range<return_internal_reference<> > @@ -311,16 +306,22 @@ void export_journal() (&journal_t::period_xacts_begin, &journal_t::period_xacts_end)) .def("sources", python::range<return_internal_reference<> > (&journal_t::sources_begin, &journal_t::sources_end)) - +#if 0 .def("read", py_read) - +#endif .def("has_xdata", &journal_t::has_xdata) .def("clear_xdata", &journal_t::clear_xdata) - .def("collect", py_collect, with_custodian_and_ward_postcall<0, 1>()) + .def("query", py_query) .def("valid", &journal_t::valid) ; + +#define EXC_TRANSLATE(type) \ + register_exception_translator<type>(&exc_translate_ ## type); + + EXC_TRANSLATE(parse_error); + EXC_TRANSLATE(error_count); } } // namespace ledger diff --git a/src/py_post.cc b/src/py_post.cc index 62323eb1..692542a0 100644 --- a/src/py_post.cc +++ b/src/py_post.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -116,7 +116,7 @@ void export_post() make_setter(&post_t::xdata_t::datetime)) .add_property("account", make_getter(&post_t::xdata_t::account, - return_value_policy<reference_existing_object>()), + return_internal_reference<>()), make_setter(&post_t::xdata_t::account, with_custodian_and_ward<1, 2>())) .add_property("sort_values", @@ -132,6 +132,9 @@ void export_post() class_< post_t, bases<item_t> > ("Posting") //.def(init<account_t *>()) + .def("id", &post_t::id) + .def("seq", &post_t::seq) + .add_property("xact", make_getter(&post_t::xact, return_internal_reference<>()), @@ -146,11 +149,15 @@ void export_post() make_getter(&post_t::amount), make_setter(&post_t::amount)) .add_property("cost", - make_getter(&post_t::cost), - make_setter(&post_t::cost)) + make_getter(&post_t::cost, + return_value_policy<return_by_value>()), + make_setter(&post_t::cost, + return_value_policy<return_by_value>())) .add_property("assigned_amount", - make_getter(&post_t::assigned_amount), - make_setter(&post_t::assigned_amount)) + make_getter(&post_t::assigned_amount, + return_value_policy<return_by_value>()), + make_setter(&post_t::assigned_amount, + return_value_policy<return_by_value>())) .def("has_tag", py_has_tag_1s) .def("has_tag", py_has_tag_1m) @@ -159,8 +166,8 @@ void export_post() .def("get_tag", py_get_tag_1m) .def("get_tag", py_get_tag_2m) - .def("date", &post_t::date) - .def("effective_date", &post_t::effective_date) + .add_property("date", &post_t::date) + .add_property("aux_date", &post_t::aux_date) .def("must_balance", &post_t::must_balance) @@ -170,8 +177,7 @@ void export_post() .def("has_xdata", &post_t::has_xdata) .def("clear_xdata", &post_t::clear_xdata) - .def("xdata", py_xdata, - return_internal_reference<>()) + .def("xdata", py_xdata, return_internal_reference<>()) //.def("add_to_value", &post_t::add_to_value) .def("set_reported_account", &post_t::set_reported_account) diff --git a/src/py_session.cc b/src/py_session.cc new file mode 100644 index 00000000..f411d5e1 --- /dev/null +++ b/src/py_session.cc @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2003-2012, 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 <system.hh> + +#include "pyinterp.h" +#include "pyutils.h" +#include "session.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + journal_t * py_read_journal(const string& pathname) + { + return python_session->read_journal(path(pathname)); + } + + journal_t * py_read_journal_from_string(const string& data) + { + return python_session->read_journal_from_string(data); + } +} + +void export_session() +{ + class_< session_t, boost::noncopyable > ("Session") + .def("read_journal", &session_t::read_journal, + return_internal_reference<>()) + .def("read_journal_from_string", &session_t::read_journal_from_string, + return_internal_reference<>()) + .def("read_journal_files", &session_t::read_journal_files, + return_internal_reference<>()) + .def("close_journal_files", &session_t::close_journal_files) + ; + + scope().attr("session") = + object(ptr(static_cast<session_t *>(python_session.get()))); + scope().attr("read_journal") = + python::make_function(&py_read_journal, + return_internal_reference<>()); + scope().attr("read_journal_from_string") = + python::make_function(&py_read_journal_from_string, + return_internal_reference<>()); +} + +} // namespace ledger diff --git a/src/py_times.cc b/src/py_times.cc index c2e0b8f8..17f9ec7e 100644 --- a/src/py_times.cc +++ b/src/py_times.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/py_utils.cc b/src/py_utils.cc index 710dca4b..45ffe545 100644 --- a/src/py_utils.cc +++ b/src/py_utils.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/py_value.cc b/src/py_value.cc index f8f36453..b931f008 100644 --- a/src/py_value.cc +++ b/src/py_value.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -51,14 +51,19 @@ namespace { return value.value(CURRENT_TIME()); } boost::optional<value_t> py_value_1(const value_t& value, - commodity_t& in_terms_of) { + const commodity_t * in_terms_of) { return value.value(CURRENT_TIME(), in_terms_of); } boost::optional<value_t> py_value_2(const value_t& value, - commodity_t& in_terms_of, - datetime_t& moment) { + const commodity_t * in_terms_of, + const datetime_t& moment) { return value.value(moment, in_terms_of); } + boost::optional<value_t> py_value_2d(const value_t& value, + const commodity_t * in_terms_of, + const date_t& moment) { + return value.value(datetime_t(moment), in_terms_of); + } PyObject * py_base_type(value_t& value) { @@ -147,7 +152,7 @@ void export_value() .def(init<balance_t>()) .def(init<mask_t>()) .def(init<std::string>()) - // jww (2009-11-02): Need to support conversion eof value_t::sequence_t + // jww (2009-11-02): Need to support conversion of value_t::sequence_t //.def(init<value_t::sequence_t>()) .def(init<value_t>()) @@ -265,9 +270,9 @@ void export_value() .def("value", py_value_0) .def("value", py_value_1, args("in_terms_of")) .def("value", py_value_2, args("in_terms_of", "moment")) + .def("value", py_value_2d, args("in_terms_of", "moment")) - .def("value", &value_t::value, value_overloads()) - .def("price", &value_t::price) + //.def("value", &value_t::value, value_overloads()) .def("exchange_commodities", &value_t::exchange_commodities, exchange_commodities_overloads()) diff --git a/src/py_xact.cc b/src/py_xact.cc index 604d8d59..3d792c7b 100644 --- a/src/py_xact.cc +++ b/src/py_xact.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -76,6 +76,12 @@ namespace { return **elem; } + string py_xact_to_string(xact_t&) + { + // jww (2012-03-01): TODO + return empty_string; + } + } // unnamed namespace using namespace boost::python; @@ -107,6 +113,11 @@ void export_xact() ; class_< xact_t, bases<xact_base_t> > ("Transaction") + .def("id", &xact_t::id) + .def("seq", &xact_t::seq) + + .def("__str__", py_xact_to_string) + .add_property("code", make_getter(&xact_t::code), make_setter(&xact_t::code)) @@ -117,8 +128,6 @@ void export_xact() .def("add_post", &xact_t::add_post, with_custodian_and_ward<1, 2>()) .def("magnitude", &xact_t::magnitude) - .def("idstring", &xact_t::idstring) - .def("id", &xact_t::id) .def("lookup", &xact_t::lookup) diff --git a/src/pyfstream.h b/src/pyfstream.h index 49b072f2..18f28bc4 100644 --- a/src/pyfstream.h +++ b/src/pyfstream.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -86,8 +86,8 @@ protected: public: pyofstream (PyFileObject * fo) : std::ostream(0), buf(fo) { - TRACE_CTOR(pyofstream, "PyFileObject *"); rdbuf(&buf); + TRACE_CTOR(pyofstream, "PyFileObject *"); } ~pyofstream() throw() { TRACE_DTOR(pyofstream); @@ -121,11 +121,11 @@ public: * => force underflow() */ pyinbuf (PyFileObject * _fo) : fo(_fo) { - TRACE_CTOR(pyinbuf, "PyFileObject *"); - setg (buffer+pbSize, // beginning of putback area buffer+pbSize, // read position buffer+pbSize); // end position + + TRACE_CTOR(pyinbuf, "PyFileObject *"); } ~pyinbuf() throw() { TRACE_DTOR(pyinbuf); @@ -191,8 +191,8 @@ protected: public: pyifstream (PyFileObject * fo) : std::istream(0), buf(fo) { - TRACE_CTOR(pyifstream, "PyFileObject *"); rdbuf(&buf); + TRACE_CTOR(pyifstream, "PyFileObject *"); } ~pyifstream() throw() { TRACE_DTOR(pyifstream); diff --git a/src/pyinterp.cc b/src/pyinterp.cc index e0fd2d59..8d9c8c84 100644 --- a/src/pyinterp.cc +++ b/src/pyinterp.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -32,6 +32,7 @@ #include <system.hh> #include "pyinterp.h" +#include "pyutils.h" #include "account.h" #include "xact.h" #include "post.h" @@ -51,6 +52,7 @@ void export_commodity(); void export_expr(); void export_format(); void export_item(); +void export_session(); void export_journal(); void export_post(); void export_times(); @@ -72,6 +74,7 @@ void initialize_for_python() export_item(); export_post(); export_xact(); + export_session(); export_journal(); } @@ -81,16 +84,56 @@ struct python_run python_run(python_interpreter_t * intepreter, const string& str, int input_mode) - : result(handle<>(borrowed(PyRun_String(str.c_str(), input_mode, - intepreter->main_nspace.ptr(), - intepreter->main_nspace.ptr())))) {} + : result + (handle<> + (borrowed + (PyRun_String(str.c_str(), input_mode, + intepreter->main_module->module_globals.ptr(), + intepreter->main_module->module_globals.ptr())))) {} operator object() { return result; } }; +python_module_t::python_module_t(const string& name) + : scope_t(), module_name(name), module_globals() +{ + import_module(name); +} + +python_module_t::python_module_t(const string& name, python::object obj) + : scope_t(), module_name(name), module_globals() +{ + module_object = obj; + module_globals = extract<dict>(module_object.attr("__dict__")); +} + +void python_module_t::import_module(const string& name, bool import_direct) +{ + object mod = python::import(name.c_str()); + if (! mod) + throw_(std::runtime_error, + _("Module import failed (couldn't find %1)") << name); + + dict globals = extract<dict>(mod.attr("__dict__")); + if (! globals) + throw_(std::runtime_error, + _("Module import failed (couldn't find %1)") << name); + + if (! import_direct) { + module_object = mod; + module_globals = globals; + } else { + // Import all top-level entries directly into the namespace + module_globals.update(mod.attr("__dict__")); + } +} + void python_interpreter_t::initialize() { + if (is_initialized) + return; + TRACE_START(python_init, 1, "Initialized Python"); try { @@ -101,15 +144,7 @@ void python_interpreter_t::initialize() hack_system_paths(); - object main_module = python::import("__main__"); - if (! main_module) - throw_(std::runtime_error, - _("Python failed to initialize (couldn't find __main__)")); - - main_nspace = extract<dict>(main_module.attr("__dict__")); - if (! main_nspace) - throw_(std::runtime_error, - _("Python failed to initialize (couldn't find __dict__)")); + main_module = import_module("__main__"); python::detail::init_module("ledger", &initialize_for_python); @@ -167,58 +202,57 @@ void python_interpreter_t::hack_system_paths() #endif } -object python_interpreter_t::import_into_main(const string& str) +object python_interpreter_t::import_option(const string& str) { if (! is_initialized) initialize(); - try { - object mod = python::import(str.c_str()); - if (! mod) - throw_(std::runtime_error, - _("Failed to import Python module %1") << str); - - // Import all top-level entries directly into the main namespace - main_nspace.update(mod.attr("__dict__")); - - return mod; - } - catch (const error_already_set&) { - PyErr_Print(); - } - return object(); -} - -object python_interpreter_t::import_option(const string& str) -{ - path file(str); - python::object sys_module = python::import("sys"); python::object sys_dict = sys_module.attr("__dict__"); + path file(str); + string name(str); python::list paths(sys_dict["path"]); + if (contains(str, ".py")) { #if BOOST_VERSION >= 103700 - paths.insert(0, file.parent_path().string()); - sys_dict["path"] = paths; + path& cwd(parsing_context.get_current().current_directory); +#if BOOST_VERSION >= 104600 && BOOST_FILESYSTEM_VERSION >= 3 + path parent(filesystem::absolute(file, cwd).parent_path()); +#else + path parent(filesystem::complete(file, cwd).parent_path()); +#endif + DEBUG("python.interp", "Adding " << parent << " to PYTHONPATH"); + paths.insert(0, parent.string()); + sys_dict["path"] = paths; #if BOOST_VERSION >= 104600 - string name = file.filename().string(); - if (contains(name, ".py")) name = file.stem().string(); #else - string name = file.filename(); - if (contains(name, ".py")) name = file.stem(); #endif #else // BOOST_VERSION >= 103700 - paths.insert(0, file.branch_path().string()); - sys_dict["path"] = paths; - - string name = file.leaf(); + paths.insert(0, file.branch_path().string()); + sys_dict["path"] = paths; + name = file.leaf(); #endif // BOOST_VERSION >= 103700 + } - return python::import(python::str(name.c_str())); + try { + if (contains(str, ".py")) + main_module->import_module(name, true); + else + import_module(str); + } + catch (const error_already_set&) { + PyErr_Print(); + throw_(std::runtime_error, _("Python failed to import: %1") << str); + } + catch (...) { + throw; + } + + return object(); } object python_interpreter_t::eval(std::istream& in, py_eval_mode_t mode) @@ -346,13 +380,13 @@ value_t python_interpreter_t::server_command(call_scope_t& args) functor_t func(main_function, "main"); try { func(args); + return true; } catch (const error_already_set&) { PyErr_Print(); throw_(std::runtime_error, _("Error while invoking ledger.server's main() function")); } - return true; } else { throw_(std::runtime_error, _("The ledger.server module is missing its main() function!")); @@ -372,6 +406,40 @@ python_interpreter_t::lookup_option(const char * p) return NULL; } +expr_t::ptr_op_t python_module_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + switch (kind) { + case symbol_t::FUNCTION: + DEBUG("python.interp", "Python lookup: " << name); + if (module_globals.has_key(name.c_str())) { + if (python::object obj = module_globals.get(name.c_str())) { + if (PyModule_Check(obj.ptr())) { + shared_ptr<python_module_t> mod; + python_module_map_t::iterator i = + python_session->modules_map.find(obj.ptr()); + if (i == python_session->modules_map.end()) { + mod.reset(new python_module_t(name, obj)); + python_session->modules_map.insert + (python_module_map_t::value_type(obj.ptr(), mod)); + } else { + mod = (*i).second; + } + return expr_t::op_t::wrap_value(scope_value(mod.get())); + } else { + return WRAP_FUNCTOR(python_interpreter_t::functor_t(obj, name)); + } + } + } + break; + + default: + break; + } + + return NULL; +} + expr_t::ptr_op_t python_interpreter_t::lookup(const symbol_t::kind_t kind, const string& name) { @@ -381,21 +449,18 @@ expr_t::ptr_op_t python_interpreter_t::lookup(const symbol_t::kind_t kind, switch (kind) { case symbol_t::FUNCTION: - if (option_t<python_interpreter_t> * handler = lookup_option(name.c_str())) - return MAKE_OPT_FUNCTOR(python_interpreter_t, handler); - - if (is_initialized && main_nspace.has_key(name.c_str())) { - DEBUG("python.interp", "Python lookup: " << name); - - if (python::object obj = main_nspace.get(name.c_str())) - return WRAP_FUNCTOR(functor_t(obj, name)); - } + if (is_initialized) + return main_module->lookup(kind, name); break; - case symbol_t::OPTION: + case symbol_t::OPTION: { if (option_t<python_interpreter_t> * handler = lookup_option(name.c_str())) return MAKE_OPT_HANDLER(python_interpreter_t, handler); + + if (is_initialized) + return main_module->lookup(symbol_t::FUNCTION, string("option_") + name); break; + } case symbol_t::PRECOMMAND: { const char * p = name.c_str(); @@ -420,29 +485,59 @@ expr_t::ptr_op_t python_interpreter_t::lookup(const symbol_t::kind_t kind, } namespace { - void append_value(list& lst, const value_t& value) + object convert_value_to_python(const value_t& val) { - if (value.is_scope()) { - const scope_t * scope = value.as_scope(); - if (const post_t * post = dynamic_cast<const post_t *>(scope)) - lst.append(ptr(post)); - else if (const xact_t * xact = dynamic_cast<const xact_t *>(scope)) - lst.append(ptr(xact)); - else if (const account_t * account = - dynamic_cast<const account_t *>(scope)) - lst.append(ptr(account)); - else if (const period_xact_t * period_xact = - dynamic_cast<const period_xact_t *>(scope)) - lst.append(ptr(period_xact)); - else if (const auto_xact_t * auto_xact = - dynamic_cast<const auto_xact_t *>(scope)) - lst.append(ptr(auto_xact)); - else - throw_(std::logic_error, - _("Cannot downcast scoped object to specific type")); - } else { - lst.append(value); + switch (val.type()) { + case value_t::VOID: // a null value (i.e., uninitialized) + return object(); + case value_t::BOOLEAN: // a boolean + return object(val.to_boolean()); + case value_t::DATETIME: // a date and time (Boost posix_time) + return object(val.to_datetime()); + case value_t::DATE: // a date (Boost gregorian::date) + return object(val.to_date()); + case value_t::INTEGER: // a signed integer value + return object(val.to_long()); + case value_t::AMOUNT: // a ledger::amount_t + return object(val.as_amount()); + case value_t::BALANCE: // a ledger::balance_t + return object(val.as_balance()); + case value_t::STRING: // a string object + return object(handle<>(borrowed(str_to_py_unicode(val.as_string())))); + case value_t::MASK: // a regular expression mask + return object(val); + case value_t::SEQUENCE: { // a vector of value_t objects + list arglist; + foreach (const value_t& elem, val.as_sequence()) + arglist.append(elem); + return arglist; + } + case value_t::SCOPE: // a pointer to a scope + if (const scope_t * scope = val.as_scope()) { + if (const post_t * post = dynamic_cast<const post_t *>(scope)) + return object(ptr(post)); + else if (const xact_t * xact = dynamic_cast<const xact_t *>(scope)) + return object(ptr(xact)); + else if (const account_t * account = + dynamic_cast<const account_t *>(scope)) + return object(ptr(account)); + else if (const period_xact_t * period_xact = + dynamic_cast<const period_xact_t *>(scope)) + return object(ptr(period_xact)); + else if (const auto_xact_t * auto_xact = + dynamic_cast<const auto_xact_t *>(scope)) + return object(ptr(auto_xact)); + else + throw_(std::logic_error, + _("Cannot downcast scoped object to specific type")); + } + return object(); + case value_t::ANY: // a pointer to an arbitrary object + return object(val); } +#if !defined(__clang__) + return object(); +#endif } } @@ -453,6 +548,7 @@ value_t python_interpreter_t::functor_t::operator()(call_scope_t& args) if (! PyCallable_Check(func.ptr())) { extract<value_t> val(func); + DEBUG("python.interp", "Value of Python '" << name << "': " << val); std::signal(SIGINT, sigint_handler); if (val.check()) return val(); @@ -464,9 +560,9 @@ value_t python_interpreter_t::functor_t::operator()(call_scope_t& args) // rather than a sequence of arguments? if (args.value().is_sequence()) foreach (const value_t& value, args.value().as_sequence()) - append_value(arglist, value); + arglist.append(convert_value_to_python(value)); else - append_value(arglist, args.value()); + arglist.append(convert_value_to_python(args.value())); if (PyObject * val = PyObject_CallObject(func.ptr(), python::tuple(arglist).ptr())) { @@ -474,11 +570,12 @@ value_t python_interpreter_t::functor_t::operator()(call_scope_t& args) value_t result; if (xval.check()) { result = xval(); + DEBUG("python.interp", + "Return from Python '" << name << "': " << result); Py_DECREF(val); } else { Py_DECREF(val); - throw_(calc_error, - _("Could not evaluate Python variable '%1'") << name); + return NULL_VALUE; } std::signal(SIGINT, sigint_handler); return result; diff --git a/src/pyinterp.h b/src/pyinterp.h index ea947c5a..556b1563 100644 --- a/src/pyinterp.h +++ b/src/pyinterp.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -38,20 +38,52 @@ namespace ledger { +class python_module_t : public scope_t, public noncopyable +{ +public: + string module_name; + python::object module_object; + python::dict module_globals; + + explicit python_module_t(const string& name); + explicit python_module_t(const string& name, python::object obj); + + void import_module(const string& name, bool import_direct = false); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + void define_global(const string& name, python::object obj) { + module_globals[name] = obj; + } + + virtual string description() { + return module_name; + } +}; + +typedef std::map<PyObject *, shared_ptr<python_module_t> > python_module_map_t; + class python_interpreter_t : public session_t { public: - python::dict main_nspace; bool is_initialized; - python_interpreter_t() - : session_t(), main_nspace(), is_initialized(false) { + shared_ptr<python_module_t> main_module; + python_module_map_t modules_map; + + shared_ptr<python_module_t> import_module(const string& name) { + shared_ptr<python_module_t> mod(new python_module_t(name)); + if (name != "__main__") + main_module->define_global(name, mod->module_object); + return mod; + } + + python_interpreter_t() : session_t(), is_initialized(false) { TRACE_CTOR(python_interpreter_t, ""); } - virtual ~python_interpreter_t() { TRACE_DTOR(python_interpreter_t); - if (is_initialized) Py_Finalize(); } @@ -59,7 +91,6 @@ public: void initialize(); void hack_system_paths(); - python::object import_into_main(const string& name); python::object import_option(const string& name); enum py_eval_mode_t { @@ -68,14 +99,10 @@ public: PY_EVAL_MULTI }; - python::object eval(std::istream& in, - py_eval_mode_t mode = PY_EVAL_EXPR); - python::object eval(const string& str, - py_eval_mode_t mode = PY_EVAL_EXPR); - python::object eval(const char * c_str, - py_eval_mode_t mode = PY_EVAL_EXPR) { - string str(c_str); - return eval(str, mode); + python::object eval(std::istream& in, py_eval_mode_t mode = PY_EVAL_EXPR); + python::object eval(const string& str, py_eval_mode_t mode = PY_EVAL_EXPR); + python::object eval(const char * c_str, py_eval_mode_t mode = PY_EVAL_EXPR) { + return eval(string(c_str), mode); } value_t python_command(call_scope_t& scope); @@ -109,8 +136,8 @@ public: virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, const string& name); - OPTION_(python_interpreter_t, import_, DO_(args) { - parent->import_option(args.get<string>(1)); + OPTION_(python_interpreter_t, import_, DO_(str) { + parent->import_option(str); }); }; diff --git a/src/pyledger.cc b/src/pyledger.cc index 4a53532a..cf5e362e 100644 --- a/src/pyledger.cc +++ b/src/pyledger.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/pyutils.h b/src/pyutils.h index 7e016502..44bb6d90 100644 --- a/src/pyutils.h +++ b/src/pyutils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/query.cc b/src/query.cc index 812123cb..3fec708a 100644 --- a/src/query.cc +++ b/src/query.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -146,7 +146,7 @@ query_t::lexer_t::next_token(query_t::lexer_t::token_t::kind_t tok_context) case '\t': case '\n': case '\r': - if (! multiple_args && ! consume_whitespace) + if (! multiple_args && ! consume_whitespace && ! consume_next_arg) goto test_ident; else ident.push_back(*arg_i); diff --git a/src/query.h b/src/query.h index 8f7917b2..fe52eb35 100644 --- a/src/query.h +++ b/src/query.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -153,8 +153,6 @@ public: case TERM: return string("TERM(") + *value + ")"; case END_REACHED: return "END_REACHED"; } - assert(false); - return empty_string; } string symbol() const { @@ -188,6 +186,9 @@ public: assert(false); return "<UNKNOWN>"; } +#if !defined(__clang__) + return "<ERROR>"; +#endif } void unexpected(); @@ -203,10 +204,11 @@ public: consume_whitespace(false), consume_next_arg(false), multiple_args(_multiple_args) { - TRACE_CTOR(query_t::lexer_t, ""); assert(begin != end); arg_i = (*begin).as_string().begin(); arg_end = (*begin).as_string().end(); + + TRACE_CTOR(query_t::lexer_t, ""); } lexer_t(const lexer_t& lexer) : begin(lexer.begin), end(lexer.end), @@ -301,18 +303,19 @@ public: query_t(const string& arg, const keep_details_t& what_to_keep = keep_details_t(), bool multiple_args = true) { - TRACE_CTOR(query_t, "string, keep_details_t, bool"); if (! arg.empty()) { value_t temp(string_value(arg)); parse_args(temp.to_sequence(), what_to_keep, multiple_args); } + TRACE_CTOR(query_t, "string, keep_details_t, bool"); } query_t(const value_t& args, const keep_details_t& what_to_keep = keep_details_t(), bool multiple_args = true) { - TRACE_CTOR(query_t, "value_t, keep_details_t, bool"); if (! args.empty()) parse_args(args, what_to_keep, multiple_args); + + TRACE_CTOR(query_t, "value_t, keep_details_t, bool"); } virtual ~query_t() { TRACE_DTOR(query_t); diff --git a/src/quotes.cc b/src/quotes.cc index 0cc8d06b..c33e0826 100644 --- a/src/quotes.cc +++ b/src/quotes.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -40,7 +40,7 @@ namespace ledger { optional<price_point_t> commodity_quote_from_script(commodity_t& commodity, - const optional<commodity_t&>& exchange_commodity) + const commodity_t * exchange_commodity) { DEBUG("commodity.download", "downloading quote for symbol " << commodity.symbol()); #if defined(DEBUG_ON) diff --git a/src/quotes.h b/src/quotes.h index 376d8918..56740e47 100644 --- a/src/quotes.h +++ b/src/quotes.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -46,7 +46,7 @@ namespace ledger { optional<price_point_t> commodity_quote_from_script(commodity_t& commodity, - const optional<commodity_t&>& exchange_commodity); + const commodity_t * exchange_commodity); } // namespace ledger diff --git a/src/report.cc b/src/report.cc index f4dc450e..c7559cf7 100644 --- a/src/report.cc +++ b/src/report.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -41,6 +41,7 @@ #include "iterators.h" #include "filters.h" #include "precmd.h" +#include "select.h" #include "stats.h" #include "generate.h" #include "draft.h" @@ -59,7 +60,7 @@ void report_t::normalize_options(const string& verb) #ifdef HAVE_ISATTY if (! HANDLED(force_color)) { if (! HANDLED(no_color) && isatty(STDOUT_FILENO)) - HANDLER(color).on_only(string("?normalize")); + HANDLER(color).on("?normalize"); if (HANDLED(color) && ! isatty(STDOUT_FILENO)) HANDLER(color).off(); } @@ -76,15 +77,14 @@ void report_t::normalize_options(const string& verb) HANDLER(pager_).off(); } - item_t::use_effective_date = (HANDLED(effective) && - ! HANDLED(actual_dates)); + item_t::use_aux_date = (HANDLED(aux_date) && ! HANDLED(primary_date)); commodity_pool_t::current_pool->keep_base = HANDLED(base); commodity_pool_t::current_pool->get_quotes = session.HANDLED(download); if (session.HANDLED(price_exp_)) commodity_pool_t::current_pool->quote_leeway = - session.HANDLER(price_exp_).value.as_long(); + lexical_cast<long>(session.HANDLER(price_exp_).value) * 3600L; if (session.HANDLED(price_db_)) commodity_pool_t::current_pool->price_db = session.HANDLER(price_db_).str(); @@ -107,39 +107,35 @@ void report_t::normalize_options(const string& verb) if (! HANDLED(meta_width_)) { string::size_type i = HANDLER(meta_).str().find(':'); if (i != string::npos) { - HANDLED(meta_width_).on_with - (string("?normalize"), - lexical_cast<long>(string(HANDLER(meta_).str(), i + 1))); - HANDLED(meta_).on(string("?normalize"), + HANDLED(meta_width_).on("?normalize", + string(HANDLER(meta_).str(), i + 1)); + HANDLED(meta_).on("?normalize", string(HANDLER(meta_).str(), 0, i)); } } if (HANDLED(meta_width_)) { - HANDLER(prepend_format_).on - (string("?normalize"), - string("%(justify(truncated(tag(\"") + - HANDLER(meta_).str() + "\"), " + - HANDLED(meta_width_).value.to_string() + " - 1), " + - HANDLED(meta_width_).value.to_string() + "))"); - meta_width = HANDLED(meta_width_).value.to_long(); + HANDLER(prepend_format_) + .on("?normalize", string("%(justify(truncated(tag(\"") + + HANDLER(meta_).str() + "\"), " + + HANDLED(meta_width_).value + " - 1), " + + HANDLED(meta_width_).value + "))"); + meta_width = lexical_cast<long>(HANDLED(meta_width_).value); } else { - HANDLER(prepend_format_).on(string("?normalize"), string("%(tag(\"") + - HANDLER(meta_).str() + "\"))"); + HANDLER(prepend_format_) + .on("?normalize", string("%(tag(\"") + HANDLER(meta_).str() + "\"))"); } } - if (! HANDLED(prepend_width_)) - HANDLER(prepend_width_).on_with(string("?normalize"), static_cast<long>(0)); if (verb == "print" || verb == "xact" || verb == "dump") { - HANDLER(related).on_only(string("?normalize")); - HANDLER(related_all).on_only(string("?normalize")); + HANDLER(related_all).parent = this; + HANDLER(related_all).on("?normalize"); } else if (verb == "equity") { - HANDLER(equity).on_only(string("?normalize")); + HANDLER(equity).on("?normalize"); } if (verb[0] != 'b' && verb[0] != 'r') - HANDLER(base).on_only(string("?normalize")); + HANDLER(base).on("?normalize"); // If a time period was specified with -p, check whether it also gave a // begin and/or end to the report period (though these can be overridden @@ -153,12 +149,10 @@ void report_t::normalize_options(const string& verb) // to avoid option ordering issues were we to have done it during the // initial parsing of the options. if (HANDLED(amount_data)) { - HANDLER(format_) - .on_with(string("?normalize"), HANDLER(plot_amount_format_).value); + HANDLER(format_).on("?normalize", HANDLER(plot_amount_format_).value); } else if (HANDLED(total_data)) { - HANDLER(format_) - .on_with(string("?normalize"), HANDLER(plot_total_format_).value); + HANDLER(format_).on("?normalize", HANDLER(plot_total_format_).value); } // If the --exchange (-X) option was used, parse out any final price @@ -169,9 +163,26 @@ void report_t::normalize_options(const string& verb) terminus); } + if (HANDLED(percent)) { + commodity_t::decimal_comma_by_default = false; + if (HANDLED(market)) { + HANDLER(total_) + .on("?normalize", + "(__tmp = market(parent.total, value_date, exchange);" + " ((is_account & parent & __tmp) ?" + " percent(scrub(market(total, value_date, exchange)), " + " scrub(__tmp)) : 0))"); + } + } + + if (HANDLED(immediate) && HANDLED(market)) { + HANDLER(amount_) + .on("?normalize", "market(amount_expr, value_date, exchange)"); + } + long cols = 0; if (HANDLED(columns_)) - cols = HANDLER(columns_).value.to_long(); + cols = lexical_cast<long>(HANDLER(columns_).value); else if (const char * columns = std::getenv("COLUMNS")) cols = lexical_cast<long>(columns); else @@ -183,23 +194,21 @@ void report_t::normalize_options(const string& verb) if (cols > 0) { DEBUG("auto.columns", "cols = " << cols); - if (! HANDLER(date_width_).specified) - HANDLER(date_width_) - .on_with(none, static_cast<long>(format_date(CURRENT_DATE(), - FMT_PRINTED).length())); - - long date_width = HANDLER(date_width_).value.to_long(); - long payee_width = (HANDLER(payee_width_).specified ? - HANDLER(payee_width_).value.to_long() : - int(double(cols) * 0.263157)); - long account_width = (HANDLER(account_width_).specified ? - HANDLER(account_width_).value.to_long() : - int(double(cols) * 0.302631)); - long amount_width = (HANDLER(amount_width_).specified ? - HANDLER(amount_width_).value.to_long() : - int(double(cols) * 0.157894)); - long total_width = (HANDLER(total_width_).specified ? - HANDLER(total_width_).value.to_long() : + long date_width = (HANDLED(date_width_) ? + lexical_cast<long>(HANDLER(date_width_).str()) : + static_cast<long> + (format_date(CURRENT_DATE(),FMT_PRINTED).length())); + long payee_width = (HANDLED(payee_width_) ? + lexical_cast<long>(HANDLER(payee_width_).str()) : + long(double(cols) * 0.263157)); + long account_width = (HANDLED(account_width_) ? + lexical_cast<long>(HANDLER(account_width_).str()) : + long(double(cols) * 0.302631)); + long amount_width = (HANDLED(amount_width_) ? + lexical_cast<long>(HANDLER(amount_width_).str()) : + long(double(cols) * 0.157894)); + long total_width = (HANDLED(total_width_) ? + lexical_cast<long>(HANDLER(total_width_).str()) : amount_width); DEBUG("auto.columns", "date_width = " << date_width); @@ -208,32 +217,46 @@ void report_t::normalize_options(const string& verb) DEBUG("auto.columns", "amount_width = " << amount_width); DEBUG("auto.columns", "total_width = " << total_width); - if (! HANDLER(date_width_).specified && - ! HANDLER(payee_width_).specified && - ! HANDLER(account_width_).specified && - ! HANDLER(amount_width_).specified && - ! HANDLER(total_width_).specified) { + if (! HANDLED(date_width_) && + ! HANDLED(payee_width_) && + ! HANDLED(account_width_) && + ! HANDLED(amount_width_) && + ! HANDLED(total_width_)) { long total = (4 /* the spaces between */ + date_width + payee_width + - account_width + amount_width + total_width); - if (total > cols) { + account_width + amount_width + total_width + + (HANDLED(dc) ? 1 + amount_width : 0)); + while (total > cols && account_width > 5 && payee_width > 5) { DEBUG("auto.columns", "adjusting account down"); - account_width -= total - cols; + if (total > cols) { + --account_width; + --total; + if (total > cols) { + --account_width; + --total; + } + } + if (total > cols) { + --payee_width; + --total; + } DEBUG("auto.columns", "account_width now = " << account_width); } } if (! HANDLED(meta_width_)) - HANDLER(meta_width_).on_with(string("?normalize"), 0L); - if (! HANDLER(date_width_).specified) - HANDLER(date_width_).on_with(string("?normalize"), date_width); - if (! HANDLER(payee_width_).specified) - HANDLER(payee_width_).on_with(string("?normalize"), payee_width); - if (! HANDLER(account_width_).specified) - HANDLER(account_width_).on_with(string("?normalize"), account_width); - if (! HANDLER(amount_width_).specified) - HANDLER(amount_width_).on_with(string("?normalize"), amount_width); - if (! HANDLER(total_width_).specified) - HANDLER(total_width_).on_with(string("?normalize"), total_width); + HANDLER(meta_width_).value = "0"; + if (! HANDLED(prepend_width_)) + HANDLER(prepend_width_).value = "0"; + if (! HANDLED(date_width_)) + HANDLER(date_width_).value = to_string(date_width); + if (! HANDLED(payee_width_)) + HANDLER(payee_width_).value = to_string(payee_width); + if (! HANDLED(account_width_)) + HANDLER(account_width_).value = to_string(account_width); + if (! HANDLED(amount_width_)) + HANDLER(amount_width_).value = to_string(amount_width); + if (! HANDLED(total_width_)) + HANDLER(total_width_).value = to_string(total_width); } } @@ -256,7 +279,7 @@ void report_t::normalize_period() if (! interval.duration) HANDLER(period_).off(); else if (! HANDLED(sort_all_)) - HANDLER(sort_xacts_).on_only(string("?normalize")); + HANDLER(sort_xacts_).on("?normalize"); } void report_t::parse_query_args(const value_t& args, const string& whence) @@ -279,7 +302,7 @@ void report_t::parse_query_args(const value_t& args, const string& whence) } if (query.has_query(query_t::QUERY_BOLD)) { - HANDLER(bold_if_).set_expr(whence, query.get_query(query_t::QUERY_BOLD)); + HANDLER(bold_if_).on(whence, query.get_query(query_t::QUERY_BOLD)); DEBUG("report.predicate", "Bolding predicate = " << HANDLER(bold_if_).str()); } @@ -298,7 +321,12 @@ namespace { report_t& report; posts_flusher(post_handler_ptr _handler, report_t& _report) - : handler(_handler), report(_report) {} + : handler(_handler), report(_report) { + TRACE_CTOR(posts_flusher, "post_handler_ptr, report_t&"); + } + ~posts_flusher() throw() { + TRACE_DTOR(posts_flusher); + } void operator()(const value_t&) { report.session.journal->clear_xdata(); @@ -310,7 +338,7 @@ void report_t::posts_report(post_handler_ptr handler) { handler = chain_post_handlers(handler, *this); if (HANDLED(group_by_)) { - std::auto_ptr<post_splitter> + unique_ptr<post_splitter> splitter(new post_splitter(handler, *this, HANDLER(group_by_).expr)); splitter->set_postflush_func(posts_flusher(handler, *this)); handler = post_handler_ptr(splitter.release()); @@ -330,9 +358,9 @@ void report_t::generate_report(post_handler_ptr handler) generate_posts_iterator walker (session, HANDLED(seed_) ? - static_cast<unsigned int>(HANDLER(seed_).value.to_long()) : 0, + lexical_cast<unsigned int>(HANDLER(seed_).str()) : 0, HANDLED(head_) ? - static_cast<unsigned int>(HANDLER(head_).value.to_long()) : 50); + lexical_cast<unsigned int>(HANDLER(head_).str()) : 50); pass_down_posts<generate_posts_iterator>(handler, walker); } @@ -447,8 +475,20 @@ void report_t::commodities_report(post_handler_ptr handler) { handler = chain_handlers(handler, *this); - posts_commodities_iterator walker(*session.journal.get()); - pass_down_posts<posts_commodities_iterator>(handler, walker); + posts_commodities_iterator * walker(new posts_commodities_iterator(*session.journal.get())); + try { + pass_down_posts<posts_commodities_iterator>(handler, *walker); + } + catch (...) { +#if defined(VERIFY_ON) + IF_VERIFY() { + // If --verify was used, clean up the posts_commodities_iterator. + // Otherwise, just leak like a sieve. + checked_delete(walker); + } +#endif + throw; + } session.journal->clear_xdata(); } @@ -514,20 +554,32 @@ value_t report_t::fn_should_bold(call_scope_t& scope) value_t report_t::fn_market(call_scope_t& args) { - optional<datetime_t> moment = (args.has<datetime_t>(1) ? - args.get<datetime_t>(1) : - optional<datetime_t>()); value_t result; + value_t arg0 = args[0]; + + datetime_t moment; + if (args.has<datetime_t>(1)) + moment = args.get<datetime_t>(1); + + if (arg0.is_string()) { + amount_t tmp(1L); + commodity_t * commodity = + commodity_pool_t::current_pool->find_or_create(arg0.as_string()); + tmp.set_commodity(*commodity); + arg0 = tmp; + } + + string target_commodity; if (args.has<string>(2)) - result = args[0].exchange_commodities(args.get<string>(2), - /* add_prices= */ false, moment); - else - result = args[0].value(moment); + target_commodity = args.get<string>(2); - if (! result.is_null()) - return result; + if (! target_commodity.empty()) + result = arg0.exchange_commodities(target_commodity, + /* add_prices= */ false, moment); + else + result = arg0.value(moment); - return args[0]; + return ! result.is_null() ? result : arg0; } value_t report_t::fn_get_at(call_scope_t& args) @@ -536,13 +588,20 @@ value_t report_t::fn_get_at(call_scope_t& args) if (index == 0) { if (! args[0].is_sequence()) return args[0]; - } else { - if (! args[0].is_sequence()) - throw_(std::runtime_error, - _("Attempting to get argument at index %1 from %2") - << index << args[0].label()); } - return args[0].as_sequence()[index]; + else if (! args[0].is_sequence()) { + throw_(std::runtime_error, + _("Attempting to get argument at index %1 from %2") + << index << args[0].label()); + } + + value_t::sequence_t& seq(args[0].as_sequence_lval()); + if (index >= seq.size()) + throw_(std::runtime_error, + _("Attempting to get index %1 from %2 with %3 elements") + << index << args[0].label() << seq.size()); + + return seq[index]; } value_t report_t::fn_is_seq(call_scope_t& scope) @@ -703,6 +762,15 @@ value_t report_t::fn_format_date(call_scope_t& args) return string_value(format_date(args.get<date_t>(0), FMT_PRINTED)); } +value_t report_t::fn_format_datetime(call_scope_t& args) +{ + if (args.has<string>(1)) + return string_value(format_datetime(args.get<datetime_t>(0), FMT_CUSTOM, + args.get<string>(1).c_str())); + else + return string_value(format_datetime(args.get<datetime_t>(0), FMT_PRINTED)); +} + value_t report_t::fn_ansify_if(call_scope_t& args) { if (args.has<string>(1)) { @@ -732,14 +800,62 @@ value_t report_t::fn_percent(call_scope_t& args) (args.get<amount_t>(0) / args.get<amount_t>(1)).number()); } -value_t report_t::fn_price(call_scope_t& args) +value_t report_t::fn_commodity(call_scope_t& args) { - return args[0].price(); + return string_value(args.get<amount_t>(0).commodity().symbol()); } -value_t report_t::fn_commodity(call_scope_t& args) +value_t report_t::fn_nail_down(call_scope_t& args) { - return string_value(args.get<amount_t>(0).commodity().symbol()); + value_t arg0(args[0]); + value_t arg1(args[1]); + + switch (arg0.type()) { + case value_t::AMOUNT: { + amount_t tmp(arg0.as_amount()); + if (tmp.has_commodity() && ! tmp.is_null() && ! tmp.is_realzero()) { + arg1 = arg1.strip_annotations(keep_details_t()).to_amount(); + expr_t value_expr(is_expr(arg1) ? + as_expr(arg1) : + expr_t::op_t::wrap_value(arg1.unrounded() / + arg0.number())); + std::ostringstream buf; + value_expr.print(buf); + value_expr.set_text(buf.str()); + + tmp.set_commodity(tmp.commodity().nail_down(value_expr)); + } + return tmp; + } + + case value_t::BALANCE: { + balance_t tmp; + foreach (const balance_t::amounts_map::value_type& pair, + arg0.as_balance_lval().amounts) { + call_scope_t inner_args(*args.parent); + inner_args.push_back(pair.second); + inner_args.push_back(arg1); + tmp += fn_nail_down(inner_args).as_amount(); + } + return tmp; + } + + case value_t::SEQUENCE: { + value_t tmp; + foreach (value_t& value, arg0.as_sequence_lval()) { + call_scope_t inner_args(*args.parent); + inner_args.push_back(value); + inner_args.push_back(arg1); + tmp.push_back(fn_nail_down(inner_args)); + } + return tmp; + } + + default: + throw_(std::runtime_error, _("Attempting to nail down %1") + << args[0].label()); + } + return arg0; } value_t report_t::fn_lot_date(call_scope_t& args) @@ -879,11 +995,9 @@ value_t report_t::echo_command(call_scope_t& args) value_t report_t::pricemap_command(call_scope_t& args) { std::ostream& out(output_stream); - - commodity_pool_t::current_pool->print_pricemap - (out, what_to_keep(), args.has<string>(0) ? - optional<datetime_t>(datetime_t(parse_date(args.get<string>(0)))) : none); - + commodity_pool_t::current_pool->commodity_price_history.print_map + (out, args.has<string>(0) ? + datetime_t(parse_date(args.get<string>(0))) : datetime_t()); return true; } @@ -914,6 +1028,9 @@ option_t<report_t> * report_t::lookup_option(const char * p) case 'G': OPT_CH(gain); break; + case 'H': + OPT_CH(historical); + break; case 'I': OPT_CH(price); break; @@ -960,12 +1077,13 @@ option_t<report_t> * report_t::lookup_option(const char * p) OPT(abbrev_len_); else OPT_(account_); else OPT(actual); - else OPT(actual_dates); else OPT(add_budget); else OPT(amount_); else OPT(amount_data); + else OPT_ALT(primary_date, actual_dates); else OPT(anon); else OPT_ALT(color, ansi); + else OPT(auto_match); else OPT(average); else OPT(account_width_); else OPT(amount_width_); @@ -977,6 +1095,7 @@ option_t<report_t> * report_t::lookup_option(const char * p) else OPT_(begin_); else OPT(bold_if_); else OPT(budget); + else OPT(budget_format_); else OPT(by_payee); break; case 'c': @@ -995,21 +1114,22 @@ option_t<report_t> * report_t::lookup_option(const char * p) else OPT(date_); else OPT(date_format_); else OPT(datetime_format_); + else OPT(dc); else OPT(depth_); else OPT(deviation); else OPT_(display_); else OPT(display_amount_); else OPT(display_total_); - else OPT_ALT(dow, days-of-week); + else OPT_ALT(dow, days_of_week); else OPT(date_width_); break; case 'e': - OPT(effective); - else OPT(empty); + OPT(empty); else OPT_(end_); else OPT(equity); else OPT(exact); else OPT(exchange_); + else OPT_ALT(aux_date, effective); break; case 'f': OPT(flat); @@ -1021,17 +1141,19 @@ option_t<report_t> * report_t::lookup_option(const char * p) else OPT_ALT(head_, first_); break; case 'g': - OPT(gain); + OPT_ALT(gain, change); else OPT(group_by_); else OPT(group_title_format_); else OPT(generated); break; case 'h': OPT(head_); + else OPT(historical); break; case 'i': OPT(invert); else OPT(inject_); + else OPT(immediate); break; case 'j': OPT_CH(amount_data); @@ -1040,13 +1162,13 @@ option_t<report_t> * report_t::lookup_option(const char * p) OPT_(limit_); else OPT(lot_dates); else OPT(lot_prices); - else OPT(lot_tags); + else OPT_ALT(lot_notes, lot_tags); else OPT(lots); else OPT(lots_actual); else OPT_ALT(tail_, last_); break; case 'm': - OPT(market); + OPT_ALT(market, value); else OPT(monthly); else OPT(meta_); else OPT(meta_width_); @@ -1093,6 +1215,7 @@ option_t<report_t> * report_t::lookup_option(const char * p) else OPT(revalued); else OPT(revalued_only); else OPT(revalued_total_); + else OPT_ALT(rich_data, detail); break; case 's': OPT(sort_); @@ -1109,6 +1232,7 @@ option_t<report_t> * report_t::lookup_option(const char * p) else OPT(total_data); else OPT(truncate_); else OPT(total_width_); + else OPT(time_report); break; case 'u': OPT(unbudgeted); @@ -1219,12 +1343,14 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, else if (is_eq(p, "display_total")) return MAKE_FUNCTOR(report_t::fn_display_total); else if (is_eq(p, "date")) - return MAKE_FUNCTOR(report_t::fn_now); + return MAKE_FUNCTOR(report_t::fn_today); break; case 'f': if (is_eq(p, "format_date")) return MAKE_FUNCTOR(report_t::fn_format_date); + else if (is_eq(p, "format_datetime")) + return MAKE_FUNCTOR(report_t::fn_format_datetime); else if (is_eq(p, "format")) return MAKE_FUNCTOR(report_t::fn_format); else if (is_eq(p, "floor")) @@ -1262,6 +1388,8 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(fn_null); else if (is_eq(p, "now")) return MAKE_FUNCTOR(report_t::fn_now); + else if (is_eq(p, "nail_down")) + return MAKE_FUNCTOR(report_t::fn_nail_down); break; case 'o': @@ -1274,8 +1402,6 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(fn_false); else if (is_eq(p, "percent")) return MAKE_FUNCTOR(report_t::fn_percent); - else if (is_eq(p, "price")) - return MAKE_FUNCTOR(report_t::fn_price); else if (is_eq(p, "print")) return MAKE_FUNCTOR(report_t::fn_print); break; @@ -1378,79 +1504,98 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, return MAKE_OPT_HANDLER(report_t, handler); break; +#define POSTS_REPORTER(formatter) \ + WRAP_FUNCTOR(reporter<>(post_handler_ptr(formatter), *this, \ + string("#") + p)) + + // Can't use WRAP_FUNCTOR here because the template arguments + // confuse the parser +#define POSTS_REPORTER_(method, formatter) \ + expr_t::op_t::wrap_functor \ + (reporter<post_t, post_handler_ptr, method> \ + (post_handler_ptr(formatter), *this, string("#") + p)) + +#define FORMATTED_POSTS_REPORTER(format) \ + POSTS_REPORTER \ + (new format_posts \ + (*this, report_format(HANDLER(format)), \ + maybe_format(HANDLER(prepend_format_)), \ + HANDLED(prepend_width_) ? \ + lexical_cast<std::size_t>(HANDLER(prepend_width_).str()) : 0)) + +#define FORMATTED_COMMODITIES_REPORTER(format) \ + POSTS_REPORTER_ \ + (&report_t::commodities_report, \ + new format_posts \ + (*this, report_format(HANDLER(format)), \ + maybe_format(HANDLER(prepend_format_)), \ + HANDLED(prepend_width_) ? \ + lexical_cast<std::size_t>(HANDLER(prepend_width_).str()) : 0)) + +#define ACCOUNTS_REPORTER(formatter) \ + expr_t::op_t::wrap_functor(reporter<account_t, acct_handler_ptr, \ + &report_t::accounts_report> \ + (acct_handler_ptr(formatter), *this, \ + string("#") + p)) + +#define FORMATTED_ACCOUNTS_REPORTER(format) \ + ACCOUNTS_REPORTER \ + (new format_accounts \ + (*this, report_format(HANDLER(format)), \ + maybe_format(HANDLER(prepend_format_)), \ + HANDLED(prepend_width_) ? \ + lexical_cast<std::size_t>(HANDLER(prepend_width_).str()) : 0)) + case symbol_t::COMMAND: switch (*p) { case 'a': if (is_eq(p, "accounts")) { - return WRAP_FUNCTOR(reporter<>(new report_accounts(*this), *this, - "#accounts")); + return POSTS_REPORTER(new report_accounts(*this)); } break; case 'b': if (*(p + 1) == '\0' || is_eq(p, "bal") || is_eq(p, "balance")) { - return expr_t::op_t::wrap_functor - (reporter<account_t, acct_handler_ptr, &report_t::accounts_report> - (new format_accounts(*this, report_format(HANDLER(balance_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#balance")); + return FORMATTED_ACCOUNTS_REPORTER(balance_format_); } else if (is_eq(p, "budget")) { - HANDLER(amount_).set_expr(string("#budget"), "(amount, 0)"); + HANDLER(amount_).on(string("#budget"), "(amount, 0)"); budget_flags |= BUDGET_WRAP_VALUES; if (! (budget_flags & ~BUDGET_WRAP_VALUES)) budget_flags |= BUDGET_BUDGETED; - return expr_t::op_t::wrap_functor - (reporter<account_t, acct_handler_ptr, &report_t::accounts_report> - (new format_accounts(*this, report_format(HANDLER(budget_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#budget")); + return FORMATTED_ACCOUNTS_REPORTER(budget_format_); } break; case 'c': if (is_eq(p, "csv")) { - return WRAP_FUNCTOR - (reporter<> - (new format_posts(*this, report_format(HANDLER(csv_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#csv")); + return FORMATTED_POSTS_REPORTER(csv_format_); } else if (is_eq(p, "cleared")) { - HANDLER(amount_).set_expr(string("#cleared"), - "(amount, cleared ? amount : 0)"); - return expr_t::op_t::wrap_functor - (reporter<account_t, acct_handler_ptr, &report_t::accounts_report> - (new format_accounts(*this, report_format(HANDLER(cleared_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#cleared")); + HANDLER(amount_).on(string("#cleared"), + "(amount, cleared ? amount : 0)"); + return FORMATTED_ACCOUNTS_REPORTER(cleared_format_); } else if (is_eq(p, "convert")) { return WRAP_FUNCTOR(convert_command); } else if (is_eq(p, "commodities")) { - return WRAP_FUNCTOR(reporter<>(new report_commodities(*this), *this, - "#commodities")); + return POSTS_REPORTER(new report_commodities(*this)); } break; case 'e': if (is_eq(p, "equity")) { - HANDLER(generated).on_only(string("#equity")); - return WRAP_FUNCTOR(reporter<>(new print_xacts(*this), *this, "#equity")); + HANDLER(generated).on("#equity"); + return POSTS_REPORTER(new print_xacts(*this)); } else if (is_eq(p, "entry")) { return WRAP_FUNCTOR(xact_command); } else if (is_eq(p, "emacs")) { - return WRAP_FUNCTOR - (reporter<>(new format_emacs_posts(output_stream), *this, "#emacs")); + return POSTS_REPORTER(new format_emacs_posts(output_stream)); } else if (is_eq(p, "echo")) { return MAKE_FUNCTOR(report_t::echo_command); @@ -1459,51 +1604,32 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, case 'o': if (is_eq(p, "org")) { - return WRAP_FUNCTOR - (reporter<> - (new posts_to_org_table(*this, maybe_format(HANDLER(prepend_format_))), - *this, "#org")); + return POSTS_REPORTER(new posts_to_org_table + (*this, maybe_format(HANDLER(prepend_format_)))); } break; case 'p': if (*(p + 1) == '\0' || is_eq(p, "print")) { - return WRAP_FUNCTOR - (reporter<>(new print_xacts(*this, HANDLED(raw)), *this, "#print")); + return POSTS_REPORTER(new print_xacts(*this, HANDLED(raw))); } else if (is_eq(p, "prices")) { - return expr_t::op_t::wrap_functor - (reporter<post_t, post_handler_ptr, &report_t::commodities_report> - (new format_posts(*this, report_format(HANDLER(prices_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#prices")); + return FORMATTED_COMMODITIES_REPORTER(prices_format_); } - else if (is_eq(p, "pricedb")) { - return expr_t::op_t::wrap_functor - (reporter<post_t, post_handler_ptr, &report_t::commodities_report> - (new format_posts(*this, report_format(HANDLER(pricedb_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#pricedb")); + else if (is_eq(p, "pricedb") || is_eq(p, "pricesdb")) { + return FORMATTED_COMMODITIES_REPORTER(pricedb_format_); } else if (is_eq(p, "pricemap")) { return MAKE_FUNCTOR(report_t::pricemap_command); } else if (is_eq(p, "payees")) { - return WRAP_FUNCTOR(reporter<>(new report_payees(*this), *this, - "#payees")); + return POSTS_REPORTER(new report_payees(*this)); } break; case 'r': if (*(p + 1) == '\0' || is_eq(p, "reg") || is_eq(p, "register")) { - return WRAP_FUNCTOR - (reporter<> - (new format_posts(*this, report_format(HANDLER(register_format_)), - maybe_format(HANDLER(prepend_format_)), - HANDLER(prepend_width_).value.to_size_t()), - *this, "#register")); + return FORMATTED_POSTS_REPORTER(register_format_); } else if (is_eq(p, "reload")) { return MAKE_FUNCTOR(report_t::reload_command); @@ -1515,13 +1641,15 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(report_statistics); else if (is_eq(p, "source")) return WRAP_FUNCTOR(source_command); + else if (is_eq(p, "select")) + return WRAP_FUNCTOR(select_command); break; case 'x': if (is_eq(p, "xact")) return WRAP_FUNCTOR(xact_command); else if (is_eq(p, "xml")) - return WRAP_FUNCTOR(reporter<>(new format_xml(*this), *this, "#xml")); + return POSTS_REPORTER(new format_xml(*this)); break; } break; @@ -1543,11 +1671,9 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(format_command); break; case 'g': - if (is_eq(p, "generate")) { - return expr_t::op_t::wrap_functor - (reporter<post_t, post_handler_ptr, &report_t::generate_report> - (new print_xacts(*this), *this, "#generate")); - } + if (is_eq(p, "generate")) + return POSTS_REPORTER_(&report_t::generate_report, + new print_xacts(*this)); break; case 'p': if (is_eq(p, "parse")) @@ -1559,6 +1685,10 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, if (is_eq(p, "query")) return WRAP_FUNCTOR(query_command); break; + case 's': + if (is_eq(p, "script")) + return WRAP_FUNCTOR(source_command); + break; case 't': if (is_eq(p, "template")) return WRAP_FUNCTOR(template_command); diff --git a/src/report.h b/src/report.h index 5b403205..e7d68dda 100644 --- a/src/report.h +++ b/src/report.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -119,9 +119,23 @@ public: explicit report_t(session_t& _session) : session(_session), terminus(CURRENT_TIME()), - budget_flags(BUDGET_NO_BUDGET) {} + budget_flags(BUDGET_NO_BUDGET) { + TRACE_CTOR(report_t, "session_t&"); + } + report_t(const report_t& report) + : session(report.session), + output_stream(report.output_stream), + terminus(report.terminus), + budget_flags(report.budget_flags) { + TRACE_CTOR(report_t, "copy"); + } virtual ~report_t() { + TRACE_DTOR(report_t); + output_stream.close(); + } + + void quick_close() { output_stream.close(); } @@ -167,10 +181,11 @@ public: value_t fn_quoted(call_scope_t& scope); value_t fn_join(call_scope_t& scope); value_t fn_format_date(call_scope_t& scope); + value_t fn_format_datetime(call_scope_t& scope); value_t fn_ansify_if(call_scope_t& scope); value_t fn_percent(call_scope_t& scope); - value_t fn_price(call_scope_t& scope); value_t fn_commodity(call_scope_t& scope); + value_t fn_nail_down(call_scope_t& scope); value_t fn_lot_date(call_scope_t& scope); value_t fn_lot_price(call_scope_t& scope); value_t fn_lot_tag(call_scope_t& scope); @@ -215,7 +230,7 @@ public: bool lots = HANDLED(lots) || HANDLED(lots_actual); return keep_details_t(lots || HANDLED(lot_prices), lots || HANDLED(lot_dates), - lots || HANDLED(lot_tags), + lots || HANDLED(lot_notes), HANDLED(lots_actual)); } @@ -224,11 +239,12 @@ public: HANDLER(abbrev_len_).report(out); HANDLER(account_).report(out); HANDLER(actual).report(out); - HANDLER(actual_dates).report(out); HANDLER(add_budget).report(out); HANDLER(amount_).report(out); HANDLER(amount_data).report(out); HANDLER(anon).report(out); + HANDLER(auto_match).report(out); + HANDLER(aux_date).report(out); HANDLER(average).report(out); HANDLER(balance_format_).report(out); HANDLER(base).report(out); @@ -249,13 +265,13 @@ public: HANDLER(date_).report(out); HANDLER(date_format_).report(out); HANDLER(datetime_format_).report(out); + HANDLER(dc).report(out); HANDLER(depth_).report(out); HANDLER(deviation).report(out); HANDLER(display_).report(out); HANDLER(display_amount_).report(out); HANDLER(display_total_).report(out); HANDLER(dow).report(out); - HANDLER(effective).report(out); HANDLER(empty).report(out); HANDLER(end_).report(out); HANDLER(equity).report(out); @@ -272,12 +288,13 @@ public: HANDLER(group_by_).report(out); HANDLER(group_title_format_).report(out); HANDLER(head_).report(out); + HANDLER(immediate).report(out); HANDLER(inject_).report(out); HANDLER(invert).report(out); HANDLER(limit_).report(out); HANDLER(lot_dates).report(out); HANDLER(lot_prices).report(out); - HANDLER(lot_tags).report(out); + HANDLER(lot_notes).report(out); HANDLER(lots).report(out); HANDLER(lots_actual).report(out); HANDLER(market).report(out); @@ -302,6 +319,7 @@ public: HANDLER(price).report(out); HANDLER(prices_format_).report(out); HANDLER(pricedb_format_).report(out); + HANDLER(primary_date).report(out); HANDLER(quantity).report(out); HANDLER(quarterly).report(out); HANDLER(raw).report(out); @@ -312,6 +330,7 @@ public: HANDLER(revalued).report(out); HANDLER(revalued_only).report(out); HANDLER(revalued_total_).report(out); + HANDLER(rich_data).report(out); HANDLER(seed_).report(out); HANDLER(sort_).report(out); HANDLER(sort_all_).report(out); @@ -319,6 +338,7 @@ public: HANDLER(start_of_week_).report(out); HANDLER(subtotal).report(out); HANDLER(tail_).report(out); + HANDLER(time_report).report(out); HANDLER(total_).report(out); HANDLER(total_data).report(out); HANDLER(truncate_).report(out); @@ -351,252 +371,299 @@ public: * Option handlers */ - OPTION__(report_t, abbrev_len_, - CTOR(report_t, abbrev_len_) { on_with(none, 2L); }); + OPTION__ + (report_t, abbrev_len_, + CTOR(report_t, abbrev_len_) { + on(none, "2"); + }); + OPTION(report_t, account_); OPTION_(report_t, actual, DO() { // -L - parent->HANDLER(limit_).on(string("--actual"), "actual"); + OTHER(limit_).on(whence, "actual"); }); - OPTION(report_t, actual_dates); - OPTION_(report_t, add_budget, DO() { parent->budget_flags |= BUDGET_BUDGETED | BUDGET_UNBUDGETED; }); OPTION__ (report_t, amount_, // -t - expr_t expr; - CTOR(report_t, amount_) { - set_expr(none, "amount"); - } - void set_expr(const optional<string>& whence, const string& str) { - expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); + DECL1(report_t, amount_, merged_expr_t, expr, ("amount_expr", "amount")) {} + DO_(str) { + expr.append(str); }); OPTION(report_t, amount_data); // -j OPTION(report_t, anon); + OPTION(report_t, auto_match); OPTION_(report_t, average, DO() { // -A - parent->HANDLER(display_total_) - .set_expr(string("--average"), "count>0?(total_expr/count):0"); + OTHER(display_total_) + .on(whence, "count>0?(display_total/count):0"); }); - OPTION__(report_t, balance_format_, CTOR(report_t, balance_format_) { - on(none, - "%(ansify_if(" - " justify(scrub(display_total), 20, 20 + 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 ? \" \" * prepend_width : \"\")" - "--------------------\n"); - }); + OPTION__ + (report_t, balance_format_, + CTOR(report_t, balance_format_) { + on(none, + "%(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"); + }); OPTION(report_t, base); OPTION_(report_t, basis, DO() { // -B - parent->HANDLER(revalued).on_only(string("--basis")); - parent->HANDLER(amount_).set_expr(string("--basis"), "rounded(cost)"); + OTHER(revalued).on(whence); + OTHER(amount_).expr.set_base_expr("rounded(cost)"); }); - OPTION_(report_t, begin_, DO_(args) { // -b - date_interval_t interval(args.get<string>(1)); - optional<date_t> begin = interval.begin(); - if (! begin) + OPTION_(report_t, begin_, DO_(str) { // -b + date_interval_t interval(str); + if (optional<date_t> begin = interval.begin()) { + string predicate = "date>=[" + to_iso_extended_string(*begin) + "]"; + OTHER(limit_).on(whence, predicate); + } else { throw_(std::invalid_argument, - _("Could not determine beginning of period '%1'") - << args.get<string>(1)); - - string predicate = "date>=[" + to_iso_extended_string(*begin) + "]"; - parent->HANDLER(limit_).on(string("--begin"), predicate); + _("Could not determine beginning of period '%1'") << str); + } }); - OPTION__ + OPTION_ (report_t, bold_if_, expr_t expr; - CTOR(report_t, bold_if_) {} - void set_expr(const optional<string>& whence, const string& str) { + DO_(str) { expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); }); OPTION_(report_t, budget, DO() { parent->budget_flags |= BUDGET_BUDGETED; }); - OPTION__(report_t, budget_format_, CTOR(report_t, budget_format_) { - on(none, - "%(justify(scrub(get_at(total_expr, 0)), 12, -1, true, color))" - " %(justify(-scrub(get_at(total_expr, 1)), 12, " - " 12 + 1 + 12, true, color))" - " %(justify(scrub(get_at(total_expr, 1) + " - " get_at(total_expr, 0)), 12, " - " 12 + 1 + 12 + 1 + 12, true, color))" - " %(ansify_if(" - " justify((get_at(total_expr, 1) ? " - " (100% * scrub(get_at(total_expr, 0))) / " - " -scrub(get_at(total_expr, 1)) : 0), " - " 5, -1, true, false)," - " magenta if (color and get_at(total_expr, 1) and " - " (abs(quantity(scrub(get_at(total_expr, 0))) / " - " quantity(scrub(get_at(total_expr, 1)))) >= 1))))" - " %(!options.flat ? depth_spacer : \"\")" - "%-(ansify_if(partial_account(options.flat), blue if color))\n" - "%/%$1 %$2 %$3 %$4\n%/" - "%(prepend_width ? \" \" * prepend_width : \"\")" - "------------ ------------ ------------ -----\n"); - }); + OPTION__ + (report_t, budget_format_, + CTOR(report_t, budget_format_) { + on(none, + "%(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% * scrub(get_at(display_total, 0))) / " + " -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"); + }); OPTION(report_t, by_payee); // -P OPTION_(report_t, cleared, DO() { // -C - parent->HANDLER(limit_).on(string("--cleared"), "cleared"); + OTHER(limit_).on(whence, "cleared"); }); - OPTION__(report_t, cleared_format_, CTOR(report_t, cleared_format_) { - on(none, - "%(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"); - }); + OPTION__ + (report_t, cleared_format_, + CTOR(report_t, cleared_format_) { + on(none, + "%(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"); + }); OPTION(report_t, color); OPTION_(report_t, collapse, DO() { // -n // Make sure that balance reports are collapsed too, but only apply it // to account xacts - parent->HANDLER(display_).on(string("--collapse"), "post|depth<=1"); + OTHER(display_).on(whence, "post|depth<=1"); }); OPTION_(report_t, collapse_if_zero, DO() { - parent->HANDLER(collapse).on_only(string("--collapse-if-zero")); + OTHER(collapse).on(whence); }); OPTION(report_t, columns_); OPTION(report_t, count); - OPTION__(report_t, csv_format_, CTOR(report_t, csv_format_) { - on(none, - "%(quoted(date))," - "%(quoted(code))," - "%(quoted(payee))," - "%(quoted(display_account))," - "%(quoted(commodity))," - "%(quoted(quantity(scrub(display_amount))))," - "%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\")))," - "%(quoted(join(note | xact.note)))\n"); - }); + OPTION__ + (report_t, csv_format_, + CTOR(report_t, csv_format_) { + on(none, + "%(quoted(date))," + "%(quoted(code))," + "%(quoted(payee))," + "%(quoted(display_account))," + "%(quoted(commodity))," + "%(quoted(quantity(scrub(display_amount))))," + "%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\")))," + "%(quoted(join(note | xact.note)))\n"); + }); OPTION_(report_t, current, DO() { // -c - parent->HANDLER(limit_).on(string("--current"), "date<=today"); + OTHER(limit_).on(whence, "date<=today"); }); OPTION_(report_t, daily, DO() { // -D - parent->HANDLER(period_).on(string("--daily"), "daily"); + OTHER(period_).on(whence, "daily"); }); OPTION(report_t, date_); OPTION(report_t, date_format_); OPTION(report_t, datetime_format_); - OPTION_(report_t, depth_, DO_(args) { - parent->HANDLER(display_) - .on(string("--depth"), string("depth<=") + args.get<string>(1)); + OPTION_(report_t, dc, DO() { + OTHER(amount_).expr.set_base_expr + ("(amount > 0 ? amount : 0, amount < 0 ? amount : 0)"); + + OTHER(register_format_) + .on(none, + "%(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(abs(get_at(display_amount, 0))), 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(abs(get_at(display_amount, 1))), int(amount_width), " + " 4 + int(meta_width) + int(date_width) + int(payee_width)" + " + int(account_width) + int(amount_width) + int(amount_width) + int(prepend_width)," + " true, color)," + " bold if should_bold))" + " %(ansify_if(" + " justify(scrub(get_at(display_total, 0) + get_at(display_total, 1)), int(total_width), " + " 5 + int(meta_width) + int(date_width) + int(payee_width)" + " + int(account_width) + int(amount_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 %$6\n"); + + OTHER(balance_format_) + .on(none, + "%(ansify_if(" + " justify(scrub(abs(get_at(display_total, 0))), 14," + " 14 + int(prepend_width), true, color)," + " bold if should_bold)) " + "%(ansify_if(" + " justify(scrub(abs(get_at(display_total, 1))), 14," + " 14 + 1 + int(prepend_width) + int(total_width), true, color)," + " bold if should_bold)) " + "%(ansify_if(" + " justify(scrub(get_at(display_total, 0) + get_at(display_total, 1)), 14," + " 14 + 2 + int(prepend_width) + int(total_width) + int(total_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 %$2 %$3\n%/" + "%(prepend_width ? \" \" * int(prepend_width) : \"\")" + "--------------------------------------------\n"); + }); + + OPTION_(report_t, depth_, DO_(str) { + OTHER(display_).on(whence, string("depth<=") + str); }); OPTION_(report_t, deviation, DO() { - parent->HANDLER(display_total_) - .set_expr(string("--deviation"), "amount_expr-total_expr/count"); + OTHER(display_total_) + .on(whence, "display_amount-display_total"); }); - OPTION__ - (report_t, display_, // -d - CTOR(report_t, display_) {} - virtual void on_with(const optional<string>& whence, const value_t& text) { - if (! handled) - option_t<report_t>::on_with(whence, text); - else - option_t<report_t>::on_with(whence, - string_value(string("(") + str() + ")&(" + - text.as_string() + ")")); + OPTION_ + (report_t, display_, + DO_(str) { // -d + if (handled) + value = string("(") + value + ")&(" + str + ")"; }); OPTION__ (report_t, display_amount_, - expr_t expr; - CTOR(report_t, display_amount_) { - set_expr(none, "amount_expr"); - } - void set_expr(const optional<string>& whence, const string& str) { - expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); + DECL1(report_t, display_amount_, merged_expr_t, expr, + ("display_amount", "amount_expr")) {} + DO_(str) { + expr.append(str); }); OPTION__ (report_t, display_total_, - expr_t expr; - CTOR(report_t, display_total_) { - set_expr(none, "total_expr"); - } - void set_expr(const optional<string>& whence, const string& str) { - expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); + DECL1(report_t, display_total_, merged_expr_t, expr, + ("display_total", "total_expr")) {} + DO_(str) { + expr.append(str); }); OPTION(report_t, dow); - OPTION(report_t, effective); + OPTION(report_t, aux_date); OPTION(report_t, empty); // -E - OPTION_(report_t, end_, DO_(args) { // -e - date_interval_t interval(args.get<string>(1)); + OPTION_(report_t, end_, DO_(str) { // -e // Use begin() here so that if the user says --end=2008, we end on // 2008/01/01 instead of 2009/01/01 (which is what end() would // return). - optional<date_t> end = interval.begin(); - if (! end) + date_interval_t interval(str); + if (optional<date_t> end = interval.begin()) { + string predicate = "date<[" + to_iso_extended_string(*end) + "]"; + OTHER(limit_).on(whence, predicate); + + parent->terminus = datetime_t(*end); + } else { throw_(std::invalid_argument, _("Could not determine end of period '%1'") - << args.get<string>(1)); - - string predicate = "date<[" + to_iso_extended_string(*end) + "]"; - parent->HANDLER(limit_).on(string("--end"), predicate); - - parent->terminus = datetime_t(*end); + << str); + } }); OPTION(report_t, equity); OPTION(report_t, exact); - OPTION_(report_t, exchange_, DO_(args) { // -X - on_with(args.get<string>(0), args[1]); - call_scope_t no_args(*parent); - no_args.push_back(args[0]); - parent->HANDLER(market).parent = parent; - parent->HANDLER(market).handler(no_args); + OPTION_(report_t, exchange_, DO_() { // -X + // Using -X implies -V. The main difference is that now + // HANDLER(exchange_) contains the name of a commodity, which + // is accessed via the "exchange" value expression function. + OTHER(market).on(whence); }); OPTION(report_t, flat); @@ -607,115 +674,112 @@ public: OPTION(report_t, format_); // -F OPTION_(report_t, gain, DO() { // -G - parent->HANDLER(revalued).on_only(string("--gain")); - parent->HANDLER(amount_).set_expr(string("--gain"), "(amount, cost)"); + OTHER(revalued).on(whence); + OTHER(amount_).expr.set_base_expr("(amount, cost)"); + // Since we are displaying the amounts of revalued postings, they // will end up being composite totals, and hence a pair of pairs. - parent->HANDLER(display_amount_) - .set_expr(string("--gain"), - "use_direct_amount ? amount :" - " (is_seq(get_at(amount_expr, 0)) ?" - " get_at(get_at(amount_expr, 0), 0) :" - " market(get_at(amount_expr, 0), value_date, exchange)" - " - get_at(amount_expr, 1))"); - parent->HANDLER(revalued_total_) - .set_expr(string("--gain"), - "(market(get_at(total_expr, 0), value_date, exchange), " - "get_at(total_expr, 1))"); - parent->HANDLER(display_total_) - .set_expr(string("--gain"), - "use_direct_amount ? total_expr :" - " market(get_at(total_expr, 0), value_date, exchange)" - " - get_at(total_expr, 1)"); + OTHER(display_amount_) + .on(whence, + "use_direct_amount ? amount :" + " (is_seq(get_at(amount_expr, 0)) ?" + " get_at(get_at(amount_expr, 0), 0) :" + " market(get_at(amount_expr, 0), value_date, exchange)" + " - get_at(amount_expr, 1))"); + OTHER(revalued_total_) + .on(whence, + "(market(get_at(total_expr, 0), value_date, exchange), " + "get_at(total_expr, 1))"); + OTHER(display_total_) + .on(whence, + "use_direct_amount ? total_expr :" + " market(get_at(total_expr, 0), value_date, exchange)" + " - get_at(total_expr, 1)"); }); OPTION(report_t, generated); - OPTION__ + OPTION_ (report_t, group_by_, expr_t expr; - CTOR(report_t, group_by_) {} - void set_expr(const optional<string>& whence, const string& str) { + DO_(str) { expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); }); - OPTION__(report_t, group_title_format_, CTOR(report_t, group_title_format_) { - on(none, "%(value)\n"); - }); + OPTION__ + (report_t, group_title_format_, + CTOR(report_t, group_title_format_) { + on(none, "%(value)\n"); + }); OPTION(report_t, head_); + + OPTION_(report_t, historical, DO() { // -H + OTHER(market).on(whence); + OTHER(amount_) + .on(whence, "nail_down(amount_expr, " + "market(amount_expr, value_date, exchange))"); + }); + + OPTION(report_t, immediate); OPTION(report_t, inject_); OPTION_(report_t, invert, DO() { - parent->HANDLER(amount_).set_expr(string("--invert"), "-amount"); + OTHER(amount_).on(whence, "-amount_expr"); }); - OPTION__ - (report_t, limit_, // -l - CTOR(report_t, limit_) {} - virtual void on_with(const optional<string>& whence, const value_t& text) { - if (! handled) - option_t<report_t>::on_with(whence, text); - else - option_t<report_t>::on_with(whence, - string_value(string("(") + str() + ")&(" + - text.as_string() + ")")); + OPTION_ + (report_t, limit_, + DO_(str) { // -l + if (handled) + value = string("(") + value + ")&(" + str + ")"; }); OPTION(report_t, lot_dates); OPTION(report_t, lot_prices); - OPTION(report_t, lot_tags); + OPTION(report_t, lot_notes); OPTION(report_t, lots); OPTION(report_t, lots_actual); OPTION_(report_t, market, DO() { // -V - parent->HANDLER(revalued).on_only(string("--market")); - parent->HANDLER(display_amount_) - .set_expr(string("--market"), - "market(amount_expr, value_date, exchange)"); - parent->HANDLER(display_total_) - .set_expr(string("--market"), - "market(total_expr, value_date, exchange)"); + OTHER(revalued).on(whence); + + OTHER(display_amount_) + .on(whence, "market(display_amount, value_date, exchange)"); + OTHER(display_total_) + .on(whence, "market(display_total, value_date, exchange)"); }); OPTION(report_t, meta_); OPTION_(report_t, monthly, DO() { // -M - parent->HANDLER(period_).on(string("--monthly"), "monthly"); + OTHER(period_).on(whence, "monthly"); }); OPTION_(report_t, no_color, DO() { - parent->HANDLER(color).off(); + OTHER(color).off(); }); OPTION(report_t, no_rounding); OPTION(report_t, no_titles); OPTION(report_t, no_total); - OPTION_(report_t, now_, DO_(args) { - date_interval_t interval(args.get<string>(1)); - optional<date_t> begin = interval.begin(); - if (! begin) + OPTION_(report_t, now_, DO_(str) { + date_interval_t interval(str); + if (optional<date_t> begin = interval.begin()) { + ledger::epoch = parent->terminus = datetime_t(*begin); + } else { throw_(std::invalid_argument, _("Could not determine beginning of period '%1'") - << args.get<string>(1)); - ledger::epoch = parent->terminus = datetime_t(*begin); + << str); + } }); - OPTION__ + OPTION_ (report_t, only_, - CTOR(report_t, only_) {} - virtual void on_with(const optional<string>& whence, const value_t& text) { - if (! handled) - option_t<report_t>::on_with(whence, text); - else - option_t<report_t>::on_with(whence, - string_value(string("(") + str() + ")&(" + - text.as_string() + ")")); + DO_(str) { + if (handled) + value = string("(") + value + ")&(" + str + ")"; }); OPTION(report_t, output_); // -o @@ -736,201 +800,197 @@ public: setenv("LESS", "-FRSX", 0); // don't overwrite } } - } - virtual void on_with(const optional<string>& whence, const value_t& text) { - string cmd(text.to_string()); - if (cmd == "" || cmd == "false" || cmd == "off" || - cmd == "none" || cmd == "no" || cmd == "disable") - option_t<report_t>::off(); - else - option_t<report_t>::on_with(whence, text); }); #else // HAVE_ISATTY - OPTION__ - (report_t, pager_, - CTOR(report_t, pager_) { - } - virtual void on_with(const optional<string>& whence, const value_t& text) { - string cmd(text.to_string()); - if (cmd == "" || cmd == "false" || cmd == "off" || - cmd == "none" || cmd == "no" || cmd == "disable") - option_t<report_t>::off(); - else - option_t<report_t>::on_with(whence, text); - }); + OPTION(report_t, pager_); #endif // HAVE_ISATTY + OPTION_(report_t, no_pager, DO() { + OTHER(pager_).off(); + }); + OPTION(report_t, payee_); OPTION_(report_t, pending, DO() { // -C - parent->HANDLER(limit_).on(string("--pending"), "pending"); + OTHER(limit_).on(whence, "pending"); }); OPTION_(report_t, percent, DO() { // -% - parent->HANDLER(total_) - .set_expr(string("--percent"), - "((is_account&parent&parent.total)?" - " percent(scrub(total), scrub(parent.total)):0)"); + OTHER(total_) + .on(whence, + "((is_account&parent&parent.total)?" + " percent(scrub(total), scrub(parent.total)):0)"); }); - OPTION__ - (report_t, period_, // -p - CTOR(report_t, period_) {} - virtual void on_with(const optional<string>& whence, const value_t& text) { - if (! handled) - option_t<report_t>::on_with(whence, text); - else - option_t<report_t>::on_with(whence, - string_value(text.as_string() + " " + str())); + OPTION_ + (report_t, period_, + DO_(str) { // -p + if (handled) + value += string(" ") + str; }); OPTION(report_t, pivot_); - OPTION__(report_t, plot_amount_format_, CTOR(report_t, plot_amount_format_) { - on(none, - "%(format_date(date, \"%Y-%m-%d\")) %(quantity(scrub(display_amount)))\n"); - }); + OPTION__ + (report_t, plot_amount_format_, + CTOR(report_t, plot_amount_format_) { + on(none, + "%(format_date(date, \"%Y-%m-%d\")) %(quantity(scrub(display_amount)))\n"); + }); - OPTION__(report_t, plot_total_format_, CTOR(report_t, plot_total_format_) { - on(none, - "%(format_date(date, \"%Y-%m-%d\")) %(quantity(scrub(display_total)))\n"); - }); + OPTION__ + (report_t, plot_total_format_, + CTOR(report_t, plot_total_format_) { + on(none, + "%(format_date(date, \"%Y-%m-%d\")) %(quantity(scrub(display_total)))\n"); + }); OPTION(report_t, prepend_format_); - OPTION_(report_t, prepend_width_, DO_(args) { - value = args.get<long>(1); - }); + OPTION(report_t, prepend_width_); OPTION_(report_t, price, DO() { // -I - parent->HANDLER(display_amount_) - .set_expr(string("--price"), "price(amount_expr)"); - parent->HANDLER(display_total_) - .set_expr(string("--price"), "price(total_expr)"); + OTHER(amount_).expr.set_base_expr("price"); }); - OPTION__(report_t, prices_format_, CTOR(report_t, prices_format_) { - on(none, - "%(date) %-8(display_account) %(justify(scrub(display_amount), 12, " - " 2 + 9 + 8 + 12, true, color))\n"); - }); + OPTION__ + (report_t, prices_format_, + CTOR(report_t, prices_format_) { + on(none, + "%(date) %-8(display_account) %(justify(scrub(display_amount), 12, " + " 2 + 9 + 8 + 12, true, color))\n"); + }); - OPTION__(report_t, pricedb_format_, CTOR(report_t, pricedb_format_) { - on(none, - "P %(datetime) %(display_account) %(scrub(display_amount))\n"); - }); + OPTION__ + (report_t, pricedb_format_, + CTOR(report_t, pricedb_format_) { + on(none, + "P %(datetime) %(display_account) %(scrub(display_amount))\n"); + }); + + OPTION(report_t, primary_date); OPTION_(report_t, quantity, DO() { // -O - parent->HANDLER(revalued).off(); - parent->HANDLER(amount_).set_expr(string("--quantity"), "amount"); - parent->HANDLER(total_).set_expr(string("--quantity"), "total"); + OTHER(revalued).off(); + + OTHER(amount_).expr.set_base_expr("amount"); + OTHER(total_).expr.set_base_expr("total"); }); OPTION_(report_t, quarterly, DO() { - parent->HANDLER(period_).on(string("--quarterly"), "quarterly"); + OTHER(period_).on(whence, "quarterly"); }); OPTION(report_t, raw); OPTION_(report_t, real, DO() { // -R - parent->HANDLER(limit_).on(string("--real"), "real"); + OTHER(limit_).on(whence, "real"); }); - OPTION__(report_t, register_format_, CTOR(report_t, register_format_) { - on(none, - "%(ansify_if(" - " ansify_if(justify(format_date(date), date_width)," - " green if color and date > today)," - " bold if should_bold))" - " %(ansify_if(" - " ansify_if(justify(truncated(payee, payee_width), payee_width), " - " bold if color and !cleared and actual)," - " bold if should_bold))" - " %(ansify_if(" - " ansify_if(justify(truncated(display_account, account_width, " - " abbrev_len), account_width)," - " blue if color)," - " bold if should_bold))" - " %(ansify_if(" - " justify(scrub(display_amount), amount_width, " - " 3 + meta_width + date_width + payee_width" - " + account_width + amount_width + prepend_width," - " true, color)," - " bold if should_bold))" - " %(ansify_if(" - " justify(scrub(display_total), total_width, " - " 4 + meta_width + date_width + payee_width" - " + account_width + amount_width + total_width" - " + prepend_width, true, color)," - " bold if should_bold))\n%/" - "%(justify(\" \", date_width))" - " %(ansify_if(" - " justify(truncated(has_tag(\"Payee\") ? payee : \" \", " - " payee_width), payee_width)," - " bold if should_bold))" - " %$3 %$4 %$5\n"); - }); + OPTION__ + (report_t, register_format_, + CTOR(report_t, register_format_) { + on(none, + "%(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"); + }); OPTION(report_t, related); // -r OPTION_(report_t, related_all, DO() { - parent->HANDLER(related).on_only(string("--related-all")); + OTHER(related).on(whence); }); OPTION(report_t, revalued); OPTION(report_t, revalued_only); - OPTION__ + OPTION_ (report_t, revalued_total_, expr_t expr; - CTOR(report_t, revalued_total_) {} - void set_expr(const optional<string>& whence, const string& str) { + DO_(str) { expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); }); + OPTION(report_t, rich_data); + OPTION(report_t, seed_); - OPTION_(report_t, sort_, DO_(args) { // -S - on_with(args.get<string>(0), args[1]); - parent->HANDLER(sort_xacts_).off(); - parent->HANDLER(sort_all_).off(); + OPTION_(report_t, sort_, DO_(str) { // -S + OTHER(sort_xacts_).off(); + OTHER(sort_all_).off(); }); - OPTION_(report_t, sort_all_, DO_(args) { - parent->HANDLER(sort_).on_with(string("--sort-all"), args[1]); - parent->HANDLER(sort_xacts_).off(); + OPTION_(report_t, sort_all_, DO_(str) { + OTHER(sort_).on(whence, str); + OTHER(sort_xacts_).off(); }); - OPTION_(report_t, sort_xacts_, DO_(args) { - parent->HANDLER(sort_).on_with(string("--sort-xacts"), args[1]); - parent->HANDLER(sort_all_).off(); + OPTION_(report_t, sort_xacts_, DO_(str) { + OTHER(sort_).on(whence, str); + OTHER(sort_all_).off(); }); OPTION(report_t, start_of_week_); OPTION(report_t, subtotal); // -s OPTION(report_t, tail_); + OPTION_(report_t, time_report, DO() { + OTHER(balance_format_) + .on(none, + "%(justify(earliest_checkin ? " + " format_datetime(earliest_checkin) : \"\", 19, -1, true)) " + "%(justify(latest_checkout ? " + " format_datetime(latest_checkout) : \"\", 19, -1, true)) " + "%(ansify_if(" + " justify(scrub(display_total), 8," + " 8 + 4 + 19 * 2, 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 %$2 %$3\n%/" + "%(prepend_width ? \" \" * int(prepend_width) : \"\")" + "--------------------------------------------------\n"); + }); + OPTION__ (report_t, total_, // -T - expr_t expr; - CTOR(report_t, total_) { - set_expr(none, "total"); - } - void set_expr(const optional<string>& whence, const string& str) { - expr = str; - on(whence, str); - } - DO_(args) { - set_expr(args.get<string>(0), args.get<string>(1)); + DECL1(report_t, total_, merged_expr_t, expr, ("total_expr", "total")) {} + DO_(str) { + expr.append(str); }); OPTION(report_t, total_data); // -J - OPTION_(report_t, truncate_, DO_(args) { - string style(args.get<string>(1)); + OPTION_(report_t, truncate_, DO_(style) { if (style == "leading") format_t::default_style = format_t::TRUNCATE_LEADING; else if (style == "middle") @@ -948,7 +1008,7 @@ public: }); OPTION_(report_t, uncleared, DO() { // -U - parent->HANDLER(limit_).on(string("--uncleared"), "uncleared|pending"); + OTHER(limit_).on(whence, "uncleared|pending"); }); OPTION(report_t, unrealized); @@ -957,51 +1017,30 @@ public: OPTION(report_t, unrealized_losses_); OPTION_(report_t, unround, DO() { - parent->HANDLER(display_amount_) - .set_expr(string("--unround"), "unrounded(amount_expr)"); - parent->HANDLER(display_total_) - .set_expr(string("--unround"), "unrounded(total_expr)"); + OTHER(amount_).on(whence, "unrounded(amount_expr)"); + OTHER(total_).on(whence, "unrounded(total_expr)"); }); OPTION_(report_t, weekly, DO() { // -W - parent->HANDLER(period_).on(string("--weekly"), "weekly"); + OTHER(period_).on(whence, "weekly"); }); OPTION_(report_t, wide, DO() { // -w - parent->HANDLER(columns_).on_with(string("--wide"), 132L); + OTHER(columns_).on(whence, "132"); }); OPTION_(report_t, yearly, DO() { // -Y - parent->HANDLER(period_).on(string("--yearly"), "yearly"); + OTHER(period_).on(whence, "yearly"); }); - OPTION__(report_t, meta_width_, - bool specified; - CTOR(report_t, meta_width_) { specified = false; } - DO_(args) { value = args.get<long>(1); specified = true; }); - OPTION__(report_t, date_width_, - bool specified; - CTOR(report_t, date_width_) { specified = false; } - DO_(args) { value = args.get<long>(1); specified = true; }); - OPTION__(report_t, payee_width_, - bool specified; - CTOR(report_t, payee_width_) { specified = false; } - DO_(args) { value = args.get<long>(1); specified = true; }); - OPTION__(report_t, account_width_, - bool specified; - CTOR(report_t, account_width_) { specified = false; } - DO_(args) { value = args.get<long>(1); specified = true; }); - OPTION__(report_t, amount_width_, - bool specified; - CTOR(report_t, amount_width_) { specified = false; } - DO_(args) { value = args.get<long>(1); specified = true; }); - OPTION__(report_t, total_width_, - bool specified; - CTOR(report_t, total_width_) { specified = false; } - DO_(args) { value = args.get<long>(1); specified = true; }); + OPTION(report_t, meta_width_); + OPTION(report_t, date_width_); + OPTION(report_t, payee_width_); + OPTION(report_t, account_width_); + OPTION(report_t, amount_width_); + OPTION(report_t, total_width_); }; - template <class Type = post_t, class handler_ptr = post_handler_ptr, void (report_t::*report_method)(handler_ptr) = @@ -1014,9 +1053,18 @@ class reporter string whence; public: - reporter(item_handler<Type> * _handler, + reporter(shared_ptr<item_handler<Type> > _handler, report_t& _report, const string& _whence) - : handler(_handler), report(_report), whence(_whence) {} + : handler(_handler), report(_report), whence(_whence) { + TRACE_CTOR(reporter, "item_handler<Type>, report_t&, string"); + } + reporter(const reporter& other) + : handler(other.handler), report(other.report), whence(other.whence) { + TRACE_CTOR(reporter, "copy"); + } + ~reporter() throw() { + TRACE_DTOR(reporter); + } value_t operator()(call_scope_t& args) { diff --git a/src/scope.cc b/src/scope.cc index e18b5a0a..00327159 100644 --- a/src/scope.cc +++ b/src/scope.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -35,12 +35,14 @@ namespace ledger { -scope_t * scope_t::default_scope = NULL; +scope_t * scope_t::default_scope = NULL; +empty_scope_t * scope_t::empty_scope = NULL; void symbol_scope_t::define(const symbol_t::kind_t kind, const string& name, expr_t::ptr_op_t def) { - DEBUG("scope.symbols", "Defining '" << name << "' = " << def); + DEBUG("scope.symbols", + "Defining '" << name << "' = " << def << " in " << this); if (! symbols) symbols = symbol_map(); @@ -52,8 +54,8 @@ void symbol_scope_t::define(const symbol_t::kind_t kind, assert(i != symbols->end()); symbols->erase(i); - result = symbols->insert(symbol_map::value_type(symbol_t(kind, name, def), - def)); + result = symbols->insert(symbol_map::value_type + (symbol_t(kind, name, def), def)); if (! result.second) throw_(compile_error, _("Redefinition of '%1' in the same scope") << name); @@ -64,9 +66,12 @@ expr_t::ptr_op_t symbol_scope_t::lookup(const symbol_t::kind_t kind, const string& name) { if (symbols) { + DEBUG("scope.symbols", "Looking for '" << name << "' in " << this); symbol_map::const_iterator i = symbols->find(symbol_t(kind, name)); - if (i != symbols->end()) + if (i != symbols->end()) { + DEBUG("scope.symbols", "Found '" << name << "' in " << this); return (*i).second; + } } return child_scope_t::lookup(kind, name); } @@ -84,8 +89,7 @@ value_t& call_scope_t::resolve(const std::size_t index, value = as_expr(value)->calc(scope, locus, depth); if (required && ! value.is_type(context)) throw_(calc_error, _("Expected %1 for argument %2, but received %3") - << value.label(context) << index - << value.label()); + << value.label(context) << index << value.label()); } return value; } diff --git a/src/scope.h b/src/scope.h index a7b3c5cb..c43d73d6 100644 --- a/src/scope.h +++ b/src/scope.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -70,8 +70,7 @@ struct symbol_t TRACE_CTOR(symbol_t, "symbol_t::kind_t, string"); } symbol_t(const symbol_t& sym) - : kind(sym.kind), name(sym.name), - definition(sym.definition) { + : kind(sym.kind), name(sym.name), definition(sym.definition) { TRACE_CTOR(symbol_t, "copy"); } ~symbol_t() throw() { @@ -81,6 +80,9 @@ struct symbol_t bool operator<(const symbol_t& sym) const { return kind < sym.kind || name < sym.name; } + bool operator==(const symbol_t& sym) const { + return kind == sym.kind || name == sym.name; + } #if defined(HAVE_BOOST_SERIALIZATION) private: @@ -97,10 +99,13 @@ private: #endif // HAVE_BOOST_SERIALIZATION }; +class empty_scope_t; + class scope_t { public: - static scope_t * default_scope; + static scope_t * default_scope; + static empty_scope_t * empty_scope; explicit scope_t() { TRACE_CTOR(scope_t, ""); @@ -134,6 +139,24 @@ private: #endif // HAVE_BOOST_SERIALIZATION }; +class empty_scope_t : public scope_t +{ +public: + empty_scope_t() { + TRACE_CTOR(empty_scope_t, ""); + } + ~empty_scope_t() throw() { + TRACE_DTOR(empty_scope_t); + } + + virtual string description() { + return _("<empty>"); + } + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t, const string&) { + return NULL; + } +}; + class child_scope_t : public noncopyable, public scope_t { public: @@ -142,8 +165,7 @@ public: explicit child_scope_t() : parent(NULL) { TRACE_CTOR(child_scope_t, ""); } - explicit child_scope_t(scope_t& _parent) - : parent(&_parent) { + explicit child_scope_t(scope_t& _parent) : parent(&_parent) { TRACE_CTOR(child_scope_t, "scope_t&"); } virtual ~child_scope_t() { @@ -187,6 +209,8 @@ public: explicit bind_scope_t(scope_t& _parent, scope_t& _grandchild) : child_scope_t(_parent), grandchild(_grandchild) { + DEBUG("scope.symbols", + "Binding scope " << &_parent << " with " << &_grandchild); TRACE_CTOR(bind_scope_t, "scope_t&, scope_t&"); } virtual ~bind_scope_t() { @@ -225,15 +249,19 @@ private: }; template <typename T> -T * search_scope(scope_t * ptr) +T * search_scope(scope_t * ptr, bool prefer_direct_parents = false) { + DEBUG("scope.search", "Searching scope " << ptr->description()); + if (T * sought = dynamic_cast<T *>(ptr)) return sought; if (bind_scope_t * scope = dynamic_cast<bind_scope_t *>(ptr)) { - if (T * sought = search_scope<T>(&scope->grandchild)) + if (T * sought = search_scope<T>(prefer_direct_parents ? + scope->parent : &scope->grandchild)) return sought; - return search_scope<T>(scope->parent); + return search_scope<T>(prefer_direct_parents ? + &scope->grandchild : scope->parent); } else if (child_scope_t * child_scope = dynamic_cast<child_scope_t *>(ptr)) { return search_scope<T>(child_scope->parent); @@ -242,9 +270,21 @@ T * search_scope(scope_t * ptr) } template <typename T> -inline T& find_scope(child_scope_t& scope, bool skip_this = true) +inline T& find_scope(child_scope_t& scope, bool skip_this = true, + bool prefer_direct_parents = false) +{ + if (T * sought = search_scope<T>(skip_this ? scope.parent : &scope, + prefer_direct_parents)) + return *sought; + + throw_(std::runtime_error, _("Could not find scope")); + return reinterpret_cast<T&>(scope); // never executed +} + +template <typename T> +inline T& find_scope(scope_t& scope, bool prefer_direct_parents = false) { - if (T * sought = search_scope<T>(skip_this ? scope.parent : &scope)) + if (T * sought = search_scope<T>(&scope, prefer_direct_parents)) return *sought; throw_(std::runtime_error, _("Could not find scope")); @@ -258,7 +298,7 @@ class symbol_scope_t : public child_scope_t optional<symbol_map> symbols; public: - explicit symbol_scope_t() { + explicit symbol_scope_t() : child_scope_t() { TRACE_CTOR(symbol_scope_t, ""); } explicit symbol_scope_t(scope_t& _parent) : child_scope_t(_parent) { @@ -347,13 +387,8 @@ protected: class call_scope_t : public context_scope_t { -#if defined(DEBUG_ON) public: -#endif value_t args; -#if defined(DEBUG_ON) -private: -#endif mutable void * ptr; value_t& resolve(const std::size_t index, @@ -370,17 +405,12 @@ public: : context_scope_t(_parent, _parent.type_context(), _parent.type_required()), ptr(NULL), locus(_locus), depth(_depth) { - TRACE_CTOR(call_scope_t, - "scope_t&, value_t::type_t, bool, expr_t::ptr_op_t *, int"); + TRACE_CTOR(call_scope_t, "scope_t&, expr_t::ptr_op_t *, const int"); } virtual ~call_scope_t() { TRACE_DTOR(call_scope_t); } - virtual string description() { - return context_scope_t::description(); - } - void set_args(const value_t& _args) { args = _args; } @@ -453,7 +483,7 @@ public: #if defined(HAVE_BOOST_SERIALIZATION) protected: - explicit call_scope_t() { + explicit call_scope_t() : depth(0) { TRACE_CTOR(call_scope_t, ""); } @@ -635,6 +665,21 @@ call_scope_t::get<expr_t::ptr_op_t>(std::size_t index, bool) { return args[index].as_any<expr_t::ptr_op_t>(); } +inline string join_args(call_scope_t& args) { + std::ostringstream buf; + bool first = true; + + for (std::size_t i = 0; i < args.size(); i++) { + if (first) + first = false; + else + buf << ' '; + buf << args[i]; + } + + return buf.str(); +} + class value_scope_t : public child_scope_t { value_t value; @@ -645,7 +690,12 @@ class value_scope_t : public child_scope_t public: value_scope_t(scope_t& _parent, const value_t& _value) - : child_scope_t(_parent), value(_value) {} + : child_scope_t(_parent), value(_value) { + TRACE_CTOR(value_scope_t, "scope_t&, value_t"); + } + ~value_scope_t() throw() { + TRACE_DTOR(value_scope_t); + } virtual string description() { return parent->description(); @@ -660,7 +710,7 @@ public: if (name == "value") return MAKE_FUNCTOR(value_scope_t::get_value); - return NULL; + return child_scope_t::lookup(kind, name); } }; diff --git a/src/select.cc b/src/select.cc new file mode 100644 index 00000000..56bd3f2d --- /dev/null +++ b/src/select.cc @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2003-2012, 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 <system.hh> + +#include "select.h" +#include "journal.h" +#include "account.h" +#include "report.h" +#include "output.h" +#include "print.h" +#include "chain.h" +#include "filters.h" +#include "scope.h" +#include "op.h" + +namespace ledger { + +namespace { + bool get_principal_identifiers(expr_t::ptr_op_t expr, string& ident, + bool do_transforms = false) + { + bool result = true; + + if (expr->is_ident()) { + string name(expr->as_ident()); + if (name == "date" || name == "aux_date" || name == "payee") { + if (! ident.empty() && + ! (name == "date" || name == "aux_date" || name == "payee")) + result = false; + ident = name; + } + else if (name == "account") { + if (! ident.empty() && ! (name == "account")) + result = false; + ident = name; + if (do_transforms) + expr->set_ident("display_account"); + } + else if (name == "amount") { + if (! ident.empty() && ! (name == "amount")) + result = false; + ident = name; + if (do_transforms) + expr->set_ident("display_amount"); + } + else if (name == "total") { + if (! ident.empty() && ! (name == "total")) + result = false; + ident = name; + if (do_transforms) + expr->set_ident("display_total"); + } + } + + if (expr->kind > expr_t::op_t::TERMINALS || expr->is_scope()) { + if (expr->left()) { + if (! get_principal_identifiers(expr->left(), ident, do_transforms)) + result = false; + if (expr->kind > expr_t::op_t::UNARY_OPERATORS && expr->has_right()) + if (! get_principal_identifiers(expr->right(), ident, do_transforms)) + result = false; + } + } + + return result; + } +} + +value_t select_command(call_scope_t& args) +{ + string text = "select " + join_args(args); + if (text.empty()) + throw std::logic_error(_("Usage: select TEXT")); + + report_t& report(find_scope<report_t>(args)); + + // Our first step is to divide the select statement into its principal + // parts: + // + // SELECT <VALEXPR-LIST> + // FROM <NAME> + // WHERE <VALEXPR> + // DISPLAY <VALEXPR> + // COLLECT <VALEXPR> + // GROUP BY <VALEXPR> + // STYLE <NAME> + + boost::regex select_re + ("(select|from|where|display|collect|group\\s+by|style)\\s+" + "(.+?)" + "(?=(\\s+(from|where|display|collect|group\\s+by|style)\\s+|$))", + boost::regex::perl | boost::regex::icase); + + boost::regex from_accounts_re("from\\s+accounts\\>"); + bool accounts_report = boost::regex_search(text, from_accounts_re); + + boost::sregex_iterator m1(text.begin(), text.end(), select_re); + boost::sregex_iterator m2; + + expr_t::ptr_op_t report_functor; + std::ostringstream formatter; + + while (m1 != m2) { + const boost::match_results<string::const_iterator>& match(*m1); + + string keyword(match[1]); + string arg(match[2]); + + DEBUG("select.parse", "keyword: " << keyword); + DEBUG("select.parse", "arg: " << arg); + + if (keyword == "select") { + expr_t args_expr(arg); + value_t columns(split_cons_expr(args_expr.get_op())); + bool first = true; + string thus_far = ""; + + std::size_t cols = 0; + if (report.HANDLED(columns_)) + cols = lexical_cast<std::size_t>(report.HANDLER(columns_).value); + else if (const char * columns_env = std::getenv("COLUMNS")) + cols = lexical_cast<std::size_t>(columns_env); + else + cols = 80; + + std::size_t date_width = + (report.HANDLED(date_width_) ? + lexical_cast<std::size_t>(report.HANDLER(date_width_).str()) : + static_cast<std::size_t> + (format_date(CURRENT_DATE(),FMT_PRINTED).length())); + std::size_t payee_width = + (report.HANDLED(payee_width_) ? + lexical_cast<std::size_t>(report.HANDLER(payee_width_).str()) : + std::size_t(double(cols) * 0.263157)); + std::size_t account_width = + (report.HANDLED(account_width_) ? + lexical_cast<std::size_t>(report.HANDLER(account_width_).str()) : + std::size_t(double(cols) * 0.302631)); + std::size_t amount_width = + (report.HANDLED(amount_width_) ? + lexical_cast<std::size_t>(report.HANDLER(amount_width_).str()) : + std::size_t(double(cols) * 0.157894)); + std::size_t total_width = + (report.HANDLED(total_width_) ? + lexical_cast<std::size_t>(report.HANDLER(total_width_).str()) : + amount_width); + std::size_t meta_width = + (report.HANDLED(meta_width_) ? + lexical_cast<std::size_t>(report.HANDLER(meta_width_).str()) : + 10); + + bool saw_date = false; + bool saw_payee = false; + bool saw_account = false; + bool saw_amount = false; + bool saw_total = false; + bool saw_meta = false; + + std::size_t cols_needed = 0; + foreach (const value_t& column, columns.to_sequence()) { + string ident; + if (get_principal_identifiers(as_expr(column), ident)) { + if (ident == "date" || ident == "aux_date") { + cols_needed += date_width + 1; + saw_date = true; + } + else if (ident == "payee") { + cols_needed += payee_width + 1; + saw_payee = true; + } + else if (ident == "account") { + cols_needed += account_width + 1; + saw_account = true; + } + else if (ident == "amount") { + cols_needed += amount_width + 1; + saw_amount = true; + } + else if (ident == "total") { + cols_needed += total_width + 1; + saw_total = true; + } + else { + cols_needed += meta_width + 1; + saw_meta = true; + } + } + } + + while ((saw_account || saw_payee) && cols_needed < cols) { + if (saw_account && cols_needed < cols) { + ++account_width; + ++cols_needed; + if (cols_needed < cols) { + ++account_width; + ++cols_needed; + } + } + if (saw_payee && cols_needed < cols) { + ++payee_width; + ++cols_needed; + } + } + + while ((saw_account || saw_payee) && cols_needed > cols && + account_width > 5 && payee_width > 5) { + DEBUG("auto.columns", "adjusting account down"); + if (saw_account && cols_needed > cols) { + --account_width; + --cols_needed; + if (cols_needed > cols) { + --account_width; + --cols_needed; + } + } + if (saw_payee && cols_needed > cols) { + --payee_width; + --cols_needed; + } + DEBUG("auto.columns", "account_width now = " << account_width); + } + + if (! report.HANDLED(date_width_)) + report.HANDLER(date_width_).value = to_string(date_width); + if (! report.HANDLED(payee_width_)) + report.HANDLER(payee_width_).value = to_string(payee_width); + if (! report.HANDLED(account_width_)) + report.HANDLER(account_width_).value = to_string(account_width); + if (! report.HANDLED(amount_width_)) + report.HANDLER(amount_width_).value = to_string(amount_width); + if (! report.HANDLED(total_width_)) + report.HANDLER(total_width_).value = to_string(total_width); + + foreach (const value_t& column, columns.to_sequence()) { + if (first) + first = false; + else + formatter << ' '; + + formatter << "%("; + + string ident; + if (get_principal_identifiers(as_expr(column), ident, true)) { + if (ident == "date" || ident == "aux_date") { + formatter << "ansify_if(" + << "ansify_if(justify(format_date("; + + as_expr(column)->print(formatter); + + formatter << "), int(date_width)),"; + formatter << "green if color and date > today)," + << "bold if should_bold)"; + + if (! thus_far.empty()) + thus_far += " + "; + thus_far += "int(date_width) + 1"; + } + else if (ident == "payee") { + formatter << "ansify_if(" + << "ansify_if(justify(truncated("; + + as_expr(column)->print(formatter); + + formatter << ", int(payee_width)), int(payee_width)),"; + formatter << "bold if color and !cleared and actual)," + << "bold if should_bold)"; + + if (! thus_far.empty()) + thus_far += " + "; + thus_far += "int(payee_width) + 1"; + } + else if (ident == "account") { + formatter << "ansify_if(" + << "ansify_if("; + + if (accounts_report) { + formatter << "partial_account(options.flat), blue if color),"; + } else { + formatter << "justify(truncated("; + as_expr(column)->print(formatter); + formatter << ", int(account_width), int(abbrev_len))," + << "int(account_width)),"; + formatter << "true, color),"; + + if (! thus_far.empty()) + thus_far += " + "; + thus_far += "int(account_width) + 1"; + } + + formatter << " bold if should_bold)"; + } + else if (ident == "amount" || ident == "total") { + formatter << "ansify_if(" + << "justify(scrub("; + + as_expr(column)->print(formatter); + + formatter << "), "; + + if (ident == "amount") + formatter << "int(amount_width),"; + else + formatter << "int(total_width),"; + + if (! thus_far.empty()) + thus_far += " + "; + + if (ident == "amount") + thus_far += "int(amount_width)"; + else + thus_far += "int(total_width)"; + + if (thus_far.empty()) + formatter << "-1"; + else + formatter << thus_far; + + formatter << ", true, color)," + << " bold if should_bold)"; + + thus_far += " + 1"; + } + else { + formatter << "ansify_if(" + << "justify(truncated("; + + as_expr(column)->print(formatter); + + formatter << ", int(meta_width or 10)), int(meta_width) or 10),"; + formatter << "bold if should_bold)"; + + if (! thus_far.empty()) + thus_far += " + "; + thus_far += "(int(meta_width) or 10) + 1"; + } + } + formatter << ")"; + } + formatter << "\\n"; + } + else if (keyword == "from") { + DEBUG("select.parse", "formatter: " << formatter.str()); + + if (arg == "xacts" || arg == "txns" || arg == "transactions") { + report_functor = expr_t::op_t::wrap_functor + (reporter<>(post_handler_ptr(new print_xacts(report, + report.HANDLED(raw))), + report, string("#select"))); + } + else if (arg == "posts" || arg == "postings") { + report_functor = expr_t::op_t::wrap_functor + (reporter<>(post_handler_ptr(new format_posts(report, formatter.str())), + report, string("#select"))); + } + else if (arg == "accounts") { + report_functor = expr_t::op_t::wrap_functor + (reporter<account_t, acct_handler_ptr, &report_t::accounts_report> + (acct_handler_ptr(new format_accounts(report, formatter.str())), + report, string("#select"))); + } + else if (arg == "commodities") { + report_functor = expr_t::op_t::wrap_functor + (reporter<post_t, post_handler_ptr, &report_t::commodities_report> + (post_handler_ptr(new format_posts(report, formatter.str())), + report, string("#select"))); + } + } + else if (keyword == "where") { +#if 0 + query_t query; + keep_details_t keeper(true, true, true); + expr_t::ptr_op_t expr = + query.parse_args(string_value(arg).to_sequence(), keeper, false, true); + report.HANDLER(limit_).on("#select", query.get_query(query_t::QUERY_LIMIT)); +#else + report.HANDLER(limit_).on("#select", arg); +#endif + } + else if (keyword == "display") { + report.HANDLER(display_).on("#select", arg); + } + else if (keyword == "collect") { + report.HANDLER(amount_).on("#select", arg); + } + else if (keyword == "group by") { + report.HANDLER(group_by_).on("#select", arg); + } + else if (keyword == "style") { + if (arg == "csv") { + } + else if (arg == "xml") { + } + else if (arg == "emacs") { + } + else if (arg == "org") { + } + } + + ++m1; + } + + if (! report_functor) { + report_functor = expr_t::op_t::wrap_functor + (reporter<>(post_handler_ptr(new format_posts(report, formatter.str())), + report, string("#select"))); + } + + call_scope_t call_args(report); + return report_functor->as_function()(call_args); +} + +} // namespace ledger diff --git a/src/predicate.cc b/src/select.h index fd301a7d..54883d22 100644 --- a/src/predicate.cc +++ b/src/select.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -29,12 +29,27 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include <system.hh> +/** + * @addtogroup select + */ + +/** + * @file select.h + * @author John Wiegley + * + * @ingroup select + */ +#ifndef _SELECT_H +#define _SELECT_H -#include "predicate.h" -#include "query.h" -#include "op.h" +#include "utils.h" +#include "value.h" namespace ledger { +class call_scope_t; +value_t select_command(call_scope_t& args); + } // namespace ledger + +#endif // _SELECT_H diff --git a/src/series.h b/src/series.h deleted file mode 100644 index 40f34051..00000000 --- a/src/series.h +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2003-2010, 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 series.h - * @author John Wiegley - * - * @ingroup expr - */ -#ifndef _SERIES_H -#define _SERIES_H - -#include "scope.h" - -namespace ledger { - -class expr_series_t -{ -protected: - scope_t * context; - -public: - optional<std::list<expr_t> > exprs; - expr_t default_expr; - std::string variable; - - expr_series_t(const std::string& _variable) - : context(NULL), default_expr(_variable), variable(_variable) { - TRACE_CTOR(expr_series_t, "std::string"); - } - expr_series_t(const expr_t& expr, const std::string& _variable) - : context(const_cast<expr_t&>(expr).get_context()), - default_expr(expr), variable(_variable) { - TRACE_CTOR(expr_series_t, "expr_t, std::string"); - } - expr_series_t(const expr_series_t& other) - : context(other.context), exprs(other.exprs), - default_expr(other.default_expr), variable(other.variable) { - TRACE_CTOR(expr_series_t, "copy"); - } - virtual ~expr_series_t() { - TRACE_DTOR(expr_series_t); - } - - scope_t * get_context() { - return context; - } - void set_context(scope_t * scope) { - context = scope; - } - - bool empty() const { - return ! exprs || exprs->empty(); - } - - void push_back(const expr_t& expr) { - if (! exprs) - exprs = std::list<expr_t>(); - exprs->push_back(expr); - } - void pop_back() { - assert(exprs); - exprs->pop_back(); - } - - void mark_uncompiled() { - if (exprs) - foreach (expr_t& expr, *exprs) - expr.mark_uncompiled(); - else - default_expr.mark_uncompiled(); - } - - void compile(scope_t& scope) { - if (exprs) - foreach (expr_t& expr, *exprs) - expr.compile(scope); - else - default_expr.compile(scope); - } - - value_t calc(scope_t& scope) { - if (exprs) { - value_t result; - symbol_scope_t sym_scope(scope); - std::size_t len(exprs->size()); - - foreach (expr_t& expr, *exprs) { - result = expr.calc(sym_scope); - if (--len > 0) - sym_scope.define(symbol_t::FUNCTION, variable, - expr_t::op_t::wrap_value(result)); - } - return result; - } else { - return default_expr.calc(scope); - } - } -}; - -} // namespace ledger - -#endif // _SERIES_H diff --git a/src/session.cc b/src/session.cc index 72e29895..9a77d341 100644 --- a/src/session.cc +++ b/src/session.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -62,12 +62,14 @@ void set_session_context(session_t * session) session_t::session_t() : flush_on_next_data_file(false), journal(new journal_t) { - TRACE_CTOR(session_t, ""); - if (const char * home_var = std::getenv("HOME")) HANDLER(price_db_).on(none, (path(home_var) / ".pricedb").string()); else HANDLER(price_db_).on(none, path("./.pricedb").string()); + + parsing_context.push(); + + TRACE_CTOR(session_t, ""); } std::size_t session_t::read_data(const string& master_account) @@ -89,14 +91,32 @@ std::size_t session_t::read_data(const string& master_account) std::size_t xact_count = 0; - account_t * acct = journal->master; - if (! master_account.empty()) + account_t * acct; + if (master_account.empty()) + acct = journal->master; + else acct = journal->find_account(master_account); optional<path> price_db_path; if (HANDLED(price_db_)) price_db_path = resolve_path(HANDLER(price_db_).str()); + if (HANDLED(explicit)) + journal->force_checking = true; + if (HANDLED(check_payees)) + journal->check_payees = true; + if (HANDLED(day_break)) + journal->day_break = true; + + if (HANDLED(permissive)) + journal->checking_style = journal_t::CHECK_PERMISSIVE; + else if (HANDLED(pedantic)) + journal->checking_style = journal_t::CHECK_ERROR; + else if (HANDLED(strict)) + journal->checking_style = journal_t::CHECK_WARNING; + else if (HANDLED(value_expr_)) + journal->value_expr = HANDLER(value_expr_).str(); + #if defined(HAVE_BOOST_SERIALIZATION) optional<archive_t> cache; if (HANDLED(cache_) && master_account.empty()) @@ -108,8 +128,17 @@ std::size_t session_t::read_data(const string& master_account) #endif // HAVE_BOOST_SERIALIZATION if (price_db_path) { if (exists(*price_db_path)) { - if (journal->read(*price_db_path) > 0) - throw_(parse_error, _("Transactions not allowed in price history file")); + parsing_context.push(*price_db_path); + parsing_context.get_current().journal = journal.get(); + try { + if (journal->read(parsing_context) > 0) + throw_(parse_error, _("Transactions not allowed in price history file")); + } + catch (...) { + parsing_context.pop(); + throw; + } + parsing_context.pop(); } } @@ -128,12 +157,22 @@ std::size_t session_t::read_data(const string& master_account) } buffer.flush(); - std::istringstream buf_in(buffer.str()); - xact_count += journal->read(buf_in, "/dev/stdin", acct); - journal->sources.push_back(journal_t::fileinfo_t()); + shared_ptr<std::istream> stream(new std::istringstream(buffer.str())); + parsing_context.push(stream); } else { - xact_count += journal->read(pathname, acct); + parsing_context.push(pathname); } + + parsing_context.get_current().journal = journal.get(); + parsing_context.get_current().master = acct; + try { + xact_count += journal->read(parsing_context); + } + catch (...) { + parsing_context.pop(); + throw; + } + parsing_context.pop(); } DEBUG("ledger.read", "xact_count [" << xact_count @@ -154,7 +193,7 @@ std::size_t session_t::read_data(const string& master_account) return journal->xacts.size(); } -void session_t::read_journal_files() +journal_t * session_t::read_journal_files() { INFO_START(journal, "Read journal file"); @@ -172,6 +211,37 @@ void session_t::read_journal_files() #if defined(DEBUG_ON) INFO("Found " << count << " transactions"); #endif + + return journal.get(); +} + +journal_t * session_t::read_journal(const path& pathname) +{ + HANDLER(file_).data_files.clear(); + HANDLER(file_).data_files.push_back(pathname); + + return read_journal_files(); +} + +journal_t * session_t::read_journal_from_string(const string& data) +{ + HANDLER(file_).data_files.clear(); + + shared_ptr<std::istream> stream(new std::istringstream(data)); + parsing_context.push(stream); + + parsing_context.get_current().journal = journal.get(); + parsing_context.get_current().master = journal->master; + try { + journal->read(parsing_context); + } + catch (...) { + parsing_context.pop(); + throw; + } + parsing_context.pop(); + + return journal.get(); } void session_t::close_journal_files() @@ -202,6 +272,15 @@ value_t session_t::fn_max(call_scope_t& args) return args[1] > args[0] ? args[1] : args[0]; } +value_t session_t::fn_int(call_scope_t& args) +{ + return args[0].to_long(); +} +value_t session_t::fn_str(call_scope_t& args) +{ + return string_value(args[0].to_string()); +} + value_t session_t::fn_lot_price(call_scope_t& args) { amount_t amt(args.get<amount_t>(1, false)); @@ -238,10 +317,15 @@ option_t<session_t> * session_t::lookup_option(const char * p) break; case 'c': OPT(cache_); + else OPT(check_payees); break; case 'd': OPT(download); // -Q else OPT(decimal_comma); + else OPT(day_break); + break; + case 'e': + OPT(explicit); break; case 'f': OPT_(file_); // -f @@ -258,10 +342,15 @@ option_t<session_t> * session_t::lookup_option(const char * p) case 'p': OPT(price_db_); else OPT(price_exp_); + else OPT(pedantic); + else OPT(permissive); break; case 's': OPT(strict); break; + case 'v': + OPT(value_expr_); + break; } return NULL; } @@ -288,6 +377,11 @@ expr_t::ptr_op_t session_t::lookup(const symbol_t::kind_t kind, return MAKE_FUNCTOR(session_t::fn_lot_tag); break; + case 'i': + if (is_eq(p, "int")) + return MAKE_FUNCTOR(session_t::fn_int); + break; + case 'm': if (is_eq(p, "min")) return MAKE_FUNCTOR(session_t::fn_min); @@ -295,6 +389,11 @@ expr_t::ptr_op_t session_t::lookup(const symbol_t::kind_t kind, return MAKE_FUNCTOR(session_t::fn_max); break; + case 's': + if (is_eq(p, "str")) + return MAKE_FUNCTOR(session_t::fn_str); + break; + default: break; } diff --git a/src/session.h b/src/session.h index b8fd52f2..a0aba91b 100644 --- a/src/session.h +++ b/src/session.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -44,6 +44,7 @@ #include "account.h" #include "journal.h" +#include "context.h" #include "option.h" #include "commodity.h" @@ -57,11 +58,15 @@ class session_t : public symbol_scope_t public: bool flush_on_next_data_file; - std::auto_ptr<journal_t> journal; + + unique_ptr<journal_t> journal; + parse_context_stack_t parsing_context; + optional<expr_t> value_expr; explicit session_t(); virtual ~session_t() { TRACE_DTOR(session_t); + parsing_context.pop(); } virtual string description() { @@ -72,14 +77,18 @@ public: flush_on_next_data_file = truth; } + journal_t * read_journal(const path& pathname); + journal_t * read_journal_from_string(const string& data); std::size_t read_data(const string& master_account = ""); - void read_journal_files(); + journal_t * read_journal_files(); void close_journal_files(); value_t fn_account(call_scope_t& scope); value_t fn_min(call_scope_t& scope); value_t fn_max(call_scope_t& scope); + value_t fn_int(call_scope_t& scope); + value_t fn_str(call_scope_t& scope); value_t fn_lot_price(call_scope_t& scope); value_t fn_lot_date(call_scope_t& scope); value_t fn_lot_tag(call_scope_t& scope); @@ -87,14 +96,20 @@ public: void report_options(std::ostream& out) { HANDLER(cache_).report(out); + HANDLER(check_payees).report(out); + HANDLER(day_break).report(out); HANDLER(download).report(out); HANDLER(decimal_comma).report(out); HANDLER(file_).report(out); HANDLER(input_date_format_).report(out); + HANDLER(explicit).report(out); HANDLER(master_account_).report(out); + HANDLER(pedantic).report(out); + HANDLER(permissive).report(out); HANDLER(price_db_).report(out); HANDLER(price_exp_).report(out); HANDLER(strict).report(out); + HANDLER(value_expr_).report(out); } option_t<session_t> * lookup_option(const char * p); @@ -107,6 +122,8 @@ public: */ OPTION(session_t, cache_); + OPTION(session_t, check_payees); + OPTION(session_t, day_break); OPTION(session_t, download); // -Q OPTION_(session_t, decimal_comma, DO() { @@ -115,33 +132,33 @@ public: OPTION__ (session_t, price_exp_, // -Z - CTOR(session_t, price_exp_) { value = 24L * 3600L; } - DO_(args) { - value = args.get<long>(1) * 60L; - }); + CTOR(session_t, price_exp_) { value = "24"; }); OPTION__ (session_t, file_, // -f std::list<path> data_files; CTOR(session_t, file_) {} - DO_(args) { - assert(args.size() == 2); + DO_(str) { if (parent->flush_on_next_data_file) { data_files.clear(); parent->flush_on_next_data_file = false; } - data_files.push_back(args.get<string>(1)); + data_files.push_back(str); }); - OPTION_(session_t, input_date_format_, DO_(args) { - // This changes static variables inside times.h, which affects the basic - // date parser. - set_input_date_format(args.get<string>(1).c_str()); + OPTION_(session_t, input_date_format_, DO_(str) { + // This changes static variables inside times.h, which affects the + // basic date parser. + set_input_date_format(str.c_str()); }); + OPTION(session_t, explicit); OPTION(session_t, master_account_); + OPTION(session_t, pedantic); + OPTION(session_t, permissive); OPTION(session_t, price_db_); OPTION(session_t, strict); + OPTION(session_t, value_expr_); }; /** diff --git a/src/stats.cc b/src/stats.cc index 524f5a87..0966fee2 100644 --- a/src/stats.cc +++ b/src/stats.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/stats.h b/src/stats.h index b7bf94c5..7b00fec8 100644 --- a/src/stats.h +++ b/src/stats.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/stream.cc b/src/stream.cc index 5d4cf5e0..ce40bfcc 100644 --- a/src/stream.cc +++ b/src/stream.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/stream.h b/src/stream.h index 42c85534..c317ebdf 100644 --- a/src/stream.h +++ b/src/stream.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/system.hh.in b/src/system.hh.in index 42a82e41..552a591a 100644 --- a/src/system.hh.in +++ b/src/system.hh.in @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -138,38 +138,58 @@ typedef std::ostream::pos_type ostream_pos_type; #include <boost/bind.hpp> #include <boost/cast.hpp> #include <boost/current_function.hpp> + #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/date_time/posix_time/posix_time_io.hpp> #include <boost/date_time/gregorian/gregorian_io.hpp> + #include <boost/filesystem/convenience.hpp> #include <boost/filesystem/exception.hpp> #include <boost/filesystem/fstream.hpp> #include <boost/filesystem/operations.hpp> #include <boost/filesystem/path.hpp> + #if !(defined(__GXX_EXPERIMENTAL_CXX0X__) && __GXX_EXPERIMENTAL_CXX0X__) #include <boost/foreach.hpp> #endif #include <boost/function.hpp> + +#include <boost/graph/adjacency_list.hpp> +#include <boost/graph/filtered_graph.hpp> +#include <boost/graph/dijkstra_shortest_paths.hpp> +#include <boost/graph/graphviz.hpp> + #include <boost/intrusive_ptr.hpp> + #include <boost/iostreams/stream.hpp> #include <boost/iostreams/write.hpp> #define BOOST_IOSTREAMS_USE_DEPRECATED 1 #include <boost/iostreams/device/file_descriptor.hpp> + #include <boost/iterator/iterator_facade.hpp> #include <boost/iterator/transform_iterator.hpp> + #include <boost/lexical_cast.hpp> #include <boost/operators.hpp> #include <boost/optional.hpp> #include <boost/ptr_container/ptr_list.hpp> + #include <boost/random/mersenne_twister.hpp> #include <boost/random/uniform_int.hpp> #include <boost/random/uniform_real.hpp> #include <boost/random/variate_generator.hpp> + #if defined(HAVE_BOOST_REGEX_UNICODE) #include <boost/regex/icu.hpp> #else #include <boost/regex.hpp> #endif // HAVE_BOOST_REGEX_UNICODE + +#include <boost/tokenizer.hpp> + +#include <boost/tuple/tuple.hpp> +#include <boost/tuple/tuple_comparison.hpp> + #include <boost/variant.hpp> #include <boost/version.hpp> @@ -226,12 +246,19 @@ void serialize(Archive& ar, boost::intrusive_ptr<T>& ptr, const unsigned int) } } -template <class Archive, class T> -void serialize(Archive&, boost::function<T>&, const unsigned int) -{ +template <class Archive> +void serialize(Archive&, boost::any&, const unsigned int) { + // jww (2012-03-29): Should we really ignore any fields entirely? + // These occur inside value_t::storage_t::data's variant. } template <class Archive> +void serialize(Archive&, boost::blank&, const unsigned int) {} + +template <class Archive, class T> +void serialize(Archive&, boost::function<T>&, const unsigned int) {} + +template <class Archive> void serialize(Archive& ar, istream_pos_type& pos, const unsigned int) { ar & make_binary_object(&pos, sizeof(istream_pos_type)); @@ -257,4 +284,6 @@ void serialize(Archive& ar, istream_pos_type& pos, const unsigned int) #include <boost/python/module_init.hpp> #include <boost/python/suite/indexing/vector_indexing_suite.hpp> +#include <boost/iterator/indirect_iterator.hpp> + #endif // HAVE_BOOST_PYTHON diff --git a/src/temps.cc b/src/temps.cc index 365c33c5..881077f6 100644 --- a/src/temps.cc +++ b/src/temps.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -81,7 +81,8 @@ post_t& temporaries_t::copy_post(post_t& origin, xact_t& xact, return temp; } -post_t& temporaries_t::create_post(xact_t& xact, account_t * account) +post_t& temporaries_t::create_post(xact_t& xact, account_t * account, + bool bidir_link) { if (! post_temps) post_temps = std::list<post_t>(); @@ -93,7 +94,10 @@ post_t& temporaries_t::create_post(xact_t& xact, account_t * account) temp.account = account; temp.account->add_post(&temp); - xact.add_post(&temp); + if (bidir_link) + xact.add_post(&temp); + else + temp.xact = &xact; return temp; } diff --git a/src/temps.h b/src/temps.h index 1e7eb69f..daa1493b 100644 --- a/src/temps.h +++ b/src/temps.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -51,7 +51,11 @@ class temporaries_t optional<std::list<account_t> > acct_temps; public: + temporaries_t() { + TRACE_CTOR(temporaries_t, ""); + } ~temporaries_t() { + TRACE_DTOR(temporaries_t); clear(); } @@ -62,7 +66,8 @@ public: } post_t& copy_post(post_t& origin, xact_t& xact, account_t * account = NULL); - post_t& create_post(xact_t& xact, account_t * account); + post_t& create_post(xact_t& xact, account_t * account, + bool bidir_link = true); post_t& last_post() { return post_temps->back(); } diff --git a/src/textual.cc b/src/textual.cc index c7c49e2a..d0e4dad2 100644 --- a/src/textual.cc +++ b/src/textual.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -32,6 +32,7 @@ #include <system.hh> #include "journal.h" +#include "context.h" #include "xact.h" #include "post.h" #include "account.h" @@ -39,7 +40,9 @@ #include "query.h" #include "pstream.h" #include "pool.h" -#include "session.h" +#if defined(HAVE_BOOST_PYTHON) +#include "pyinterp.h" +#endif #define TIMELOG_SUPPORT 1 #if defined(TIMELOG_SUPPORT) @@ -49,123 +52,150 @@ namespace ledger { namespace { - typedef std::pair<commodity_t *, amount_t> fixed_rate_t; - typedef variant<account_t *, string, fixed_rate_t> state_t; + typedef std::pair<commodity_t *, amount_t> fixed_rate_t; - class parse_context_t : public noncopyable + struct application_t + { + string label; + variant<optional<datetime_t>, account_t *, string, fixed_rate_t> value; + + application_t(string _label, optional<datetime_t> epoch) + : label(_label), value(epoch) {} + application_t(string _label, account_t * acct) + : label(_label), value(acct) {} + application_t(string _label, string tag) + : label(_label), value(tag) {} + application_t(string _label, fixed_rate_t rate) + : label(_label), value(rate) {} + }; + + class instance_t : public noncopyable, public scope_t { public: - journal_t& journal; - scope_t& scope; - std::list<state_t> state_stack; + parse_context_stack_t& context_stack; + parse_context_t& context; + std::istream& in; + instance_t * parent; + std::list<application_t> apply_stack; #if defined(TIMELOG_SUPPORT) - time_log_t timelog; + time_log_t timelog; #endif - bool strict; - std::size_t count; - std::size_t errors; - std::size_t sequence; - - parse_context_t(journal_t& _journal, scope_t& _scope) - : journal(_journal), scope(_scope), timelog(journal, scope), - strict(false), count(0), errors(0), sequence(1) { - timelog.context_count = &count; - } - bool front_is_account() { - return state_stack.front().type() == typeid(account_t *); - } - bool front_is_string() { - return state_stack.front().type() == typeid(string); - } - bool front_is_fixed_rate() { - return state_stack.front().type() == typeid(fixed_rate_t); - } + instance_t(parse_context_stack_t& _context_stack, + parse_context_t& _context, + instance_t * _parent = NULL) + : context_stack(_context_stack), context(_context), + in(*context.stream.get()), parent(_parent), + timelog(context) {} - account_t * top_account() { - foreach (state_t& state, state_stack) - if (state.type() == typeid(account_t *)) - return boost::get<account_t *>(state); - return NULL; + virtual string description() { + return _("textual parser"); } - void close() { - timelog.close(); + template <typename T> + void get_applications(std::vector<T>& result) { + foreach (application_t& state, apply_stack) { + if (state.value.type() == typeid(T)) + result.push_back(boost::get<T>(state.value)); + } + if (parent) + parent->get_applications<T>(result); } - }; - class instance_t : public noncopyable, public scope_t - { - static const std::size_t MAX_LINE = 1024; - - public: - parse_context_t& context; - instance_t * parent; - accounts_map account_aliases; - const path * original_file; - path pathname; - std::istream& in; - char linebuf[MAX_LINE + 1]; - std::size_t linenum; - istream_pos_type line_beg_pos; - istream_pos_type curr_pos; - optional<datetime_t> prev_epoch; - - instance_t(parse_context_t& _context, - std::istream& _in, - const path * _original_file = NULL, - instance_t * _parent = NULL); - - ~instance_t(); + template <typename T> + optional<T> get_application() { + foreach (application_t& state, apply_stack) { + if (state.value.type() == typeid(T)) + return boost::get<T>(state.value); + } + return parent ? parent->get_application<T>() : none; + } - virtual string description() { - return _("textual parser"); + account_t * top_account() { + if (optional<account_t *> acct = get_application<account_t *>()) + return *acct; + else + return NULL; } void parse(); + std::streamsize read_line(char *& line); + bool peek_whitespace_line() { return (in.good() && ! in.eof() && (in.peek() == ' ' || in.peek() == '\t')); } +#if defined(HAVE_BOOST_PYTHON) + bool peek_blank_line() { + return (in.good() && ! in.eof() && + (in.peek() == '\n' || in.peek() == '\r')); + } +#endif - void read_next_directive(); + void read_next_directive(bool& error_flag); #if defined(TIMELOG_SUPPORT) void clock_in_directive(char * line, bool capitalized); void clock_out_directive(char * line, bool capitalized); #endif - void default_commodity_directive(char * line); + bool general_directive(char * line); + + void account_directive(char * line); + void account_alias_directive(account_t * account, string alias); + void account_payee_directive(account_t * account, string payee); + void account_value_directive(account_t * account, string expr_str); + void account_default_directive(account_t * account); + void default_account_directive(char * line); - void price_conversion_directive(char * line); + void alias_directive(char * line); + + void payee_directive(char * line); + void payee_alias_directive(const string& payee, string alias); + + void commodity_directive(char * line); + void commodity_alias_directive(commodity_t& comm, string alias); + void commodity_value_directive(commodity_t& comm, string expr_str); + void commodity_format_directive(commodity_t& comm, string format); + void commodity_nomarket_directive(commodity_t& comm); + void commodity_default_directive(commodity_t& comm); + + void default_commodity_directive(char * line); + + void tag_directive(char * line); + + void apply_directive(char * line); + void apply_account_directive(char * line); + void apply_tag_directive(char * line); + void apply_rate_directive(char * line); + void apply_year_directive(char * line); + void end_apply_directive(char * line); + + void xact_directive(char * line, std::streamsize len); + void period_xact_directive(char * line); + void automated_xact_directive(char * line); void price_xact_directive(char * line); + void price_conversion_directive(char * line); void nomarket_directive(char * line); - void year_directive(char * line); - void option_directive(char * line); - void automated_xact_directive(char * line); - void period_xact_directive(char * line); - void xact_directive(char * line, std::streamsize len); + void include_directive(char * line); - void master_account_directive(char * line); - void end_directive(char * line); - void alias_directive(char * line); - void fixed_directive(char * line); - void payee_mapping_directive(char * line); - void account_mapping_directive(char * line); - void tag_directive(char * line); - void define_directive(char * line); + void option_directive(char * line); + void comment_directive(char * line); + + void eval_directive(char * line); void assert_directive(char * line); void check_directive(char * line); - void comment_directive(char * line); - void expr_directive(char * line); - bool general_directive(char * line); + void value_directive(char * line); + + void import_directive(char * line); + void python_directive(char * line); post_t * parse_post(char * line, std::streamsize len, account_t * account, xact_t * xact, - bool defer_expr = false); + bool defer_expr = false); bool parse_posts(account_t * account, xact_base_t& xact, @@ -209,43 +239,27 @@ namespace { } } -instance_t::instance_t(parse_context_t& _context, - std::istream& _in, - const path * _original_file, - instance_t * _parent) - : context(_context), parent(_parent), original_file(_original_file), - pathname(original_file ? *original_file : "/dev/stdin"), in(_in) -{ - TRACE_CTOR(instance_t, "..."); - DEBUG("times.epoch", "Saving epoch " << epoch); - prev_epoch = epoch; // declared in times.h -} - -instance_t::~instance_t() -{ - TRACE_DTOR(instance_t); - epoch = prev_epoch; - DEBUG("times.epoch", "Restored epoch to " << epoch); -} - void instance_t::parse() { - INFO("Parsing file '" << pathname.string() << "'"); + INFO("Parsing file " << context.pathname); - TRACE_START(instance_parse, 1, - "Done parsing file '" << pathname.string() << "'"); + TRACE_START(instance_parse, 1, "Done parsing file " << context.pathname); if (! in.good() || in.eof()) return; - linenum = 0; - curr_pos = in.tellg(); + context.linenum = 0; + context.curr_pos = in.tellg(); + + bool error_flag = false; while (in.good() && ! in.eof()) { try { - read_next_directive(); + read_next_directive(error_flag); } catch (const std::exception& err) { + error_flag = true; + string current_context = error_context(); if (parent) { @@ -258,11 +272,9 @@ void instance_t::parse() foreach (instance_t * instance, instances) add_error_context(_("In file included from %1") - << file_context(instance->pathname, - instance->linenum)); + << instance->context.location()); } - add_error_context(_("While parsing file %1") - << file_context(pathname, linenum)); + add_error_context(_("While parsing file %1") << context.location()); if (caught_signal != NONE_CAUGHT) throw; @@ -287,48 +299,55 @@ std::streamsize instance_t::read_line(char *& line) assert(in.good()); assert(! in.eof()); // no one should call us in that case - line_beg_pos = curr_pos; + context.line_beg_pos = context.curr_pos; check_for_signal(); - in.getline(linebuf, MAX_LINE); + in.getline(context.linebuf, parse_context_t::MAX_LINE); std::streamsize len = in.gcount(); if (len > 0) { - if (linenum == 0 && utf8::is_bom(linebuf)) - line = &linebuf[3]; - else - line = linebuf; + context.linenum++; - if (line[len - 1] == '\r') // strip Windows CRLF down to LF - line[--len] = '\0'; + context.curr_pos = context.line_beg_pos; + context.curr_pos += len; - linenum++; + if (context.linenum == 0 && utf8::is_bom(context.linebuf)) { + line = &context.linebuf[3]; + len -= 3; + } else { + line = context.linebuf; + } - curr_pos = line_beg_pos; - curr_pos += len; + --len; + while (len > 0 && std::isspace(line[len - 1])) // strip trailing whitespace + line[--len] = '\0'; - return len - 1; // LF is being silently dropped + return len; } return 0; } -void instance_t::read_next_directive() +void instance_t::read_next_directive(bool& error_flag) { char * line; std::streamsize len = read_line(line); if (len == 0 || line == NULL) return; + if (! std::isspace(line[0])) + error_flag = false; + switch (line[0]) { case '\0': assert(false); // shouldn't ever reach here break; case ' ': - case '\t': { + case '\t': + if (! error_flag) + throw parse_error(_("Unexpected whitespace at beginning of line")); break; - } case ';': // comments case '#': @@ -402,7 +421,7 @@ void instance_t::read_next_directive() price_xact_directive(line); break; case 'Y': // set the current year - year_directive(line); + apply_year_directive(line); break; } } @@ -426,19 +445,19 @@ void instance_t::clock_in_directive(char * line, bool /*capitalized*/) end = NULL; position_t position; - position.pathname = pathname; - position.beg_pos = line_beg_pos; - position.beg_line = linenum; - position.end_pos = curr_pos; - position.end_line = linenum; + position.pathname = context.pathname; + position.beg_pos = context.line_beg_pos; + position.beg_line = context.linenum; + position.end_pos = context.curr_pos; + position.end_line = context.linenum; position.sequence = context.sequence++; time_xact_t event(position, parse_datetime(datetime), - p ? context.top_account()->find_account(p) : NULL, + p ? top_account()->find_account(p) : NULL, n ? n : "", end ? end : ""); - context.timelog.clock_in(event); + timelog.clock_in(event); } void instance_t::clock_out_directive(char * line, bool /*capitalized*/) @@ -455,20 +474,19 @@ void instance_t::clock_out_directive(char * line, bool /*capitalized*/) end = NULL; position_t position; - position.pathname = pathname; - position.beg_pos = line_beg_pos; - position.beg_line = linenum; - position.end_pos = curr_pos; - position.end_line = linenum; + position.pathname = context.pathname; + position.beg_pos = context.line_beg_pos; + position.beg_line = context.linenum; + position.end_pos = context.curr_pos; + position.end_line = context.linenum; position.sequence = context.sequence++; time_xact_t event(position, parse_datetime(datetime), - p ? context.top_account()->find_account(p) : NULL, + p ? top_account()->find_account(p) : NULL, n ? n : "", end ? end : ""); - context.timelog.clock_out(event); - context.count++; + context.count += timelog.clock_out(event); } #endif // TIMELOG_SUPPORT @@ -483,8 +501,8 @@ void instance_t::default_commodity_directive(char * line) void instance_t::default_account_directive(char * line) { - context.journal.bucket = context.top_account()->find_account(skip_ws(line + 1)); - context.journal.bucket->add_flags(ACCOUNT_KNOWN); + context.journal->bucket = top_account()->find_account(skip_ws(line + 1)); + context.journal->bucket->add_flags(ACCOUNT_KNOWN); } void instance_t::price_conversion_directive(char * line) @@ -514,16 +532,6 @@ void instance_t::nomarket_directive(char * line) commodity->add_flags(COMMODITY_NOMARKET | COMMODITY_KNOWN); } -void instance_t::year_directive(char * line) -{ - unsigned short year(lexical_cast<unsigned short>(skip_ws(line + 1))); - DEBUG("times.epoch", "Setting current year to " << year); - // This must be set to the last day of the year, otherwise partial - // dates like "11/01" will refer to last year's november, not the - // current year. - epoch = datetime_t(date_t(year, 12, 31)); -} - void instance_t::option_directive(char * line) { char * p = next_element(line); @@ -533,13 +541,15 @@ void instance_t::option_directive(char * line) *p++ = '\0'; } - if (! process_option(pathname.string(), line + 2, context.scope, p, line)) + path abs_path(filesystem::absolute(context.pathname, + context.current_directory)); + if (! process_option(abs_path.string(), line + 2, *context.scope, p, line)) throw_(option_error, _("Illegal option --%1") << line + 2); } void instance_t::automated_xact_directive(char * line) { - istream_pos_type pos= line_beg_pos; + istream_pos_type pos = context.line_beg_pos; bool reveal_context = true; @@ -550,18 +560,17 @@ void instance_t::automated_xact_directive(char * line) query.parse_args(string_value(skip_ws(line + 1)).to_sequence(), keeper, false, true); - std::auto_ptr<auto_xact_t> ae(new auto_xact_t(predicate_t(expr, keeper))); + unique_ptr<auto_xact_t> ae(new auto_xact_t(predicate_t(expr, keeper))); ae->pos = position_t(); - ae->pos->pathname = pathname; - ae->pos->beg_pos = line_beg_pos; - ae->pos->beg_line = linenum; + ae->pos->pathname = context.pathname; + ae->pos->beg_pos = context.line_beg_pos; + ae->pos->beg_line = context.linenum; ae->pos->sequence = context.sequence++; post_t * last_post = NULL; while (peek_whitespace_line()) { std::streamsize len = read_line(line); - char * p = skip_ws(line); if (! *p) break; @@ -576,59 +585,56 @@ void instance_t::automated_xact_directive(char * line) item = ae.get(); // This is a trailing note, and possibly a metadata info tag - item->append_note(p + 1, context.scope, true); - item->pos->end_pos = curr_pos; + item->append_note(p + 1, *context.scope, true); + item->add_flags(ITEM_NOTE_ON_NEXT_LINE); + item->pos->end_pos = context.curr_pos; item->pos->end_line++; - - // If there was no last_post yet, then deferred notes get applied to - // the matched posting. Other notes get applied to the auto-generated - // posting. - ae->deferred_notes->back().apply_to_post = last_post; } else if ((remlen > 7 && *p == 'a' && std::strncmp(p, "assert", 6) == 0 && std::isspace(p[6])) || (remlen > 6 && *p == 'c' && std::strncmp(p, "check", 5) == 0 && std::isspace(p[5])) || (remlen > 5 && *p == 'e' && - std::strncmp(p, "expr", 4) == 0 && std::isspace(p[4]))) { + ((std::strncmp(p, "expr", 4) == 0 && std::isspace(p[4])) || + (std::strncmp(p, "eval", 4) == 0 && std::isspace(p[4]))))) { const char c = *p; p = skip_ws(&p[*p == 'a' ? 6 : (*p == 'c' ? 5 : 4)]); if (! ae->check_exprs) - ae->check_exprs = auto_xact_t::check_expr_list(); + ae->check_exprs = expr_t::check_expr_list(); ae->check_exprs->push_back - (auto_xact_t::check_expr_pair(expr_t(p), - c == 'a' ? - auto_xact_t::EXPR_ASSERTION : - (c == 'c' ? - auto_xact_t::EXPR_CHECK : - auto_xact_t::EXPR_GENERAL))); + (expr_t::check_expr_pair(expr_t(p), + c == 'a' ? + expr_t::EXPR_ASSERTION : + (c == 'c' ? + expr_t::EXPR_CHECK : + expr_t::EXPR_GENERAL))); } else { reveal_context = false; if (post_t * post = - parse_post(p, len - (p - line), context.top_account(), - NULL, true)) { + parse_post(p, len - (p - line), top_account(), NULL, true)) { reveal_context = true; ae->add_post(post); - last_post = post; + ae->active_post = last_post = post; } reveal_context = true; } } - context.journal.auto_xacts.push_back(ae.get()); + context.journal->auto_xacts.push_back(ae.get()); - ae->journal = &context.journal; - ae->pos->end_pos = curr_pos; - ae->pos->end_line = linenum; + ae->journal = context.journal; + ae->pos->end_pos = context.curr_pos; + ae->pos->end_line = context.linenum; ae.release(); } catch (const std::exception&) { if (reveal_context) { add_error_context(_("While parsing automated transaction:")); - add_error_context(source_context(pathname, pos, curr_pos, "> ")); + add_error_context(source_context(context.pathname, pos, + context.curr_pos, "> ")); } throw; } @@ -636,31 +642,31 @@ void instance_t::automated_xact_directive(char * line) void instance_t::period_xact_directive(char * line) { - istream_pos_type pos = line_beg_pos; + istream_pos_type pos = context.line_beg_pos; bool reveal_context = true; try { - std::auto_ptr<period_xact_t> pe(new period_xact_t(skip_ws(line + 1))); + unique_ptr<period_xact_t> pe(new period_xact_t(skip_ws(line + 1))); pe->pos = position_t(); - pe->pos->pathname = pathname; - pe->pos->beg_pos = line_beg_pos; - pe->pos->beg_line = linenum; + pe->pos->pathname = context.pathname; + pe->pos->beg_pos = context.line_beg_pos; + pe->pos->beg_line = context.linenum; pe->pos->sequence = context.sequence++; reveal_context = false; - if (parse_posts(context.top_account(), *pe.get())) { + if (parse_posts(top_account(), *pe.get())) { reveal_context = true; - pe->journal = &context.journal; + pe->journal = context.journal; if (pe->finalize()) { - context.journal.extend_xact(pe.get()); - context.journal.period_xacts.push_back(pe.get()); + context.journal->extend_xact(pe.get()); + context.journal->period_xacts.push_back(pe.get()); - pe->pos->end_pos = curr_pos; - pe->pos->end_line = linenum; + pe->pos->end_pos = context.curr_pos; + pe->pos->end_line = context.linenum; pe.release(); } else { @@ -674,7 +680,8 @@ void instance_t::period_xact_directive(char * line) catch (const std::exception&) { if (reveal_context) { add_error_context(_("While parsing periodic transaction:")); - add_error_context(source_context(pathname, pos, curr_pos, "> ")); + add_error_context(source_context(context.pathname, pos, + context.curr_pos, "> ")); } throw; } @@ -684,10 +691,10 @@ void instance_t::xact_directive(char * line, std::streamsize len) { TRACE_START(xacts, 1, "Time spent handling transactions:"); - if (xact_t * xact = parse_xact(line, len, context.top_account())) { - std::auto_ptr<xact_t> manager(xact); + if (xact_t * xact = parse_xact(line, len, top_account())) { + unique_ptr<xact_t> manager(xact); - if (context.journal.add_xact(xact)) { + if (context.journal->add_xact(xact)) { manager.release(); // it's owned by the journal now context.count++; } @@ -709,12 +716,13 @@ void instance_t::include_directive(char * line) if (line[0] != '/' && line[0] != '\\' && line[0] != '~') { DEBUG("textual.include", "received a relative path"); - DEBUG("textual.include", "parent file path: " << pathname.string()); - string::size_type pos = pathname.string().rfind('/'); + DEBUG("textual.include", "parent file path: " << context.pathname); + string pathstr(context.pathname.string()); + string::size_type pos = pathstr.rfind('/'); if (pos == string::npos) - pos = pathname.string().rfind('\\'); + pos = pathstr.rfind('\\'); if (pos != string::npos) { - filename = path(string(pathname.string(), 0, pos + 1)) / line; + filename = path(string(pathstr, 0, pos + 1)) / line; DEBUG("textual.include", "normalized path: " << filename.string()); } else { filename = path(string(".")) / line; @@ -761,10 +769,42 @@ void instance_t::include_directive(char * line) string base = (*iter).leaf(); #endif // BOOST_VERSION >= 103700 if (glob.match(base)) { - path inner_file(*iter); - ifstream stream(inner_file); - instance_t instance(context, stream, &inner_file, this); - instance.parse(); + journal_t * journal = context.journal; + account_t * master = top_account(); + scope_t * scope = context.scope; + std::size_t& errors = context.errors; + std::size_t& count = context.count; + std::size_t& sequence = context.sequence; + + DEBUG("textual.include", "Including: " << *iter); + DEBUG("textual.include", "Master account: " << master->fullname()); + + context_stack.push(*iter); + + context_stack.get_current().journal = journal; + context_stack.get_current().master = master; + context_stack.get_current().scope = scope; + try { + instance_t instance(context_stack, + context_stack.get_current(), this); + instance.apply_stack.push_front(application_t("account", master)); + instance.parse(); + } + catch (...) { + errors += context_stack.get_current().errors; + count += context_stack.get_current().count; + sequence += context_stack.get_current().sequence; + + context_stack.pop(); + throw; + } + + errors += context_stack.get_current().errors; + count += context_stack.get_current().count; + sequence += context_stack.get_current().sequence; + + context_stack.pop(); + files_found = true; } } @@ -773,156 +813,348 @@ void instance_t::include_directive(char * line) if (! files_found) throw_(std::runtime_error, - _("File to include was not found: '%1'") << filename); + _("File to include was not found: %1") << filename); } -void instance_t::master_account_directive(char * line) +void instance_t::apply_directive(char * line) +{ + char * b = next_element(line); + string keyword(line); + if (keyword == "account") + apply_account_directive(b); + else if (keyword == "tag") + apply_tag_directive(b); + else if (keyword == "fixed" || keyword == "rate") + apply_rate_directive(b); + else if (keyword == "year") + apply_year_directive(b); +} + +void instance_t::apply_account_directive(char * line) { - if (account_t * acct = context.top_account()->find_account(line)) - context.state_stack.push_front(acct); + if (account_t * acct = top_account()->find_account(line)) + apply_stack.push_front(application_t("account", acct)); #if !defined(NO_ASSERTS) else assert("Failed to create account" == NULL); #endif } -void instance_t::end_directive(char * kind) +void instance_t::apply_tag_directive(char * line) { - string name(kind ? kind : ""); + string tag(trim_ws(line)); - if ((name.empty() || name == "account") && ! context.front_is_account()) - throw_(std::runtime_error, - _("'end account' directive does not match open directive")); - else if (name == "tag" && ! context.front_is_string()) - throw_(std::runtime_error, - _("'end tag' directive does not match open directive")); - else if (name == "fixed" && ! context.front_is_fixed_rate()) - throw_(std::runtime_error, - _("'end fixed' directive does not match open directive")); + if (tag.find(':') == string::npos) + tag = string(":") + tag + ":"; - if (context.state_stack.size() <= 1) + apply_stack.push_front(application_t("tag", tag)); +} + +void instance_t::apply_rate_directive(char * line) +{ + if (optional<std::pair<commodity_t *, price_point_t> > price_point = + commodity_pool_t::current_pool->parse_price_directive(trim_ws(line), true)) { + apply_stack.push_front + (application_t("fixed", fixed_rate_t(price_point->first, + price_point->second.price))); + } else { + throw_(std::runtime_error, _("Error in fixed directive")); + } +} + +void instance_t::apply_year_directive(char * line) +{ + apply_stack.push_front(application_t("year", epoch)); + + // This must be set to the last day of the year, otherwise partial + // dates like "11/01" will refer to last year's november, not the + // current year. + unsigned short year(lexical_cast<unsigned short>(skip_ws(line + 1))); + DEBUG("times.epoch", "Setting current year to " << year); + epoch = datetime_t(date_t(year, 12, 31)); +} + +void instance_t::end_apply_directive(char * kind) +{ + char * b = kind ? next_element(kind) : NULL; + string name(b ? b : ""); + + if (apply_stack.size() <= 1) { + if (name.empty()) { + throw_(std::runtime_error, + _("'end' or 'end apply' found, but no enclosing 'apply' directive")); + } else { + throw_(std::runtime_error, + _("'end apply %1' found, but no enclosing 'apply' directive") + << name); + } + } + + if (! name.empty() && name != apply_stack.front().label) throw_(std::runtime_error, - _("'end' found, but no enclosing tag or account directive")); - else - context.state_stack.pop_front(); + _("'end apply %1' directive does not match 'apply %2' directive") + << name << apply_stack.front().label); + + if (apply_stack.front().value.type() == typeid(optional<datetime_t>)) + epoch = boost::get<optional<datetime_t> >(apply_stack.front().value); + + apply_stack.pop_front(); +} + +void instance_t::account_directive(char * line) +{ + istream_pos_type beg_pos = context.line_beg_pos; + std::size_t beg_linenum = context.linenum; + + char * p = skip_ws(line); + account_t * account = + context.journal->register_account(p, NULL, top_account()); + unique_ptr<auto_xact_t> ae; + + while (peek_whitespace_line()) { + read_line(line); + char * q = skip_ws(line); + if (! *q) + break; + + char * b = next_element(q); + string keyword(q); + if (keyword == "alias") { + account_alias_directive(account, b); + } + else if (keyword == "payee") { + account_payee_directive(account, b); + } + else if (keyword == "value") { + account_value_directive(account, b); + } + else if (keyword == "default") { + account_default_directive(account); + } + else if (keyword == "assert" || keyword == "check") { + keep_details_t keeper(true, true, true); + expr_t expr(string("account == \"") + account->fullname() + "\""); + predicate_t pred(expr.get_op(), keeper); + + if (! ae.get()) { + ae.reset(new auto_xact_t(pred)); + + ae->pos = position_t(); + ae->pos->pathname = context.pathname; + ae->pos->beg_pos = beg_pos; + ae->pos->beg_line = beg_linenum; + ae->pos->sequence = context.sequence++; + ae->check_exprs = expr_t::check_expr_list(); + } + + ae->check_exprs->push_back + (expr_t::check_expr_pair(expr_t(b), + keyword == "assert" ? + expr_t::EXPR_ASSERTION : + expr_t::EXPR_CHECK)); + } + else if (keyword == "eval" || keyword == "expr") { + // jww (2012-02-27): Make account into symbol scopes so that this + // can be used to override definitions within the account. + bind_scope_t bound_scope(*context.scope, *account); + expr_t(b).calc(bound_scope); + } + else if (keyword == "note") { + account->note = b; + } + } + + if (ae.get()) { + context.journal->auto_xacts.push_back(ae.get()); + + ae->journal = context.journal; + ae->pos->end_pos = in.tellg(); + ae->pos->end_line = context.linenum; + + ae.release(); + } +} + +void instance_t::account_alias_directive(account_t * account, string alias) +{ + // Once we have an alias name (alias) and the target account + // (account), add a reference to the account in the `account_aliases' + // map, which is used by the post parser to resolve alias references. + trim(alias); + std::pair<accounts_map::iterator, bool> result = + context.journal->account_aliases.insert + (accounts_map::value_type(alias, account)); + if (! result.second) + (*result.first).second = account; } void instance_t::alias_directive(char * line) { - char * b = skip_ws(line); - if (char * e = std::strchr(b, '=')) { + if (char * e = std::strchr(line, '=')) { char * z = e - 1; while (std::isspace(*z)) *z-- = '\0'; *e++ = '\0'; e = skip_ws(e); - // Once we have an alias name (b) and the target account - // name (e), add a reference to the account in the - // `account_aliases' map, which is used by the post - // parser to resolve alias references. - account_t * acct = context.top_account()->find_account(e); - std::pair<accounts_map::iterator, bool> result - = account_aliases.insert(accounts_map::value_type(b, acct)); - assert(result.second); + account_alias_directive(top_account()->find_account(e), line); } } -void instance_t::fixed_directive(char * line) +void instance_t::account_payee_directive(account_t * account, string payee) { - if (optional<std::pair<commodity_t *, price_point_t> > price_point = - commodity_pool_t::current_pool->parse_price_directive(trim_ws(line), - true)) { - context.state_stack.push_front(fixed_rate_t(price_point->first, - price_point->second.price)); - } else { - throw_(std::runtime_error, _("Error in fixed directive")); - } + trim(payee); + context.journal->payees_for_unknown_accounts + .push_back(account_mapping_t(mask_t(payee), account)); } -void instance_t::payee_mapping_directive(char * line) +void instance_t::account_default_directive(account_t * account) { - char * payee = skip_ws(line); - char * regex = next_element(payee, true); + context.journal->bucket = account; +} - if (regex) - context.journal.payee_mappings.push_back - (payee_mapping_t(mask_t(regex), payee)); +void instance_t::account_value_directive(account_t * account, string expr_str) +{ + account->value_expr = expr_t(expr_str); +} + +void instance_t::payee_directive(char * line) +{ + string payee = context.journal->register_payee(line, NULL); while (peek_whitespace_line()) { -#if defined(NO_ASSERTS) read_line(line); -#else - std::streamsize len = read_line(line); - assert(len > 0); -#endif - - regex = skip_ws(line); - if (! *regex) + char * p = skip_ws(line); + if (! *p) break; - context.journal.payee_mappings.push_back - (payee_mapping_t(mask_t(regex), payee)); + char * b = next_element(p); + string keyword(p); + if (keyword == "alias") + payee_alias_directive(payee, b); } } -void instance_t::account_mapping_directive(char * line) +void instance_t::payee_alias_directive(const string& payee, string alias) { - char * account_name = skip_ws(line); - char * payee_regex = next_element(account_name, true); + trim(alias); + context.journal->payee_mappings + .push_back(payee_mapping_t(mask_t(alias), payee)); +} - if (payee_regex) - context.journal.account_mappings.push_back - (account_mapping_t(mask_t(payee_regex), - context.top_account()->find_account(account_name))); +void instance_t::commodity_directive(char * line) +{ + char * p = skip_ws(line); + string symbol; + commodity_t::parse_symbol(p, symbol); - while (peek_whitespace_line()) { -#if defined(NO_ASSERTS) - read_line(line); -#else - std::streamsize len = read_line(line); - assert(len > 0); -#endif + if (commodity_t * commodity = + commodity_pool_t::current_pool->find_or_create(symbol)) { + context.journal->register_commodity(*commodity, 0); - payee_regex = skip_ws(line); - if (! *payee_regex) - break; + while (peek_whitespace_line()) { + read_line(line); + char * q = skip_ws(line); + if (! *q) + break; - context.journal.account_mappings.push_back - (account_mapping_t(mask_t(payee_regex), - context.top_account()->find_account(account_name))); + char * b = next_element(q); + string keyword(q); + if (keyword == "alias") + commodity_alias_directive(*commodity, b); + else if (keyword == "value") + commodity_value_directive(*commodity, b); + else if (keyword == "format") + commodity_format_directive(*commodity, b); + else if (keyword == "nomarket") + commodity_nomarket_directive(*commodity); + else if (keyword == "default") + commodity_default_directive(*commodity); + else if (keyword == "note") + commodity->set_note(string(b)); + } } } +void instance_t::commodity_alias_directive(commodity_t& comm, string alias) +{ + trim(alias); + commodity_pool_t::current_pool->alias(alias, comm); +} + +void instance_t::commodity_value_directive(commodity_t& comm, string expr_str) +{ + comm.set_value_expr(expr_t(expr_str)); +} + +void instance_t::commodity_format_directive(commodity_t&, string format) +{ + // jww (2012-02-27): A format specified this way should turn off + // observational formatting. + trim(format); + amount_t amt; + amt.parse(format); + VERIFY(amt.valid()); +} + +void instance_t::commodity_nomarket_directive(commodity_t& comm) +{ + comm.add_flags(COMMODITY_NOMARKET); +} + +void instance_t::commodity_default_directive(commodity_t& comm) +{ + commodity_pool_t::current_pool->default_commodity = &comm; +} + void instance_t::tag_directive(char * line) { - string tag(trim_ws(line)); + char * p = skip_ws(line); + context.journal->register_metadata(p, NULL_VALUE, 0); - if (tag.find(':') == string::npos) - tag = string(":") + tag + ":"; + while (peek_whitespace_line()) { + read_line(line); + char * q = skip_ws(line); + if (! *q) + break; - context.state_stack.push_front(tag); + char * b = next_element(q); + string keyword(q); + if (keyword == "assert" || keyword == "check") { + context.journal->tag_check_exprs.insert + (tag_check_exprs_map::value_type + (string(p), expr_t::check_expr_pair(expr_t(b), + keyword == "assert" ? + expr_t::EXPR_ASSERTION : + expr_t::EXPR_CHECK))); + } + } } -void instance_t::define_directive(char * line) +void instance_t::eval_directive(char * line) { - expr_t def(skip_ws(line)); - def.compile(context.scope); // causes definitions to be established + expr_t expr(line); + expr.calc(*context.scope); } void instance_t::assert_directive(char * line) { expr_t expr(line); - if (! expr.calc(context.scope).to_boolean()) + if (! expr.calc(*context.scope).to_boolean()) throw_(parse_error, _("Assertion failed: %1") << line); } void instance_t::check_directive(char * line) { expr_t expr(line); - if (! expr.calc(context.scope).to_boolean()) - warning_(_("Check failed: %1") << line); + if (! expr.calc(*context.scope).to_boolean()) + context.warning(STR(_("Check failed: %1") << line)); +} + +void instance_t::value_directive(char * line) +{ + context.journal->value_expr = expr_t(line); } void instance_t::comment_directive(char * line) @@ -936,12 +1168,71 @@ void instance_t::comment_directive(char * line) } } -void instance_t::expr_directive(char * line) +#if defined(HAVE_BOOST_PYTHON) + +void instance_t::import_directive(char * line) { - expr_t expr(line); - expr.calc(context.scope); + string module_name(line); + trim(module_name); + python_session->import_option(module_name); +} + +void instance_t::python_directive(char * line) +{ + std::ostringstream script; + + if (line) + script << skip_ws(line) << '\n'; + + std::size_t indent = 0; + + while (peek_whitespace_line() || peek_blank_line()) { + if (read_line(line) > 0) { + if (! indent) { + const char * p = line; + while (*p && std::isspace(*p)) { + ++indent; + ++p; + } + } + + const char * p = line; + for (std::size_t i = 0; i < indent; i++) { + if (std::isspace(*p)) + ++p; + else + break; + } + + if (*p) + script << p << '\n'; + } + } + + if (! python_session->is_initialized) + python_session->initialize(); + + python_session->main_module->define_global + ("journal", python::object(python::ptr(context.journal))); + python_session->eval(script.str(), python_interpreter_t::PY_EVAL_MULTI); +} + +#else + +void instance_t::import_directive(char *) +{ + throw_(parse_error, + _("'python' directive seen, but Python support is missing")); +} + +void instance_t::python_directive(char *) +{ + throw_(parse_error, + _("'import' directive seen, but Python support is missing")); } +#endif // HAVE_BOOST_PYTHON + bool instance_t::general_directive(char * line) { char buf[8192]; @@ -957,13 +1248,17 @@ bool instance_t::general_directive(char * line) switch (*p) { case 'a': if (std::strcmp(p, "account") == 0) { - master_account_directive(arg); + account_directive(arg); return true; } else if (std::strcmp(p, "alias") == 0) { alias_directive(arg); return true; } + else if (std::strcmp(p, "apply") == 0) { + apply_directive(arg); + return true; + } else if (std::strcmp(p, "assert") == 0) { assert_directive(arg); return true; @@ -978,11 +1273,7 @@ bool instance_t::general_directive(char * line) break; case 'c': - if (std::strcmp(p, "capture") == 0) { - account_mapping_directive(arg); - return true; - } - else if (std::strcmp(p, "check") == 0) { + if (std::strcmp(p, "check") == 0) { check_directive(arg); return true; } @@ -990,29 +1281,26 @@ bool instance_t::general_directive(char * line) comment_directive(arg); return true; } + else if (std::strcmp(p, "commodity") == 0) { + commodity_directive(arg); + return true; + } break; case 'd': if (std::strcmp(p, "def") == 0 || std::strcmp(p, "define") == 0) { - define_directive(arg); + eval_directive(arg); return true; } break; case 'e': if (std::strcmp(p, "end") == 0) { - end_directive(arg); + end_apply_directive(arg); return true; } - else if (std::strcmp(p, "expr") == 0) { - expr_directive(arg); - return true; - } - break; - - case 'f': - if (std::strcmp(p, "fixed") == 0) { - fixed_directive(arg); + else if (std::strcmp(p, "expr") == 0 || std::strcmp(p, "eval") == 0) { + eval_directive(arg); return true; } break; @@ -1022,11 +1310,19 @@ bool instance_t::general_directive(char * line) include_directive(arg); return true; } + else if (std::strcmp(p, "import") == 0) { + import_directive(arg); + return true; + } break; case 'p': if (std::strcmp(p, "payee") == 0) { - payee_mapping_directive(arg); + payee_directive(arg); + return true; + } + else if (std::strcmp(p, "python") == 0) { + python_directive(arg); return true; } break; @@ -1042,9 +1338,9 @@ bool instance_t::general_directive(char * line) } break; - case 'y': - if (std::strcmp(p, "year") == 0) { - year_directive(arg); + case 'v': + if (std::strcmp(p, "value") == 0) { + value_directive(arg); return true; } break; @@ -1068,16 +1364,16 @@ post_t * instance_t::parse_post(char * line, { TRACE_START(post_details, 1, "Time spent parsing postings:"); - std::auto_ptr<post_t> post(new post_t); + unique_ptr<post_t> post(new post_t); post->xact = xact; // this could be NULL post->pos = position_t(); - post->pos->pathname = pathname; - post->pos->beg_pos = line_beg_pos; - post->pos->beg_line = linenum; + post->pos->pathname = context.pathname; + post->pos->beg_pos = context.line_beg_pos; + post->pos->beg_line = context.linenum; post->pos->sequence = context.sequence++; - char buf[MAX_LINE + 1]; + char buf[parse_context_t::MAX_LINE + 1]; std::strcpy(buf, line); std::streamsize beg = 0; @@ -1094,14 +1390,14 @@ post_t * instance_t::parse_post(char * line, case '*': post->set_state(item_t::CLEARED); p = skip_ws(p + 1); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Parsed the CLEARED flag"); break; case '!': post->set_state(item_t::PENDING); p = skip_ws(p + 1); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Parsed the PENDING flag"); break; } @@ -1124,44 +1420,23 @@ post_t * instance_t::parse_post(char * line, if ((*p == '[' && *(e - 1) == ']') || (*p == '(' && *(e - 1) == ')')) { post->add_flags(POST_VIRTUAL); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Parsed a virtual account name"); if (*p == '[') { post->add_flags(POST_MUST_BALANCE); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Posting must balance"); } p++; e--; } - string name(p, static_cast<std::string::size_type>(e - p)); - DEBUG("textual.parse", "line " << linenum << ": " + string name(p, static_cast<string::size_type>(e - p)); + DEBUG("textual.parse", "line " << context.linenum << ": " << "Parsed account name " << name); - if (account_aliases.size() > 0) { - accounts_map::const_iterator i = account_aliases.find(name); - if (i != account_aliases.end()) - post->account = (*i).second; - } - if (! post->account) - post->account = account->find_account(name); - - if (context.strict && ! post->account->has_flags(ACCOUNT_KNOWN)) { - if (post->_state == item_t::UNCLEARED) - warning_(_("\"%1\", line %2: Unknown account '%3'") - << pathname.string() << linenum << post->account->fullname()); - post->account->add_flags(ACCOUNT_KNOWN); - } - - if (post->account->name == _("Unknown")) { - foreach (account_mapping_t& value, context.journal.account_mappings) { - if (value.first.match(xact->payee)) { - post->account = value.second; - break; - } - } - } + post->account = + context.journal->register_account(name, post.get(), account); // Parse the optional amount @@ -1172,35 +1447,28 @@ post_t * instance_t::parse_post(char * line, if (*next != '(') // indicates a value expression post->amount.parse(stream, PARSE_NO_REDUCE); else - parse_amount_expr(stream, context.scope, *post.get(), post->amount, + parse_amount_expr(stream, *context.scope, *post.get(), post->amount, PARSE_NO_REDUCE | PARSE_SINGLE | PARSE_NO_ASSIGN, defer_expr, &post->amount_expr); if (! post->amount.is_null() && post->amount.has_commodity()) { - if (context.strict && - ! post->amount.commodity().has_flags(COMMODITY_KNOWN)) { - if (post->_state == item_t::UNCLEARED) - warning_(_("\"%1\", line %2: Unknown commodity '%3'") - << pathname.string() << linenum << post->amount.commodity()); - post->amount.commodity().add_flags(COMMODITY_KNOWN); - } + context.journal->register_commodity(post->amount.commodity(), post.get()); if (! post->amount.has_annotation()) { - foreach (state_t& state, context.state_stack) { - if (state.type() == typeid(fixed_rate_t)) { - fixed_rate_t& rate(boost::get<fixed_rate_t>(state)); - if (*rate.first == post->amount.commodity()) { - annotation_t details(rate.second); - details.add_flags(ANNOTATION_PRICE_FIXATED); - post->amount.annotate(details); - break; - } + std::vector<fixed_rate_t> rates; + get_applications<fixed_rate_t>(rates); + foreach (fixed_rate_t& rate, rates) { + if (*rate.first == post->amount.commodity()) { + annotation_t details(rate.second); + details.add_flags(ANNOTATION_PRICE_FIXATED); + post->amount.annotate(details); + break; } } } } - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "post amount = " << post->amount); if (stream.eof()) { @@ -1210,19 +1478,26 @@ post_t * instance_t::parse_post(char * line, // Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST) - if (*next == '@') { - DEBUG("textual.parse", "line " << linenum << ": " + if (*next == '@' || (*next == '(' && *(next + 1) == '@')) { + DEBUG("textual.parse", "line " << context.linenum << ": " << "Found a price indicator"); - bool per_unit = true; + if (*next == '(') { + post->add_flags(POST_COST_VIRTUAL); + ++next; + } + bool per_unit = true; if (*++next == '@') { per_unit = false; post->add_flags(POST_COST_IN_FULL); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "And it's for a total price"); } + if (post->has_flags(POST_COST_VIRTUAL) && *(next + 1) == ')') + ++next; + beg = static_cast<std::streamsize>(++next - line); p = skip_ws(next); @@ -1243,7 +1518,7 @@ post_t * instance_t::parse_post(char * line, if (*p != '(') // indicates a value expression post->cost->parse(cstream, PARSE_NO_MIGRATE); else - parse_amount_expr(cstream, context.scope, *post.get(), *post->cost, + parse_amount_expr(cstream, *context.scope, *post.get(), *post->cost, PARSE_NO_MIGRATE | PARSE_SINGLE | PARSE_NO_ASSIGN); if (post->cost->sign() < 0) @@ -1266,9 +1541,9 @@ post_t * instance_t::parse_post(char * line, if (fixed_cost) post->add_flags(POST_COST_FIXATED); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Total cost is " << *post->cost); - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Annotated amount is " << post->amount); if (cstream.eof()) @@ -1285,7 +1560,7 @@ post_t * instance_t::parse_post(char * line, // Parse the optional balance assignment if (xact && next && *next == '=') { - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Found a balance assignment indicator"); beg = static_cast<std::streamsize>(++next - line); @@ -1300,7 +1575,7 @@ post_t * instance_t::parse_post(char * line, if (*p != '(') // indicates a value expression post->assigned_amount->parse(stream, PARSE_NO_MIGRATE); else - parse_amount_expr(stream, context.scope, *post.get(), + parse_amount_expr(stream, *context.scope, *post.get(), *post->assigned_amount, PARSE_SINGLE | PARSE_NO_MIGRATE); @@ -1311,17 +1586,17 @@ post_t * instance_t::parse_post(char * line, throw parse_error(_("Balance assertion must evaluate to a constant")); } - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "POST assign: parsed amt = " << *post->assigned_amount); amount_t& amt(*post->assigned_amount); value_t account_total (post->account->amount().strip_annotations(keep_details_t())); + DEBUG("post.assign", "line " << context.linenum << ": " + << "account balance = " << account_total); DEBUG("post.assign", - "line " << linenum << ": " "account balance = " << account_total); - DEBUG("post.assign", - "line " << linenum << ": " "post amount = " << amt); + "line " << context.linenum << ": " << "post amount = " << amt); amount_t diff = amt; @@ -1341,9 +1616,9 @@ post_t * instance_t::parse_post(char * line, } DEBUG("post.assign", - "line " << linenum << ": " << "diff = " << diff); - DEBUG("textual.parse", - "line " << linenum << ": " << "POST assign: diff = " << diff); + "line " << context.linenum << ": " << "diff = " << diff); + DEBUG("textual.parse", "line " << context.linenum << ": " + << "POST assign: diff = " << diff); if (! diff.is_zero()) { if (! post->amount.is_null()) { @@ -1352,7 +1627,7 @@ post_t * instance_t::parse_post(char * line, throw_(parse_error, _("Balance assertion off by %1") << diff); } else { post->amount = diff; - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Overwrite null posting"); } } @@ -1369,9 +1644,9 @@ post_t * instance_t::parse_post(char * line, // Parse the optional note if (next && *next == ';') { - post->append_note(++next, context.scope, true); + post->append_note(++next, *context.scope, true); next = line + len; - DEBUG("textual.parse", "line " << linenum << ": " + DEBUG("textual.parse", "line " << context.linenum << ": " << "Parsed a posting note"); } @@ -1382,14 +1657,13 @@ post_t * instance_t::parse_post(char * line, _("Unexpected char '%1' (Note: inline math requires parentheses)") << *next); - post->pos->end_pos = curr_pos; - post->pos->end_line = linenum; + post->pos->end_pos = context.curr_pos; + post->pos->end_line = context.linenum; - if (! context.state_stack.empty()) { - foreach (const state_t& state, context.state_stack) - if (state.type() == typeid(string)) - post->parse_tags(boost::get<string>(state).c_str(), context.scope, true); - } + std::vector<string> tags; + get_applications<string>(tags); + foreach (string& tag, tags) + post->parse_tags(tag.c_str(), *context.scope, true); TRACE_STOP(post_details, 1); @@ -1398,8 +1672,8 @@ post_t * instance_t::parse_post(char * line, } catch (const std::exception&) { add_error_context(_("While parsing posting:")); - add_error_context(line_context(buf, static_cast<std::string::size_type>(beg), - static_cast<std::string::size_type>(len))); + add_error_context(line_context(buf, static_cast<string::size_type>(beg), + static_cast<string::size_type>(len))); throw; } } @@ -1415,11 +1689,12 @@ bool instance_t::parse_posts(account_t * account, while (peek_whitespace_line()) { char * line; std::streamsize len = read_line(line); - assert(len > 0); - - if (post_t * post = parse_post(line, len, account, NULL, defer_expr)) { - xact.add_post(post); - added = true; + char * p = skip_ws(line); + if (*p != ';') { + if (post_t * post = parse_post(line, len, account, NULL, defer_expr)) { + xact.add_post(post); + added = true; + } } } @@ -1437,9 +1712,9 @@ xact_t * instance_t::parse_xact(char * line, unique_ptr<xact_t> xact(new xact_t); xact->pos = position_t(); - xact->pos->pathname = pathname; - xact->pos->beg_pos = line_beg_pos; - xact->pos->beg_line = linenum; + xact->pos->pathname = context.pathname; + xact->pos->beg_pos = context.line_beg_pos; + xact->pos->beg_line = context.linenum; xact->pos->sequence = context.sequence++; bool reveal_context = true; @@ -1452,7 +1727,7 @@ xact_t * instance_t::parse_xact(char * line, if (char * p = std::strchr(line, '=')) { *p++ = '\0'; - xact->_date_eff = parse_date(p); + xact->_date_aux = parse_date(p); } xact->_date = parse_date(line); @@ -1484,15 +1759,31 @@ xact_t * instance_t::parse_xact(char * line, // Parse the description text if (next && *next) { - char * p = next_element(next, true); - foreach (payee_mapping_t& value, context.journal.payee_mappings) { - if (value.first.match(next)) { - xact->payee = value.second; + char * p = next; + std::size_t spaces = 0; + std::size_t tabs = 0; + while (*p) { + if (*p == ' ') { + ++spaces; + } + else if (*p == '\t') { + ++tabs; + } + else if (*p == ';' && (tabs > 0 || spaces > 1)) { + char *q = p - 1; + while (q > next && std::isspace(*q)) + --q; + if (q > next) + *(q + 1) = '\0'; break; } + else { + spaces = 0; + tabs = 0; + } + ++p; } - if (xact->payee.empty()) - xact->payee = next; + xact->payee = context.journal->register_payee(next, xact.get()); next = p; } else { xact->payee = _("<Unspecified payee>"); @@ -1501,7 +1792,7 @@ xact_t * instance_t::parse_xact(char * line, // Parse the xact note if (next && *next == ';') - xact->append_note(++next, context.scope, false); + xact->append_note(++next, *context.scope, false); TRACE_STOP(xact_text, 1); @@ -1513,7 +1804,6 @@ xact_t * instance_t::parse_xact(char * line, while (peek_whitespace_line()) { len = read_line(line); - char * p = skip_ws(line); if (! *p) break; @@ -1528,8 +1818,9 @@ xact_t * instance_t::parse_xact(char * line, if (*p == ';') { // This is a trailing note, and possibly a metadata info tag - item->append_note(p + 1, context.scope, true); - item->pos->end_pos = curr_pos; + item->append_note(p + 1, *context.scope, true); + item->add_flags(ITEM_NOTE_ON_NEXT_LINE); + item->pos->end_pos = context.curr_pos; item->pos->end_line++; } else if ((remlen > 7 && *p == 'a' && @@ -1541,7 +1832,7 @@ xact_t * instance_t::parse_xact(char * line, const char c = *p; p = skip_ws(&p[*p == 'a' ? 6 : (*p == 'c' ? 5 : 4)]); expr_t expr(p); - bind_scope_t bound_scope(context.scope, *item); + bind_scope_t bound_scope(*context.scope, *item); if (c == 'e') { expr.calc(bound_scope); } @@ -1549,7 +1840,7 @@ xact_t * instance_t::parse_xact(char * line, if (c == 'a') { throw_(parse_error, _("Transaction assertion failed: %1") << p); } else { - warning_(_("Transaction check failed: %1") << p); + context.warning(STR(_("Transaction check failed: %1") << p)); } } } @@ -1568,7 +1859,7 @@ xact_t * instance_t::parse_xact(char * line, #if 0 if (xact->_state == item_t::UNCLEARED) { - item_t::state_t result = item_t::CLEARED; + item_t::application_t result = item_t::CLEARED; foreach (post_t * post, xact->posts) { if (post->_state == item_t::UNCLEARED) { @@ -1582,15 +1873,13 @@ xact_t * instance_t::parse_xact(char * line, } #endif - xact->pos->end_pos = curr_pos; - xact->pos->end_line = linenum; + xact->pos->end_pos = context.curr_pos; + xact->pos->end_line = context.linenum; - if (! context.state_stack.empty()) { - foreach (const state_t& state, context.state_stack) - if (state.type() == typeid(string)) - xact->parse_tags(boost::get<string>(state).c_str(), context.scope, - false); - } + std::vector<string> tags; + get_applications<string>(tags); + foreach (string& tag, tags) + xact->parse_tags(tag.c_str(), *context.scope, false); TRACE_STOP(xact_details, 1); @@ -1601,7 +1890,8 @@ xact_t * instance_t::parse_xact(char * line, if (reveal_context) { add_error_context(_("While parsing transaction:")); add_error_context(source_context(xact->pos->pathname, - xact->pos->beg_pos, curr_pos, "> ")); + xact->pos->beg_pos, + context.curr_pos, "> ")); } throw; } @@ -1610,27 +1900,18 @@ xact_t * instance_t::parse_xact(char * line, expr_t::ptr_op_t instance_t::lookup(const symbol_t::kind_t kind, const string& name) { - return context.scope.lookup(kind, name); + return context.scope->lookup(kind, name); } -std::size_t journal_t::parse(std::istream& in, - scope_t& scope, - account_t * master_account, - const path * original_file, - bool strict) +std::size_t journal_t::read_textual(parse_context_stack_t& context_stack) { TRACE_START(parsing_total, 1, "Total time spent parsing text:"); - - parse_context_t context(*this, scope); - context.strict = strict; - if (master_account || this->master) - context.state_stack.push_front(master_account ? - master_account : this->master); - - instance_t instance(context, in, original_file); - instance.parse(); - context.close(); - + { + instance_t instance(context_stack, context_stack.get_current()); + instance.apply_stack.push_front + (application_t("account", context_stack.get_current().master)); + instance.parse(); + } TRACE_STOP(parsing_total, 1); // These tracers were started in textual.cc @@ -1641,10 +1922,10 @@ std::size_t journal_t::parse(std::istream& in, TRACE_FINISH(instance_parse, 1); // report per-instance timers TRACE_FINISH(parsing_total, 1); - if (context.errors > 0) - throw static_cast<int>(context.errors); + if (context_stack.get_current().errors > 0) + throw error_count(context_stack.get_current().errors); - return context.count; + return context_stack.get_current().count; } } // namespace ledger diff --git a/src/timelog.cc b/src/timelog.cc index ee9a0b6c..e84e4188 100644 --- a/src/timelog.cc +++ b/src/timelog.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -36,14 +36,48 @@ #include "post.h" #include "account.h" #include "journal.h" +#include "context.h" namespace ledger { namespace { - void clock_out_from_timelog(std::list<time_xact_t>& time_xacts, - time_xact_t out_event, - journal_t& journal, - scope_t& scope) + void create_timelog_xact(const time_xact_t& in_event, + const time_xact_t& out_event, + parse_context_t& context) + { + unique_ptr<xact_t> curr(new xact_t); + curr->_date = in_event.checkin.date(); + curr->code = out_event.desc; // if it wasn't used above + curr->payee = in_event.desc; + curr->pos = in_event.position; + + if (! in_event.note.empty()) + curr->append_note(in_event.note.c_str(), *context.scope); + + char buf[32]; + std::sprintf(buf, "%lds", long((out_event.checkin - in_event.checkin) + .total_seconds())); + amount_t amt; + amt.parse(buf); + VERIFY(amt.valid()); + + post_t * post = new post_t(in_event.account, amt, POST_VIRTUAL); + post->set_state(item_t::CLEARED); + post->pos = in_event.position; + post->checkin = in_event.checkin; + post->checkout = out_event.checkin; + curr->add_post(post); + in_event.account->add_post(post); + + if (! context.journal->add_xact(curr.get())) + throw parse_error(_("Failed to record 'out' timelog transaction")); + else + curr.release(); + } + + std::size_t clock_out_from_timelog(std::list<time_xact_t>& time_xacts, + time_xact_t out_event, + parse_context_t& context) { time_xact_t event; @@ -76,6 +110,11 @@ namespace { (_("Timelog check-out event does not match any current check-ins")); } + if (event.checkin.is_not_a_date_time()) + throw parse_error(_("Timelog check-in has no corresponding check-out")); + if (out_event.checkin.is_not_a_date_time()) + throw parse_error(_("Timelog check-out has no corresponding check-in")); + if (out_event.checkin < event.checkin) throw parse_error (_("Timelog check-out date less than corresponding check-in")); @@ -88,32 +127,35 @@ namespace { if (! out_event.note.empty() && event.note.empty()) event.note = out_event.note; - std::auto_ptr<xact_t> curr(new xact_t); - curr->_date = out_event.checkin.date(); - curr->code = out_event.desc; // if it wasn't used above - curr->payee = event.desc; - curr->pos = event.position; - - if (! event.note.empty()) - curr->append_note(event.note.c_str(), scope); - - char buf[32]; - std::sprintf(buf, "%lds", long((out_event.checkin - event.checkin) - .total_seconds())); - amount_t amt; - amt.parse(buf); - VERIFY(amt.valid()); - - post_t * post = new post_t(event.account, amt, POST_VIRTUAL); - post->set_state(item_t::CLEARED); - post->pos = event.position; - curr->add_post(post); - event.account->add_post(post); - - if (! journal.add_xact(curr.get())) - throw parse_error(_("Failed to record 'out' timelog transaction")); - else - curr.release(); + if (! context.journal->day_break) { + create_timelog_xact(event, out_event, context); + return 1; + } else { + time_xact_t begin(event); + std::size_t xact_count = 0; + + while (begin.checkin < out_event.checkin) { + DEBUG("timelog", "begin.checkin: " << begin.checkin); + datetime_t days_end(begin.checkin.date(), time_duration_t(23, 59, 59)); + days_end += seconds(1); + DEBUG("timelog", "days_end: " << days_end); + + if (out_event.checkin <= days_end) { + create_timelog_xact(begin, out_event, context); + ++xact_count; + break; + } else { + time_xact_t end(out_event); + end.checkin = days_end; + DEBUG("timelog", "end.checkin: " << end.checkin); + create_timelog_xact(begin, end, context); + ++xact_count; + + begin.checkin = end.checkin; + } + } + return xact_count; + } } } // unnamed namespace @@ -129,9 +171,8 @@ void time_log_t::close() DEBUG("timelog", "Clocking out from account " << account->fullname()); clock_out_from_timelog(time_xacts, time_xact_t(none, CURRENT_TIME(), account), - journal, scope); - if (context_count) - (*context_count)++; + context); + context.count++; } assert(time_xacts.empty()); } @@ -149,12 +190,12 @@ void time_log_t::clock_in(time_xact_t event) time_xacts.push_back(event); } -void time_log_t::clock_out(time_xact_t event) +std::size_t time_log_t::clock_out(time_xact_t event) { if (time_xacts.empty()) throw std::logic_error(_("Timelog check-out event without a check-in")); - clock_out_from_timelog(time_xacts, event, journal, scope); + return clock_out_from_timelog(time_xacts, event, context); } } // namespace ledger diff --git a/src/timelog.h b/src/timelog.h index 020ae4f2..857952ff 100644 --- a/src/timelog.h +++ b/src/timelog.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -50,6 +50,7 @@ namespace ledger { class account_t; class journal_t; +class parse_context_t; class time_xact_t { @@ -86,22 +87,18 @@ public: class time_log_t : public boost::noncopyable { std::list<time_xact_t> time_xacts; - journal_t& journal; - scope_t& scope; + parse_context_t& context; public: - std::size_t * context_count; - - time_log_t(journal_t& _journal, scope_t& _scope) - : journal(_journal), scope(_scope), context_count(NULL) { - TRACE_CTOR(time_log_t, "journal_t&, scope_t&, std::size&"); + time_log_t(parse_context_t& _context) : context(_context) { + TRACE_CTOR(time_log_t, "parse_context_t&"); } ~time_log_t() { TRACE_DTOR(time_log_t); } void clock_in(time_xact_t event); - void clock_out(time_xact_t event); + std::size_t clock_out(time_xact_t event); void close(); }; diff --git a/src/times.cc b/src/times.cc index 0384edf6..3c556a47 100644 --- a/src/times.cc +++ b/src/times.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -173,6 +173,7 @@ namespace { #else // USE_BOOST_FACETS std::tm data; std::memset(&data, 0, sizeof(std::tm)); + data.tm_year = CURRENT_DATE().year() - 1900; data.tm_mday = 1; // some formats have no day if (strptime(str, fmt_str, &data)) return gregorian::date_from_tm(data); @@ -196,6 +197,8 @@ namespace { std::deque<shared_ptr<date_io_t> > readers; + bool convert_separators_to_slashes = true; + date_t parse_date_mask_routine(const char * date_str, date_io_t& io, date_traits_t * traits = NULL) { @@ -204,9 +207,11 @@ namespace { char buf[128]; std::strcpy(buf, date_str); - for (char * p = buf; *p; p++) - if (*p == '.' || *p == '-') - *p = '/'; + if (convert_separators_to_slashes) { + for (char * p = buf; *p; p++) + if (*p == '.' || *p == '-') + *p = '/'; + } date_t when = io.parse(buf); @@ -1305,7 +1310,7 @@ void date_interval_t::stabilize(const optional<date_t>& date) date_interval_t next_interval(*this); ++next_interval; - if (next_interval.start && *next_interval.start < *date) { + if (next_interval.start && *next_interval.start <= *date) { *this = next_interval; } else { end_of_duration = none; @@ -1355,7 +1360,8 @@ void date_interval_t::stabilize(const optional<date_t>& date) } } -bool date_interval_t::find_period(const date_t& date) +bool date_interval_t::find_period(const date_t& date, + const bool allow_shift) { stabilize(date); @@ -1397,6 +1403,12 @@ bool date_interval_t::find_period(const date_t& date) DEBUG("times.interval", "date = " << date); DEBUG("times.interval", "scan = " << scan); DEBUG("times.interval", "end_of_scan = " << end_of_scan); +#if defined(DEBUG_ON) + if (finish) + DEBUG("times.interval", "finish = " << *finish); + else + DEBUG("times.interval", "finish is not set"); +#endif while (date >= scan && (! finish || scan < *finish)) { if (date < end_of_scan) { @@ -1411,11 +1423,19 @@ bool date_interval_t::find_period(const date_t& date) return true; } + else if (! allow_shift) { + break; + } scan = duration->add(scan); end_of_scan = duration->add(scan); + + DEBUG("times.interval", "scan = " << scan); + DEBUG("times.interval", "end_of_scan = " << end_of_scan); } + DEBUG("times.interval", "false: failed scan"); + return false; } @@ -1759,6 +1779,7 @@ void set_date_format(const char * format) void set_input_date_format(const char * format) { readers.push_front(shared_ptr<date_io_t>(new date_io_t(format, true))); + convert_separators_to_slashes = false; } void times_initialize() diff --git a/src/times.h b/src/times.h index 39945824..3bb95903 100644 --- a/src/times.h +++ b/src/times.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -218,6 +218,9 @@ struct date_duration_t case YEARS: return date + gregorian::years(length); } +#if !defined(__clang__) + return date_t(); +#endif } date_t subtract(const date_t& date) const { @@ -233,6 +236,9 @@ struct date_duration_t case YEARS: return date - gregorian::years(length); } +#if !defined(__clang__) + return date_t(); +#endif } string to_string() const { @@ -301,13 +307,14 @@ public: } date_specifier_t(const date_t& date, const optional<date_traits_t>& traits = none) { - TRACE_CTOR(date_specifier_t, "date_t, date_traits_t"); if (! traits || traits->has_year) year = date.year(); if (! traits || traits->has_month) month = date.month(); if (! traits || traits->has_day) day = date.day(); + + TRACE_CTOR(date_specifier_t, "date_t, date_traits_t"); } date_specifier_t(const date_specifier_t& other) : year(other.year), month(other.month), @@ -532,8 +539,8 @@ public: TRACE_CTOR(date_interval_t, ""); } date_interval_t(const string& str) : aligned(false) { - TRACE_CTOR(date_interval_t, "const string&"); parse(str); + TRACE_CTOR(date_interval_t, "const string&"); } date_interval_t(const date_interval_t& other) : range(other.range), @@ -553,6 +560,10 @@ public: return (start == other.start && (! start || *start == *other.start)); } + bool operator<(const date_interval_t& other) const { + return (start == other.start && + (! start || *start < *other.start)); + } operator bool() const { return is_valid(); @@ -574,10 +585,14 @@ public: return start; } - /** Find the current or next period containing date. Returns true if the - date_interval_t object has been altered to reflect the interval - containing date, or false if no such period can be found. */ - bool find_period(const date_t& date = CURRENT_DATE()); + /** Find the current or next period containing date. Returns false if + no such period can be found. If allow_shift is true, the default, + then the interval may be shifted in time to find the period. */ + bool find_period(const date_t& date = CURRENT_DATE(), + const bool allow_shift = true); + bool within_period(const date_t& date = CURRENT_DATE()) { + return find_period(date, false); + } optional<date_t> inclusive_end() const { if (end_of_duration) diff --git a/src/token.cc b/src/token.cc index 77092d49..e5d6b218 100644 --- a/src/token.cc +++ b/src/token.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -415,16 +415,13 @@ void expr_t::token_t::next(std::istream& in, const parse_flags_t& pflags) try { amount_t temp; if (! temp.parse(in, parse_flags.plus_flags(PARSE_SOFT_FAIL))) { - // If the amount had no commodity, it must be an unambiguous - // variable reference - in.clear(); in.seekg(pos, std::ios::beg); if (in.fail()) throw_(parse_error, _("Failed to reset input stream")); c = static_cast<char>(in.peek()); - if (! std::isalpha(c)) + if (! std::isalpha(c) && c != '_') expected('\0', c); parse_ident(in); diff --git a/src/token.h b/src/token.h index cbdf1258..01ff7ee9 100644 --- a/src/token.h +++ b/src/token.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/src/unistring.h b/src/unistring.h index 4be36b0d..b2278796 100644 --- a/src/unistring.h +++ b/src/unistring.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -64,14 +64,14 @@ public: } unistring(const std::string& input) { - TRACE_CTOR(unistring, "std::string"); - const char * p = input.c_str(); std::size_t len = input.length(); assert(len < 1024); VERIFY(utf8::is_valid(p, p + len)); utf8::unchecked::utf8to32(p, p + len, std::back_inserter(utf32chars)); + + TRACE_CTOR(unistring, "std::string"); } ~unistring() { TRACE_DTOR(unistring); diff --git a/src/utils.cc b/src/utils.cc index 42600db3..17118904 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -50,8 +50,8 @@ void debug_assert(const string& reason, std::size_t line) { std::ostringstream buf; - buf << "Assertion failed in \"" << file << "\", line " << line - << ": " << func << ": " << reason; + buf << "Assertion failed in " << file_context(file, line) + << func << ": " << reason; throw assertion_failed(buf.str()); } @@ -270,13 +270,81 @@ void operator delete[](void * ptr, const std::nothrow_t&) throw() { namespace ledger { -inline void report_count_map(std::ostream& out, object_count_map& the_map) -{ - foreach (object_count_map::value_type& pair, the_map) - out << " " << std::right << std::setw(12) << pair.second.first - << " " << std::right << std::setw(7) << pair.second.second - << " " << std::left << pair.first - << std::endl; +namespace { + void stream_commified_number(std::ostream& out, std::size_t num) + { + std::ostringstream buf; + std::ostringstream obuf; + + buf << num; + + string number(buf.str()); + + int integer_digits = 0; + // Count the number of integer digits + for (const char * p = number.c_str(); *p; p++) { + if (*p == '.') + break; + else if (*p != '-') + integer_digits++; + } + + for (const char * p = number.c_str(); *p; p++) { + if (*p == '.') { + obuf << *p; + assert(integer_digits <= 3); + } + else if (*p == '-') { + obuf << *p; + } + else { + obuf << *p; + + if (integer_digits > 3 && --integer_digits % 3 == 0) + obuf << ','; + } + } + + out << obuf.str(); + } + + void stream_memory_size(std::ostream& out, std::size_t size) + { + std::ostringstream obuf; + + if (size > 10 * 1024 * 1024) + obuf << "\033[1m"; + if (size > 100 * 1024 * 1024) + obuf << "\033[31m"; + + obuf << std::setw(7); + + if (size < 1024) + obuf << size << 'b'; + else if (size < (1024 * 1024)) + obuf << int(double(size) / 1024.0) << 'K'; + else if (size < (1024 * 1024 * 1024)) + obuf << int(double(size) / (1024.0 * 1024.0)) << 'M'; + else + obuf << int(double(size) / (1024.0 * 1024.0 * 1024.0)) << 'G'; + + if (size > 10 * 1024 * 1024) + obuf << "\033[0m"; + + out << obuf.str(); + } + + void report_count_map(std::ostream& out, object_count_map& the_map) + { + foreach (object_count_map::value_type& pair, the_map) { + out << " " << std::right << std::setw(18); + stream_commified_number(out, pair.second.first); + out << " " << std::right << std::setw(7); + stream_memory_size(out, pair.second.second); + out << " " << std::left << pair.first + << std::endl; + } + } } std::size_t current_objects_size() @@ -354,7 +422,7 @@ void trace_dtor_func(void * ptr, const char * cls_name, std::size_t cls_size) void report_memory(std::ostream& out, bool report_all) { - if (! live_memory || ! memory_tracing_active) return; + if (! live_memory) return; if (live_memory_count->size() > 0) { out << "NOTE: There may be memory held by Boost " @@ -366,11 +434,13 @@ void report_memory(std::ostream& out, bool report_all) if (live_memory->size() > 0) { out << "Live memory:" << std::endl; - foreach (const memory_map::value_type& pair, *live_memory) - out << " " << std::right << std::setw(12) << pair.first - << " " << std::right << std::setw(7) << pair.second.second - << " " << std::left << pair.second.first + foreach (const memory_map::value_type& pair, *live_memory) { + out << " " << std::right << std::setw(18) << pair.first + << " " << std::right << std::setw(7); + stream_memory_size(out, pair.second.second); + out << " " << std::left << pair.second.first << std::endl; + } } if (report_all && total_memory_count->size() > 0) { @@ -386,11 +456,13 @@ void report_memory(std::ostream& out, bool report_all) if (live_objects->size() > 0) { out << "Live objects:" << std::endl; - foreach (const objects_map::value_type& pair, *live_objects) - out << " " << std::right << std::setw(12) << pair.first - << " " << std::right << std::setw(7) << pair.second.second - << " " << std::left << pair.second.first + foreach (const objects_map::value_type& pair, *live_objects) { + out << " " << std::right << std::setw(18) << pair.first + << " " << std::right << std::setw(7); + stream_memory_size(out, pair.second.second); + out << " " << std::left << pair.second.first << std::endl; + } } if (report_all) { @@ -529,18 +601,6 @@ std::ostringstream _log_buffer; uint8_t _trace_level; #endif -static inline void stream_memory_size(std::ostream& out, std::size_t size) -{ - if (size < 1024) - out << size << 'b'; - else if (size < (1024 * 1024)) - out << (double(size) / 1024.0) << 'K'; - else if (size < (1024 * 1024 * 1024)) - out << (double(size) / (1024.0 * 1024.0)) << 'M'; - else - out << (double(size) / (1024.0 * 1024.0 * 1024.0)) << 'G'; -} - static bool logger_has_run = false; static ptime logger_start; @@ -553,9 +613,6 @@ void logger_func(log_level_t level) #if defined(VERIFY_ON) IF_VERIFY() *_log_stream << " TIME OBJSZ MEMSZ" << std::endl; -#else - IF_VERIFY() - *_log_stream << " TIME" << std::endl; #endif } @@ -603,12 +660,16 @@ void logger_func(log_level_t level) namespace ledger { -optional<std::string> _log_category; +optional<std::string> _log_category; +#if defined(HAVE_BOOST_REGEX_UNICODE) +optional<boost::u32regex> _log_category_re; +#else +optional<boost::regex> _log_category_re; +#endif struct __maybe_enable_debugging { __maybe_enable_debugging() { - const char * p = std::getenv("LEDGER_DEBUG"); - if (p != NULL) { + if (const char * p = std::getenv("LEDGER_DEBUG")) { _log_level = LOG_DEBUG; _log_category = p; } diff --git a/src/utils.h b/src/utils.h index e1a03d79..8f11f75a 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -171,7 +171,7 @@ void report_memory(std::ostream& out, bool report_all = false); #else // ! VERIFY_ON #define VERIFY(x) -#define DO_VERIFY() true +#define DO_VERIFY() false #define TRACE_CTOR(cls, args) #define TRACE_DTOR(cls) @@ -278,6 +278,28 @@ extern string empty_string; strings_list split_arguments(const char * line); +inline string to_string(long num) { + std::ostringstream buf; + buf << num; + return buf.str(); +} + +inline string to_string(std::size_t num) { + std::ostringstream buf; + buf << num; + return buf.str(); +} + +inline string lowered(const string& str) { + string tmp(str); + to_lower(tmp); + return tmp; +} + +inline string operator+(const char * left, const string& right) { + return string(left) + right; +} + } // namespace ledger /*@}*/ @@ -338,10 +360,32 @@ extern uint8_t _trace_level; #if defined(DEBUG_ON) -extern optional<std::string> _log_category; +extern optional<std::string> _log_category; +#if defined(HAVE_BOOST_REGEX_UNICODE) + extern optional<boost::u32regex> _log_category_re; +#else + extern optional<boost::regex> _log_category_re; +#endif inline bool category_matches(const char * cat) { - return _log_category && starts_with(cat, *_log_category); + if (_log_category) { + if (! _log_category_re) { + _log_category_re = +#if defined(HAVE_BOOST_REGEX_UNICODE) + boost::make_u32regex(_log_category->c_str(), + boost::regex::perl | boost::regex::icase); +#else + boost::regex(_log_category->c_str(), + boost::regex::perl | boost::regex::icase); +#endif + } +#if defined(HAVE_BOOST_REGEX_UNICODE) + return boost::u32regex_search(cat, *_log_category_re); +#else + return boost::regex_search(cat, *_log_category_re); +#endif + } + return false; } #define SHOW_DEBUG(cat) \ diff --git a/src/value.cc b/src/value.cc index c62e6f32..2b1b561f 100644 --- a/src/value.cc +++ b/src/value.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -724,7 +724,7 @@ value_t& value_t::operator/=(const value_t& val) return *this; case AMOUNT: if (as_balance().single_amount()) { - in_place_simplify(); + in_place_cast(AMOUNT); as_amount_lval() /= val.as_amount(); return *this; } @@ -742,7 +742,7 @@ value_t& value_t::operator/=(const value_t& val) break; } - add_error_context(_("While dividing %1 by %2:") << val << *this); + add_error_context(_("While dividing %1 by %2:") << *this << val); throw_(value_error, _("Cannot divide %1 by %2") << label() << val.label()); return *this; @@ -869,7 +869,7 @@ bool value_t::is_less_than(const value_t& val) const case INTEGER: return as_long() < val.as_long(); case AMOUNT: - return val.as_amount() >= as_long(); + return val.as_amount() > as_long(); default: break; } @@ -948,8 +948,7 @@ bool value_t::is_less_than(const value_t& val) const break; } - add_error_context(_("While comparing if %1 is less than %2:") - << *this << val); + add_error_context(_("While comparing if %1 is less than %2:") << *this << val); throw_(value_error, _("Cannot compare %1 to %2") << label() << val.label()); return *this; @@ -990,7 +989,7 @@ bool value_t::is_greater_than(const value_t& val) const case INTEGER: return as_long() > val.as_long(); case AMOUNT: - return val.as_amount() > as_long(); + return val.as_amount() < as_long(); default: break; } @@ -1064,8 +1063,7 @@ bool value_t::is_greater_than(const value_t& val) const break; } - add_error_context(_("While comparing if %1 is greater than %2:") - << *this << val); + add_error_context(_("While comparing if %1 is greater than %2:") << *this << val); throw_(value_error, _("Cannot compare %1 to %2") << label() << val.label()); return *this; @@ -1228,7 +1226,7 @@ void value_t::in_place_cast(type_t cast_type) case STRING: switch (cast_type) { case INTEGER: { - if (all(as_string(), is_digit())) { + if (all(as_string(), is_any_of("-0123456789"))) { set_long(lexical_cast<long>(as_string())); return; } @@ -1251,13 +1249,23 @@ void value_t::in_place_cast(type_t cast_type) } break; + case MASK: + switch (cast_type) { + case STRING: + set_string(as_mask().str()); + return; + default: + break; + } + break; + default: break; } add_error_context(_("While converting %1:") << *this); - throw_(value_error, _("Cannot convert %1 to %2") - << label() << label(cast_type)); + throw_(value_error, + _("Cannot convert %1 to %2") << label() << label(cast_type)); } void value_t::in_place_negate() @@ -1389,25 +1397,30 @@ bool value_t::is_zero() const return false; } -value_t value_t::value(const optional<datetime_t>& moment, - const optional<commodity_t&>& in_terms_of) const +value_t value_t::value(const datetime_t& moment, + const commodity_t * in_terms_of) const { switch (type()) { case INTEGER: return NULL_VALUE; case AMOUNT: - if (optional<amount_t> val = - as_amount().value(moment, in_terms_of)) + if (optional<amount_t> val = as_amount().value(moment, in_terms_of)) return *val; return NULL_VALUE; case BALANCE: - if (optional<balance_t> bal = - as_balance().value(moment, in_terms_of)) + if (optional<balance_t> bal = as_balance().value(moment, in_terms_of)) return *bal; return NULL_VALUE; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(value.value(moment, in_terms_of)); + return temp; + } + default: break; } @@ -1417,37 +1430,100 @@ value_t value_t::value(const optional<datetime_t>& moment, return NULL_VALUE; } -value_t value_t::price() const +value_t value_t::exchange_commodities(const std::string& commodities, + const bool add_prices, + const datetime_t& moment) { - switch (type()) { - case AMOUNT: - return as_amount().price(); - case BALANCE: - return as_balance().price(); - default: - return *this; + if (type() == SEQUENCE) { + value_t temp; + foreach (value_t& value, as_sequence_lval()) + temp.push_back(value.exchange_commodities(commodities, add_prices, moment)); + return temp; } -} -value_t value_t::exchange_commodities(const std::string& commodities, - const bool add_prices, - const optional<datetime_t>& moment) -{ - scoped_array<char> buf(new char[commodities.length() + 1]); - - std::strcpy(buf.get(), commodities.c_str()); - - for (char * p = std::strtok(buf.get(), ","); - p; - p = std::strtok(NULL, ",")) { - if (commodity_t * commodity = - commodity_pool_t::current_pool->parse_price_expression(p, add_prices, - moment)) { - value_t result = value(moment, *commodity); - if (! result.is_null()) - return result; + // If we are repricing to just a single commodity, with no price + // expression, skip the expensive logic below. + if (commodities.find(',') == string::npos && + commodities.find('=') == string::npos) + return value(moment, commodity_pool_t::current_pool->find_or_create(commodities)); + + std::vector<commodity_t *> comms; + std::vector<bool> force; + + typedef tokenizer<char_separator<char> > tokenizer; + tokenizer tokens(commodities, char_separator<char>(",")); + + foreach (const string& name, tokens) { + string::size_type name_len = name.length(); + + if (commodity_t * commodity = commodity_pool_t::current_pool + ->parse_price_expression(name[name_len - 1] == '!' ? + string(name, 0, name_len - 1) : + name, add_prices, moment)) { + DEBUG("commodity.exchange", "Pricing for commodity: " << commodity->symbol()); + comms.push_back(&commodity->referent()); + force.push_back(name[name_len - 1] == '!'); + } + } + + std::size_t index = 0; + foreach (commodity_t * comm, comms) { + switch (type()) { + case AMOUNT: + DEBUG("commodity.exchange", "We have an amount: " << as_amount_lval()); + if (! force[index] && + std::find(comms.begin(), comms.end(), + &as_amount_lval().commodity().referent()) != comms.end()) + break; + + DEBUG("commodity.exchange", "Referent doesn't match, pricing..."); + if (optional<amount_t> val = as_amount_lval().value(moment, comm)) { + DEBUG("commodity.exchange", "Re-priced amount is: " << *val); + return *val; + } + DEBUG("commodity.exchange", "Was unable to find a price"); + break; + + case BALANCE: { + balance_t temp; + bool repriced = false; + + DEBUG("commodity.exchange", "We have a balance: " << as_balance_lval()); + foreach (const balance_t::amounts_map::value_type& pair, + as_balance_lval().amounts) { + DEBUG("commodity.exchange", "We have a balance amount of commodity: " + << pair.first->symbol() << " == " + << pair.second.commodity().symbol()); + if (! force[index] && + std::find(comms.begin(), comms.end(), + &pair.first->referent()) != comms.end()) { + temp += pair.second; + } else { + DEBUG("commodity.exchange", "Referent doesn't match, pricing..."); + if (optional<amount_t> val = pair.second.value(moment, comm)) { + DEBUG("commodity.exchange", "Re-priced member amount is: " << *val); + temp += *val; + repriced = true; + } else { + DEBUG("commodity.exchange", "Was unable to find price"); + temp += pair.second; + } + } + } + + if (repriced) { + DEBUG("commodity.exchange", "Re-priced balance is: " << temp); + return temp; + } } + + default: + break; + } + + ++index; } + return *this; } @@ -1727,7 +1803,7 @@ void value_t::print(std::ostream& _out, switch (type()) { case VOID: - out << ""; + out << "(null)"; break; case BOOLEAN: diff --git a/src/value.h b/src/value.h index f8495002..d128bb89 100644 --- a/src/value.h +++ b/src/value.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -179,8 +179,8 @@ public: */ explicit storage_t(const storage_t& rhs) : type(rhs.type), refc(0) { - TRACE_CTOR(value_t::storage_t, "copy"); *this = rhs; + TRACE_CTOR(value_t::storage_t, "copy"); } storage_t& operator=(const storage_t& rhs); @@ -290,73 +290,75 @@ public: } value_t(const bool val) { - TRACE_CTOR(value_t, "const bool"); set_boolean(val); + TRACE_CTOR(value_t, "const bool"); } value_t(const datetime_t& val) { - TRACE_CTOR(value_t, "const datetime_t&"); set_datetime(val); + TRACE_CTOR(value_t, "const datetime_t&"); } value_t(const date_t& val) { - TRACE_CTOR(value_t, "const date_t&"); set_date(val); + TRACE_CTOR(value_t, "const date_t&"); } value_t(const long val) { - TRACE_CTOR(value_t, "const long"); set_long(val); + TRACE_CTOR(value_t, "const long"); } value_t(const unsigned long val) { - TRACE_CTOR(value_t, "const unsigned long"); set_amount(val); + TRACE_CTOR(value_t, "const unsigned long"); } value_t(const double val) { - TRACE_CTOR(value_t, "const double"); set_amount(val); + TRACE_CTOR(value_t, "const double"); } value_t(const amount_t& val) { - TRACE_CTOR(value_t, "const amount_t&"); set_amount(val); + TRACE_CTOR(value_t, "const amount_t&"); } value_t(const balance_t& val) { - TRACE_CTOR(value_t, "const balance_t&"); set_balance(val); + TRACE_CTOR(value_t, "const balance_t&"); } value_t(const mask_t& val) { - TRACE_CTOR(value_t, "const mask_t&"); set_mask(val); + TRACE_CTOR(value_t, "const mask_t&"); } explicit value_t(const string& val, bool literal = false) { - TRACE_CTOR(value_t, "const string&, bool"); if (literal) set_string(val); else set_amount(amount_t(val)); + + TRACE_CTOR(value_t, "const string&, bool"); } explicit value_t(const char * val, bool literal = false) { - TRACE_CTOR(value_t, "const char *"); if (literal) set_string(val); else set_amount(amount_t(val)); + + TRACE_CTOR(value_t, "const char *"); } value_t(const sequence_t& val) { - TRACE_CTOR(value_t, "const sequence_t&"); set_sequence(val); + TRACE_CTOR(value_t, "const sequence_t&"); } explicit value_t(scope_t * item) { - TRACE_CTOR(value_t, "scope_t *"); set_scope(item); + TRACE_CTOR(value_t, "scope_t *"); } #if 0 template <typename T> explicit value_t(T& item) { - TRACE_CTOR(value_t, "T&"); set_any(item); + TRACE_CTOR(value_t, "T&"); } #endif @@ -375,8 +377,8 @@ public: * object. A true copy is only ever made prior to modification. */ value_t(const value_t& val) { - TRACE_CTOR(value_t, "copy"); *this = val; + TRACE_CTOR(value_t, "copy"); } value_t& operator=(const value_t& val) { if (! (this == &val || storage == val.storage)) @@ -477,14 +479,12 @@ public: void in_place_unreduce(); // exists for efficiency's sake // Return the "market value" of a given value at a specific time. - value_t value(const optional<datetime_t>& moment = none, - const optional<commodity_t&>& in_terms_of = none) const; - - value_t price() const; + value_t value(const datetime_t& moment = datetime_t(), + const commodity_t * in_terms_of = NULL) const; - value_t exchange_commodities(const std::string& commodities, - const bool add_prices = false, - const optional<datetime_t>& moment = none); + value_t exchange_commodities(const std::string& commodities, + const bool add_prices = false, + const datetime_t& moment = datetime_t()); /** * Truth tests. diff --git a/src/views.cc b/src/views.cc new file mode 100644 index 00000000..bbd58ce2 --- /dev/null +++ b/src/views.cc @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2003-2012, 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. + */ + +#ifdef DOCUMENT_MODEL + +#include <system.hh> + +#include "views.h" +#include "report.h" +#include "journal.h" +#include "xact.h" +#include "post.h" +#include "account.h" + +namespace ledger { + +r_xact_ptr r_journal_t::create_xact(xact_t * xact) +{ + r_xact_ptr x = new r_xact_t(this, xact); + add_xact(x); + assert(xact->data == NULL); + xact->data = &x; + return x; +} + +void r_journal_t::add_xact(r_xact_ptr xact) +{ + xacts.push_back(xact); +} + +r_post_ptr r_journal_t::add_post(post_t * post) +{ + r_xact_ptr x; + if (post->xact->data) + x = *static_cast<r_xact_ptr *>(post->xact->data); + else + x = create_xact(post->xact); + + r_post_ptr p = create_post(post, x, create_account(post->account)); + return p; +} + +void r_journal_t::add_post(r_post_ptr post) +{ + posts.push_back(post); +} + +r_post_ptr r_journal_t::create_post(post_t * post, r_xact_ptr xact, + r_account_ptr account) +{ + r_post_ptr p = new r_post_t(this, post, xact, account); + + add_post(p); + xact->add_post(p); + account->add_post(p); + + return p; +} + +r_post_ptr r_journal_t::create_post(r_post_ptr post, r_xact_ptr xact, + r_account_ptr account) +{ + r_post_ptr temp(new r_post_t(*post.get())); + + add_post(temp); + + temp->set_xact(xact); + xact->add_post(temp); + + temp->set_account(account); + account->add_post(temp); + + return temp; +} + +r_account_ptr r_journal_t::create_account(account_t * account) +{ + return create_account(account->fullname()); +} + +r_account_ptr r_journal_t::create_account(const std::string& name) +{ + return master_ptr->create_account(name); +} + + +const optional<position_t> r_item_t::position() const +{ + return ptr()->pos; +} + +date_t r_item_t::date() const +{ + return ptr()->date(); +} + +void r_item_t::set_date(const date_t& when) +{ +} + +item_t::state_t r_item_t::state() const +{ + return ptr()->state(); +} + +void r_item_t::set_state(item_t::state_t val) +{ +} + +string r_item_t::payee() const +{ + if (optional<value_t> desc = get_tag(_("Payee"))) + return desc->as_string(); + else + return empty_string; +} + +void r_item_t::set_payee(const string& name) +{ +} + +void r_item_t::define(const symbol_t::kind_t, const string& name, + expr_t::ptr_op_t def) +{ + bind_scope_t bound_scope(*scope_t::default_scope, *this); + set_tag(name, def->calc(bound_scope)); +} + +expr_t::ptr_op_t r_item_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (kind != symbol_t::FUNCTION) + return NULL; + + switch (name[0]) { + } + + return base_item->lookup(kind, name); +} + + +string r_xact_t::description() +{ + return ptr()->description(); +} + +void r_xact_t::add_post(r_post_ptr post) +{ + posts.push_back(post); +} + +string r_xact_t::payee() const +{ + string desc(r_item_t::payee()); + if (desc.empty()) + return ptr()->payee; + else + return desc; +} + + +string r_post_t::description() +{ + return ptr()->description(); +} + +string r_post_t::payee() const +{ + string desc(r_item_t::payee()); + if (desc.empty()) + return const_cast<r_post_t *>(this)->xact()->payee(); + else + return desc; +} + + +string r_account_t::description() +{ + return string(_("account ")) + fullname(); +} + +void r_account_t::add_post(r_post_ptr post) +{ + posts.push_back(post); +} + +r_account_ptr r_account_t::create_account(const std::string& fname) +{ + string::size_type sep = fname.find(':'); + string head, tail; + if (sep == string::npos) { + head = fname; + } else { + head = string(fname, 0, sep); + tail = string(fname, sep + 1); + } + + std::pair<r_accounts_map::iterator, bool> result = + accounts.insert(r_accounts_map::value_type + (head, new r_account_t(journal_ptr, this, name))); + + r_account_ptr acct((*result.first).second); + if (tail.empty()) + return acct; + else + return acct->create_account(tail); +} + +string r_account_t::fullname() const +{ + if (! _fullname.empty()) { + return _fullname; + } else { + r_account_ptr first = NULL; + string fname = name; + + while (! first || first->parent_ptr) { + first = first ? first->parent_ptr : parent_ptr; + if (! first->name.empty()) + fname = first->name + ":" + fname; + } + + _fullname = fname; + + return fname; + } +} + +} // namespace ledger + +#endif /* DOCUMENT_MODEL */ diff --git a/src/views.h b/src/views.h new file mode 100644 index 00000000..f9a007b7 --- /dev/null +++ b/src/views.h @@ -0,0 +1,457 @@ +/* + * Copyright (c) 2003-2012, 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 views + */ + +/** + * @file views.h + * @author John Wiegley + * + * @ingroup views + */ +#ifndef _VIEWS_H +#define _VIEWS_H + +#include "utils.h" + +#ifdef DOCUMENT_MODEL + +#include "scope.h" +#include "item.h" +#include "report.h" +#include "post.h" +#include "predicate.h" + +namespace ledger { + +class journal_t; +class xact_t; +class post_t; +class account_t; +class report_t; + +class r_base_t : public supports_flags<uint_least16_t>, + public scope_t +{ +public: + r_base_t() : refc(0) { + TRACE_CTOR(r_base_t, ""); + } + r_base_t(const r_base_t& other) : refc(0) { + TRACE_CTOR(r_base_t, "copy"); + } + virtual ~r_base_t() { + TRACE_DTOR(r_base_t); + } + +protected: + /** + * `refc' holds the current reference count for each object. + */ + mutable int refc; + + /** + * Reference counting methods. The intrusive_ptr_* methods are used + * by boost::intrusive_ptr to manage the calls to acquire and release. + */ + void acquire() const { + VERIFY(refc >= 0); + refc++; + } + void release() const { + VERIFY(refc > 0); + if (--refc == 0) + checked_delete(this); + } + + friend inline void intrusive_ptr_add_ref(r_base_t * r_ptr) { + r_ptr->acquire(); + } + friend inline void intrusive_ptr_release(r_base_t * r_ptr) { + r_ptr->release(); + } +}; + +class r_journal_t; +class r_item_t; +class r_xact_t; +class r_post_t; +class r_account_t; + +typedef intrusive_ptr<r_journal_t> r_journal_ptr; +typedef intrusive_ptr<r_item_t> r_item_ptr; +typedef intrusive_ptr<r_xact_t> r_xact_ptr; +typedef intrusive_ptr<r_post_t> r_post_ptr; +typedef intrusive_ptr<r_account_t> r_account_ptr; + +typedef std::list<r_xact_ptr> r_xacts_list; +typedef std::list<r_post_ptr> r_posts_list; + +class r_journal_t : public r_base_t +{ + journal_t * base_journal; + + journal_t * ptr() { + return base_journal; + } + const journal_t * ptr() const { + return base_journal; + } + + r_account_ptr master_ptr; + r_xacts_list xacts; + r_posts_list posts; + + void set_master(r_account_ptr ptr) { + master_ptr = ptr; + } + +public: + r_journal_t(journal_t * _journal, r_account_ptr _master) + : r_base_t(), base_journal(_journal), master_ptr(_master) { + TRACE_CTOR(r_journal_t, "journal_t *, account_t *"); + } + r_journal_t(const r_journal_t& other) + : r_base_t(other), + base_journal(other.base_journal), + master_ptr(other.master_ptr), + xacts(other.xacts), + posts(other.posts) { + TRACE_CTOR(r_journal_t, "copy"); + } + virtual ~r_journal_t() { + TRACE_DTOR(r_journal_t); + } + + r_xact_ptr create_xact(xact_t * xact = NULL); + + void add_xact(r_xact_ptr xact); + + r_xacts_list::iterator xacts_begin() { + return xacts.begin(); + } + r_xacts_list::iterator xacts_end() { + return xacts.end(); + } + + r_post_ptr add_post(post_t * post); + void add_post(r_post_ptr post); + + r_post_ptr create_post(post_t * post = NULL, r_xact_ptr xact = NULL, + r_account_ptr account = NULL); + r_post_ptr create_post(r_post_ptr post, r_xact_ptr xact = NULL, + r_account_ptr account = NULL); + + r_posts_list::iterator posts_begin() { + return posts.begin(); + } + r_posts_list::iterator posts_end() { + return posts.end(); + } + + r_account_ptr create_account(account_t * account = NULL); + r_account_ptr create_account(const std::string& name); + + friend void to_xml(std::ostream& out, r_journal_ptr journal); +}; + +class r_item_t : public r_base_t +{ +protected: + item_t * base_item; + + item_t * ptr() { + return base_item; + } + const item_t * ptr() const { + return base_item; + } + + r_journal_ptr journal_ptr; + +public: + r_item_t(r_journal_ptr _journal_ptr, item_t * _item) + : r_base_t(), base_item(_item), journal_ptr(_journal_ptr) { + TRACE_CTOR(r_item_t, "r_journal_ptr, item_t *"); + } + r_item_t(const r_item_t& other) + : r_base_t(other), + base_item(other.base_item), + journal_ptr(other.journal_ptr) { + TRACE_CTOR(r_item_t, "copy"); + } + virtual ~r_item_t() { + TRACE_DTOR(r_item_t); + } + + const optional<position_t> position() const; + + string id() const { + return ptr()->id(); + } + std::size_t seq() const { + return ptr()->seq(); + } + + date_t date() const; + void set_date(const date_t& when); + + item_t::state_t state() const; + void set_state(item_t::state_t val); + + string payee() const; + void set_payee(const string& name); + + optional<string> note() const { + return ptr()->note; + } + + bool has_tag(const string& tag) const { + return ptr()->has_tag(tag); + } + bool has_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask = none) const { + return ptr()->has_tag(tag_mask, value_mask); + } + + optional<value_t> get_tag(const string& tag) const { + return ptr()->get_tag(tag); + } + optional<value_t> get_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask = none) const { + return ptr()->get_tag(tag_mask, value_mask); + } + + void set_tag(const string& tag, + const optional<value_t>& value = none, + const bool overwrite_existing = true) { + ptr()->set_tag(tag, value, overwrite_existing); + } + + /** + * Symbol scope methods. + */ + virtual void define(const symbol_t::kind_t, const string&, + expr_t::ptr_op_t); + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + friend class r_journal_t; + friend void to_xml(std::ostream& out, r_item_ptr item); +}; + +class r_xact_t : public r_item_t +{ + xact_t * ptr() { + return reinterpret_cast<xact_t *>(base_item); + } + const xact_t * ptr() const { + return reinterpret_cast<const xact_t *>(base_item); + } + + r_posts_list posts; + +public: + r_xact_t(r_journal_ptr journal_ptr, xact_t * _xact) + : r_item_t(journal_ptr, reinterpret_cast<item_t *>(_xact)) { + TRACE_CTOR(r_xact_t, "r_journal_ptr, xact_t *"); + } + r_xact_t(const r_xact_t& other) + : r_item_t(other), + posts(other.posts) { + TRACE_CTOR(r_xact_t, "copy"); + } + virtual ~r_xact_t() { + TRACE_DTOR(r_xact_t); + } + + virtual string description(); + + void add_post(r_post_ptr post); + +#if 0 + r_post_ptr create_post(post_t * post = NULL, r_account_ptr account = NULL); + r_post_ptr create_post(r_post_ptr post, r_account_ptr account = NULL); +#endif + + r_posts_list::iterator posts_begin() { + return posts.begin(); + } + r_posts_list::iterator posts_end() { + return posts.end(); + } + + string code() const; + string payee() const; + + friend class r_journal_t; + friend void to_xml(std::ostream& out, r_xact_ptr xact); +}; + +class r_post_t : public r_item_t +{ + post_t * ptr() { + return reinterpret_cast<post_t *>(base_item); + } + const post_t * ptr() const { + return reinterpret_cast<const post_t *>(base_item); + } + + r_xact_ptr xact_ptr; + r_account_ptr account_ptr; + + void set_xact(r_xact_ptr ptr) { + xact_ptr = ptr; + } + void set_account(r_account_ptr ptr) { + account_ptr = ptr; + } + +public: + r_post_t(r_journal_ptr journal_ptr, post_t * _post, + r_xact_ptr _xact_ptr, r_account_ptr _account_ptr) + : r_item_t(journal_ptr, reinterpret_cast<item_t *>(_post)), + xact_ptr(_xact_ptr), account_ptr(_account_ptr) { + TRACE_CTOR(r_post_t, "r_journal_ptr, post_t *, r_xact_ptr, r_account_ptr"); + } + r_post_t(const r_post_t& other) + : r_item_t(other), + xact_ptr(other.xact_ptr), + account_ptr(other.account_ptr) { + TRACE_CTOR(r_post_t, "copy"); + } + virtual ~r_post_t() { + TRACE_DTOR(r_post_t); + } + + virtual string description(); + + string payee() const; + + r_xact_ptr xact(); + r_account_ptr account(); + + value_t amount() const; + value_t cost() const; + + std::size_t count() const; + value_t running_total() const; + + optional<datetime_t> checkin() const; + optional<datetime_t> checkout() const; + + friend class r_journal_t; + friend void to_xml(std::ostream& out, r_post_ptr post); +}; + +typedef std::map<string, r_account_ptr> r_accounts_map; + +class r_account_t : public r_base_t +{ + r_journal_ptr journal_ptr; + r_account_ptr parent_ptr; + r_accounts_map accounts; + r_posts_list posts; + + string name; + + mutable string _fullname; + +public: + r_account_t(r_journal_ptr _journal_ptr, r_account_ptr _parent_ptr, + string _name) + : r_base_t(), journal_ptr(_journal_ptr), parent_ptr(_parent_ptr), + name(_name) { + TRACE_CTOR(r_account_t, "r_journal_ptr, r_account_ptr, string"); + } + r_account_t(const r_account_t& other) + : r_base_t(other), + journal_ptr(other.journal_ptr), + parent_ptr(other.parent_ptr), + accounts(other.accounts), + posts(other.posts), + name(other.name), + _fullname(other._fullname) { + TRACE_CTOR(r_account_t, "copy"); + } + virtual ~r_account_t() { + TRACE_DTOR(r_account_t); + } + + virtual string description(); + + void add_post(r_post_ptr post); + + r_posts_list::iterator posts_begin() { + return posts.begin(); + } + r_posts_list::iterator posts_end() { + return posts.end(); + } + + r_account_ptr create_account(const std::string& name); + + string fullname() const; + + /** + * Symbol scope methods. + */ + virtual void define(const symbol_t::kind_t, const string&, + expr_t::ptr_op_t) {} + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& fname) { + return NULL; + } + + friend class r_journal_t; + friend void to_xml(std::ostream& out, r_account_ptr account); +}; + +template <typename PostsIterator> +void populate_journal(r_journal_ptr journal, report_t& report, + PostsIterator iter, predicate_t& pred) +{ + while (post_t * post = *iter) { + bind_scope_t bound_scope(report, *post); + if (pred.calc(bound_scope)) + journal->add_post(post); + + iter.increment(); + } +} + +} // namespace ledger + +#endif /* DOCUMENT_MODEL */ + +#endif // _VIEWS_H diff --git a/src/xact.cc b/src/xact.cc index 596b39fa..226fd5ab 100644 --- a/src/xact.cc +++ b/src/xact.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -35,6 +35,7 @@ #include "post.h" #include "account.h" #include "journal.h" +#include "context.h" #include "pool.h" namespace ledger { @@ -54,6 +55,9 @@ xact_base_t::~xact_base_t() // If the posting is a temporary, it will be destructed when the // temporary is. assert(! post->has_flags(ITEM_TEMP)); + + if (post->account) + post->account->remove_post(post); checked_delete(post); } } @@ -108,6 +112,46 @@ value_t xact_base_t::magnitude() const return halfbal; } +namespace { + inline bool account_ends_with_special_char(const string& name) { + string::size_type len(name.length()); + return (std::isdigit(name[len - 1]) || name[len - 1] == ')' || + name[len - 1] == '}' || name[len - 1] == ']'); + } + + struct add_balancing_post + { + bool first; + xact_base_t& xact; + post_t * null_post; + + explicit add_balancing_post(xact_base_t& _xact, post_t * _null_post) + : first(true), xact(_xact), null_post(_null_post) { + TRACE_CTOR(add_balancing_post, "xact_base_t&, post_t *"); + } + add_balancing_post(const add_balancing_post& other) + : first(other.first), xact(other.xact), null_post(other.null_post) { + TRACE_CTOR(add_balancing_post, "copy"); + } + ~add_balancing_post() throw() { + TRACE_DTOR(add_balancing_post); + } + + void operator()(const amount_t& amount) { + if (first) { + null_post->amount = amount.negated(); + null_post->add_flags(POST_CALCULATED); + first = false; + } else { + unique_ptr<post_t> p(new post_t(null_post->account, amount.negated(), + ITEM_GENERATED | POST_CALCULATED)); + p->set_state(null_post->state()); + xact.add_post(p.release()); + } + } + }; +} + bool xact_base_t::finalize() { // Scan through and compute the total balance for the xact. This is used @@ -133,8 +177,19 @@ bool xact_base_t::finalize() p.rounded().reduced() : p.reduced()); } else if (null_post) { - throw_(std::logic_error, - _("Only one posting with null amount allowed per transaction")); + bool post_account_bad = + account_ends_with_special_char(post->account->fullname()); + bool null_post_account_bad = + account_ends_with_special_char(null_post->account->fullname()); + + if (post_account_bad || null_post_account_bad) + throw_(std::logic_error, + _("Posting with null amount's account may be mispelled:\n \"%1\"") + << (post_account_bad ? post->account->fullname() : + null_post->account->fullname())); + else + throw_(std::logic_error, + _("Only one posting with null amount allowed per transaction")); } else { null_post = post; @@ -266,11 +321,13 @@ bool xact_base_t::finalize() cost_breakdown_t breakdown = commodity_pool_t::current_pool->exchange - (post->amount, *post->cost, false, + (post->amount, *post->cost, false, ! post->has_flags(POST_COST_VIRTUAL), datetime_t(date(), time_duration(0, 0, 0, 0))); if (post->amount.has_annotation() && post->amount.annotation().price) { if (breakdown.basis_cost.commodity() == breakdown.final_cost.commodity()) { + DEBUG("xact.finalize", "breakdown.basis_cost = " << breakdown.basis_cost); + DEBUG("xact.finalize", "breakdown.final_cost = " << breakdown.final_cost); if (amount_t gain_loss = breakdown.basis_cost - breakdown.final_cost) { DEBUG("xact.finalize", "gain_loss = " << gain_loss); gain_loss.in_place_round(); @@ -323,43 +380,17 @@ bool xact_base_t::finalize() // generated to balance them all. DEBUG("xact.finalize", "there was a null posting"); - - if (balance.is_balance()) { - const balance_t& bal(balance.as_balance()); - typedef std::map<string, amount_t> sorted_amounts_map; - sorted_amounts_map samp; - foreach (const balance_t::amounts_map::value_type& pair, bal.amounts) { - std::pair<sorted_amounts_map::iterator, bool> result = - samp.insert(sorted_amounts_map::value_type(pair.first->mapping_key(), - pair.second)); - assert(result.second); - } - - bool first = true; - foreach (sorted_amounts_map::value_type& pair, samp) { - if (first) { - null_post->amount = pair.second.negated(); - null_post->add_flags(POST_CALCULATED); - first = false; - } else { - post_t * p = new post_t(null_post->account, pair.second.negated(), - ITEM_GENERATED | POST_CALCULATED); - p->set_state(null_post->state()); - add_post(p); - } - } - } - else if (balance.is_amount()) { - null_post->amount = balance.as_amount().negated(); - null_post->add_flags(POST_CALCULATED); - } - else if (balance.is_long()) { - null_post->amount = amount_t(- balance.as_long()); - null_post->add_flags(POST_CALCULATED); - } - else if (! balance.is_null() && ! balance.is_realzero()) { + add_balancing_post post_adder(*this, null_post); + + if (balance.is_balance()) + balance.as_balance_lval().map_sorted_amounts(post_adder); + else if (balance.is_amount()) + post_adder(balance.as_amount_lval()); + else if (balance.is_long()) + post_adder(balance.to_amount()); + else if (! balance.is_null() && ! balance.is_realzero()) throw_(balance_error, _("Transaction does not balance")); - } + balance = NULL_VALUE; } @@ -457,6 +488,9 @@ bool xact_base_t::verify() xact_t::xact_t(const xact_t& e) : xact_base_t(e), code(e.code), payee(e.payee) +#ifdef DOCUMENT_MODEL + , data(NULL) +#endif { TRACE_CTOR(xact_t, "copy"); } @@ -467,30 +501,10 @@ void xact_t::add_post(post_t * post) xact_base_t::add_post(post); } -string xact_t::idstring() const -{ - std::ostringstream buf; - buf << format_date(*_date, FMT_WRITTEN); - buf << payee; - magnitude().number().print(buf); - return buf.str(); -} - -string xact_t::id() const -{ - return sha1sum(idstring()); -} - namespace { value_t get_magnitude(xact_t& xact) { return xact.magnitude(); } - value_t get_idstring(xact_t& xact) { - return string_value(xact.idstring()); - } - value_t get_id(xact_t& xact) { - return string_value(xact.id()); - } value_t get_code(xact_t& xact) { if (xact.code) @@ -554,13 +568,6 @@ expr_t::ptr_op_t xact_t::lookup(const symbol_t::kind_t kind, return WRAP_FUNCTOR(get_wrapper<&get_code>); break; - case 'i': - if (name == "id") - return WRAP_FUNCTOR(get_wrapper<&get_id>); - else if (name == "idstring") - return WRAP_FUNCTOR(get_wrapper<&get_idstring>); - break; - case 'm': if (name == "magnitude") return WRAP_FUNCTOR(get_wrapper<&get_magnitude>); @@ -592,7 +599,6 @@ bool xact_t::valid() const } namespace { - bool post_pred(expr_t::ptr_op_t op, post_t& post) { switch (op->kind) { @@ -609,6 +615,9 @@ namespace { else break; + case expr_t::op_t::O_EQ: + return post_pred(op->left(), post) == post_pred(op->right(), post); + case expr_t::op_t::O_NOT: return ! post_pred(op->left(), post); @@ -631,10 +640,9 @@ namespace { throw_(calc_error, _("Unhandled operator")); return false; } +} -} // unnamed namespace - -void auto_xact_t::extend_xact(xact_base_t& xact) +void auto_xact_t::extend_xact(xact_base_t& xact, parse_context_t& context) { posts_list initial_posts(xact.posts.begin(), xact.posts.end()); @@ -646,6 +654,8 @@ void auto_xact_t::extend_xact(xact_base_t& xact) if (initial_post->has_flags(ITEM_GENERATED)) continue; + bind_scope_t bound_scope(*scope_t::default_scope, *initial_post); + bool matches_predicate = false; if (try_quick_match) { try { @@ -673,14 +683,13 @@ void auto_xact_t::extend_xact(xact_base_t& xact) DEBUG("xact.extend.fail", "The quick matcher failed, going back to regular eval"); try_quick_match = false; - matches_predicate = predicate(*initial_post); + matches_predicate = predicate(bound_scope); } } else { - matches_predicate = predicate(*initial_post); + matches_predicate = predicate(bound_scope); } - if (matches_predicate) { - bind_scope_t bound_scope(*scope_t::default_scope, *initial_post); + if (matches_predicate) { if (deferred_notes) { foreach (deferred_tag_data_t& data, *deferred_notes) { if (data.apply_to_post == NULL) @@ -688,18 +697,19 @@ void auto_xact_t::extend_xact(xact_base_t& xact) data.overwrite_existing); } } + if (check_exprs) { - foreach (check_expr_pair& pair, *check_exprs) { - if (pair.second == auto_xact_t::EXPR_GENERAL) { + foreach (expr_t::check_expr_pair& pair, *check_exprs) { + if (pair.second == expr_t::EXPR_GENERAL) { pair.first.calc(bound_scope); } else if (! pair.first.calc(bound_scope).to_boolean()) { - if (pair.second == auto_xact_t::EXPR_ASSERTION) { + if (pair.second == expr_t::EXPR_ASSERTION) throw_(parse_error, _("Transaction assertion failed: %1") << pair.first); - } else { - warning_(_("Transaction check failed: %1") << pair.first); - } + else + context.warning(STR(_("Transaction check failed: %1") + << pair.first)); } } } @@ -770,20 +780,25 @@ void auto_xact_t::extend_xact(xact_base_t& xact) post_t * new_post = new post_t(account, amt); new_post->copy_details(*post); new_post->add_flags(ITEM_GENERATED); - - xact.add_post(new_post); - new_post->account->add_post(new_post); - - if (new_post->must_balance()) - needs_further_verification = true; + new_post->account = + journal->register_account(account->fullname(), new_post, + journal->master); if (deferred_notes) { foreach (deferred_tag_data_t& data, *deferred_notes) { - if (data.apply_to_post == post) + if (! data.apply_to_post || data.apply_to_post == post) new_post->parse_tags(data.tag_data.c_str(), bound_scope, data.overwrite_existing); } } + + extend_post(*new_post, *journal); + + xact.add_post(new_post); + new_post->account->add_post(new_post); + + if (new_post->must_balance()) + needs_further_verification = true; } } } @@ -817,9 +832,9 @@ void to_xml(std::ostream& out, const xact_t& xact) push_xml y(out, "date"); to_xml(out, *xact._date, false); } - if (xact._date_eff) { - push_xml y(out, "effective-date"); - to_xml(out, *xact._date_eff, false); + if (xact._date_aux) { + push_xml y(out, "aux-date"); + to_xml(out, *xact._date_aux, false); } if (xact.code) { @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -49,6 +49,7 @@ namespace ledger { class post_t; class journal_t; +class parse_context_t; typedef std::list<post_t *> posts_list; @@ -108,7 +109,15 @@ public: optional<string> code; string payee; - xact_t() { +#ifdef DOCUMENT_MODEL + mutable void * data; +#endif + + xact_t() +#ifdef DOCUMENT_MODEL + : data(NULL) +#endif + { TRACE_CTOR(xact_t, ""); } xact_t(const xact_t& e); @@ -129,9 +138,6 @@ public: virtual void add_post(post_t * post); - string idstring() const; - string id() const; - virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, const string& name); @@ -155,21 +161,11 @@ private: class auto_xact_t : public xact_base_t { public: - predicate_t predicate; - bool try_quick_match; - + predicate_t predicate; + bool try_quick_match; std::map<string, bool> memoized_results; - enum xact_expr_kind_t { - EXPR_GENERAL, - EXPR_ASSERTION, - EXPR_CHECK - }; - - typedef std::pair<expr_t, xact_expr_kind_t> check_expr_pair; - typedef std::list<check_expr_pair> check_expr_list; - - optional<check_expr_list> check_exprs; + optional<expr_t::check_expr_list> check_exprs; struct deferred_tag_data_t { string tag_data; @@ -180,22 +176,39 @@ public: bool _overwrite_existing) : tag_data(_tag_data), overwrite_existing(_overwrite_existing), apply_to_post(NULL) {} + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + deferred_tag_data_t() : apply_to_post(NULL) {} + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & tag_data; + ar & overwrite_existing; + ar & apply_to_post; + } +#endif // HAVE_BOOST_SERIALIZATION }; typedef std::list<deferred_tag_data_t> deferred_notes_list; optional<deferred_notes_list> deferred_notes; + post_t * active_post; - auto_xact_t() : try_quick_match(true) { + auto_xact_t() : try_quick_match(true), active_post(NULL) { TRACE_CTOR(auto_xact_t, ""); } auto_xact_t(const auto_xact_t& other) : xact_base_t(), predicate(other.predicate), - try_quick_match(other.try_quick_match) { + try_quick_match(other.try_quick_match), + active_post(other.active_post) { TRACE_CTOR(auto_xact_t, "copy"); } auto_xact_t(const predicate_t& _predicate) - : predicate(_predicate), try_quick_match(true) + : predicate(_predicate), try_quick_match(true), active_post(NULL) { TRACE_CTOR(auto_xact_t, "const predicate_t&"); } @@ -214,15 +227,15 @@ public: } } - virtual void parse_tags(const char * p, - scope_t&, - bool overwrite_existing = true) { + virtual void parse_tags(const char * p, scope_t&, + bool overwrite_existing = true) { if (! deferred_notes) deferred_notes = deferred_notes_list(); deferred_notes->push_back(deferred_tag_data_t(p, overwrite_existing)); + deferred_notes->back().apply_to_post = active_post; } - virtual void extend_xact(xact_base_t& xact); + virtual void extend_xact(xact_base_t& xact, parse_context_t& context); #if defined(HAVE_BOOST_SERIALIZATION) private: @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 @@ -52,6 +52,7 @@ namespace { out << "\">\n"; out << "<name>" << acct->name << "</name>\n"; + out << "<fullname>" << acct->fullname() << "</fullname>\n"; value_t total = acct->amount(); if (! total.is_null()) { out << "<amount>\n"; @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003-2010, John Wiegley. All rights reserved. + * Copyright (c) 2003-2012, 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 diff --git a/test/ConfirmTests.py b/test/ConfirmTests.py index 6fc04336..dffa74a6 100755 --- a/test/ConfirmTests.py +++ b/test/ConfirmTests.py @@ -13,6 +13,8 @@ harness = LedgerHarness(sys.argv) tests = sys.argv[3] if not os.path.isdir(tests) and not os.path.isfile(tests): + sys.stderr.write("'%s' is not a directory or file (cwd %s)" % + (tests, os.getcwd())) sys.exit(1) commands = [ diff --git a/test/LedgerHarness.py b/test/LedgerHarness.py index c0dbe368..b8900971 100755 --- a/test/LedgerHarness.py +++ b/test/LedgerHarness.py @@ -34,6 +34,7 @@ class LedgerHarness: failed = 0 verify = False gmalloc = False + python = False def __init__(self, argv): if not os.path.isfile(argv[1]): @@ -43,12 +44,13 @@ class LedgerHarness: print "Cannot find source path at '%s'" % argv[2] sys.exit(1) - self.ledger = argv[1] - self.sourcepath = argv[2] + self.ledger = os.path.abspath(argv[1]) + self.sourcepath = os.path.abspath(argv[2]) self.succeeded = 0 self.failed = 0 self.verify = '--verify' in argv self.gmalloc = '--gmalloc' in argv + self.python = '--python' in argv def run(self, command, verify=None, gmalloc=None, columns=True): env = os.environ.copy() @@ -77,8 +79,16 @@ class LedgerHarness: command = re.sub('\$ledger', '%s%s %s' % \ (self.ledger, insert, '--args-only'), command) + valgrind = '/usr/bin/valgrind' + if not os.path.isfile(valgrind): + valgrind = '/opt/local/bin/valgrind' + + if os.path.isfile(valgrind) and '--verify' in insert: + command = valgrind + ' -q ' + command + return Popen(command, shell=True, close_fds=True, env=env, - stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdin=PIPE, stdout=PIPE, stderr=PIPE, + cwd=self.sourcepath) def read(self, fd): text = "" @@ -108,8 +118,10 @@ class LedgerHarness: sys.stdout.write(".") self.succeeded += 1 - def failure(self): + def failure(self, name=None): sys.stdout.write("E") + if name: + sys.stdout.write("[%s]" % name) self.failed += 1 def exit(self): diff --git a/test/PyUnitTests.py b/test/PyUnitTests.py index a77d99ad..2aed48b9 100755 --- a/test/PyUnitTests.py +++ b/test/PyUnitTests.py @@ -1,6 +1,13 @@ -#!/bin/sh +#!/bin/bash + +set -e + +PYTHONPATH="%builddir%/.libs":$PYTHONPATH \ +LD_LIBRARY_PATH="%builddir%/.libs":$LD_LIBRARY_PATH \ +DYLD_LIBRARY_PATH="%builddir%/.libs":$DYLD_LIBRARY_PATH \ + %python% "%builddir%"/test/python/ConvertedTests.py PYTHONPATH="%builddir%/.libs":$PYTHONPATH \ LD_LIBRARY_PATH="%builddir%/.libs":$LD_LIBRARY_PATH \ DYLD_LIBRARY_PATH="%builddir%/.libs":$DYLD_LIBRARY_PATH \ - %python% "%builddir%"/test/python/UnitTests.py + %python% "%srcdir%"/test/python/UnitTests.py diff --git a/test/RegressTests.py b/test/RegressTests.py index 28a6c709..7d67eb21 100755 --- a/test/RegressTests.py +++ b/test/RegressTests.py @@ -31,6 +31,8 @@ harness = LedgerHarness(args) tests = args[3] if not os.path.isdir(tests) and not os.path.isfile(tests): + sys.stderr.write("'%s' is not a directory or file (cwd %s)" % + (tests, os.getcwd())) sys.exit(1) class RegressFile(object): @@ -40,6 +42,7 @@ class RegressFile(object): def transform_line(self, line): line = re.sub('\$sourcepath', harness.sourcepath, line) + line = re.sub('\$FILE', os.path.abspath(self.filename), line) return line def read_test(self): @@ -90,7 +93,7 @@ class RegressFile(object): def notify_user(self, msg, test): print msg print "--" - print test['command'], + print self.transform_line(test['command']), print "--" def run_test(self, test): @@ -98,7 +101,8 @@ class RegressFile(object): if test['command'].find("-f - ") != -1: use_stdin = True else: - test['command'] = (('$ledger -f "%s" ' % self.filename) + + test['command'] = (('$ledger -f "%s" ' % + os.path.abspath(self.filename)) + test['command']) p = harness.run(test['command'], @@ -130,9 +134,7 @@ class RegressFile(object): printed = False index = 0 if test['error'] is not None: - for line in unified_diff([re.sub('\$FILE', self.filename, line) - for line in test['error']], - harness.readlines(p.stderr)): + for line in unified_diff(test['error'], harness.readlines(p.stderr)): index += 1 if index < 3: continue @@ -148,14 +150,16 @@ class RegressFile(object): if success: harness.success() else: - harness.failure() + harness.failure(os.path.basename(self.filename)) + print "STDERR:" + print p.stderr.read() else: if success: print if test['exitcode']: self.notify_user("FAILURE in exit code (%d != %d) from %s:" % (test['exitcode'], p.returncode, self.filename), test) - harness.failure() + harness.failure(os.path.basename(self.filename)) def run_tests(self): test = self.read_test() @@ -179,7 +183,10 @@ if __name__ == '__main__': if os.path.isdir(tests): tests = [os.path.join(tests, x) - for x in os.listdir(tests) if x.endswith('.test')] + for x in os.listdir(tests) + if (x.endswith('.test') and + (not '_py.test' in x or (harness.python and + not harness.verify)))] if pool: pool.map(do_test, tests, 1) else: diff --git a/test/baseline/cmd-accounts.test b/test/baseline/cmd-accounts.test new file mode 100644 index 00000000..be6365fd --- /dev/null +++ b/test/baseline/cmd-accounts.test @@ -0,0 +1,38 @@ +2011-01-01 * Opening balance + Assets:Bank 10.00 GBP + Equity:Opening balance + +2012-01-02 * List XXX before AAA to test sorting + Assets:XXX 5.00 GBP + Assets:Bank + +2012-01-03 * List AAA after XXX to test sorting + Assets:AAA 3.00 GBP + Assets:Bank + +2012-01-03 * Account name with UTF-8 + Assets:♚ 3.00 GBP + Assets:Testing123ÕßDone + +test accounts +Assets:Bank +Equity:Opening balance +Assets:XXX +Assets:AAA +Assets:♚ +Assets:Testing123ÕßDone +end test + +test accounts assets:a +Assets:AAA +end test + +test accounts b +Assets:Bank +Equity:Opening balance +end test + +test accounts ß +Assets:Testing123ÕßDone +end test + diff --git a/test/baseline/cmd-balance.test b/test/baseline/cmd-balance.test new file mode 100644 index 00000000..064a046d --- /dev/null +++ b/test/baseline/cmd-balance.test @@ -0,0 +1,85 @@ + +2012-01-01 * Opening balances + Assets:A 10.00 + Equity:Opening balances -10.00 + +2012-01-02 * A to B + Assets:A -10.00 + Assets:B 10.00 + +2012-01-03 * B partly to C + Assets:B -5.00 + Assets:C 5.00 + +2012-01-04 * Borrow + Assets:A 10.00 + Liabilities:A -10.00 + +2012-01-05 * Return A + Assets:A -10.00 + Liabilities:A 10.00 + +test bal + 10 Assets + 5 B + 5 C + -10 Equity:Opening balances +-------------------- + 0 +end test + +test bal -n + 10 Assets + -10 Equity +-------------------- + 0 +end test + +test bal -n -E + 10 Assets + -10 Equity + 0 Liabilities +-------------------- + 0 +end test + +test bal -E + 10 Assets + 0 A + 5 B + 5 C + -10 Equity:Opening balances + 0 Liabilities:A +-------------------- + 0 +end test + +test bal --flat + 5 Assets:B + 5 Assets:C + -10 Equity:Opening balances +-------------------- + 0 +end test + +test bal --flat -E + 0 Assets:A + 5 Assets:B + 5 Assets:C + -10 Equity:Opening balances + 0 Liabilities:A +-------------------- + 0 +end test + +test bal -E --flat --no-total + 0 Assets:A + 5 Assets:B + 5 Assets:C + -10 Equity:Opening balances + 0 Liabilities:A +end test + +test bal -n --flat +end test + diff --git a/test/baseline/cmd-budget.test b/test/baseline/cmd-budget.test new file mode 100644 index 00000000..91d5a901 --- /dev/null +++ b/test/baseline/cmd-budget.test @@ -0,0 +1,65 @@ +~ Monthly + Expenses:Phone 10.00 GBP + Expenses:Rent 550.00 GBP + Assets + +2012-01-10 * Phone expense on holidays + Expenses:Phone 12.00 EUR @@ 10.00 GBP + Assets:Cash -10.00 GBP + +2012-01-31 * Rent expense + Expenses:Rent 550.00 GBP + Assets:Cash -550.00 GBP + +2012-02-28 * Phone expense + Expenses:Phone 20.00 GBP + Assets:Cash -20.00 GBP + +2012-02-29 * Rent expense + Expenses:Rent 530.00 GBP + Assets:Cash -530.00 GBP + +2012-03-10 * Phone expense + Expenses:Phone 15.00 GBP + Assets:Cash -15.00 GBP + +2012-03-31 * Rent expense + Expenses:Rent 570.00 GBP + Assets:Cash -570.00 GBP + +test budget -X GBP -p "in january 2012" + -560.00 GBP -560.00 GBP 0 100% Assets + 560.00 GBP 560.00 GBP 0 100% Expenses + 10.00 GBP 10.00 GBP 0 100% Phone + 550.00 GBP 550.00 GBP 0 100% Rent +------------ ------------ ------------ ----- + 0 0 0 0 +end test + +test budget -X GBP -p "in feb 2012" + -550.00 GBP -560.00 GBP 10.00 GBP 98% Assets + 550.00 GBP 560.00 GBP -10.00 GBP 98% Expenses + 20.00 GBP 10.00 GBP 10.00 GBP 200% Phone + 530.00 GBP 550.00 GBP -20.00 GBP 96% Rent +------------ ------------ ------------ ----- + 0 0 0 0 +end test + +test budget -X GBP -p "in march 2012" + -585.00 GBP -560.00 GBP -25.00 GBP 104% Assets + 585.00 GBP 560.00 GBP 25.00 GBP 104% Expenses + 15.00 GBP 10.00 GBP 5.00 GBP 150% Phone + 570.00 GBP 550.00 GBP 20.00 GBP 104% Rent +------------ ------------ ------------ ----- + 0 0 0 0 +end test + +test budget -X GBP --now "2012-03-31" +-1695.00 GBP -1680.00 GBP -15.00 GBP 101% Assets + 1695.00 GBP 1680.00 GBP 15.00 GBP 101% Expenses + 45.00 GBP 30.00 GBP 15.00 GBP 150% Phone + 1650.00 GBP 1650.00 GBP 0 100% Rent +------------ ------------ ------------ ----- + 0 0 0 0 +end test + diff --git a/test/baseline/cmd-cleared.test b/test/baseline/cmd-cleared.test new file mode 100644 index 00000000..501d207f --- /dev/null +++ b/test/baseline/cmd-cleared.test @@ -0,0 +1,36 @@ +2012-02-23 * Test 1 + A 10.00 + B + +2012-02-24 Test 1 + C 15.00 + D + +; leave E/F uncleared +2012-02-25 Test 1 + E 20.00 + F + +; have a cleared posting last for C +2012-02-26 * Test 1 + C 30.00 + G + +; have an uncleared posting last for A +2012-02-27 Test 1 + A 40.00 + H + +test cleared + 50 10 12-Feb-23 A + -10 -10 12-Feb-23 B + 45 30 12-Feb-26 C + -15 0 D + 20 0 E + -20 0 F + -30 -30 12-Feb-26 G + -40 0 H +---------------- ---------------- --------- + 0 0 +end test + diff --git a/test/baseline/cmd-commodities.test b/test/baseline/cmd-commodities.test new file mode 100644 index 00000000..0ce6f7a0 --- /dev/null +++ b/test/baseline/cmd-commodities.test @@ -0,0 +1,30 @@ +2011-01-01 * Opening balance + Assets:Bank 10.00 GBP + Equity:Opening balance + +2011-03-04 * Buy AAA + Assets:Broker 2 AAA @ 0.90 GBP + Assets:Bank -1.80 GBP + +2011-03-05 * Buy AA2 + Assets:Broker 2 "AA2" @ 1.00 GBP + Assets:Bank + +2011-03-06 * Get Miles&More airmiles + Assets:Rewards 1000 "M&M" + Income:Rewards + +test commodities +GBP +AAA +"AA2" +"M&M" +end test + +test commodities Assets:Rewards +"M&M" +end test + +test commodities no:such:account +end test + diff --git a/test/baseline/cmd-convert.test b/test/baseline/cmd-convert.test new file mode 100644 index 00000000..181165df --- /dev/null +++ b/test/baseline/cmd-convert.test @@ -0,0 +1,35 @@ + +test -f /dev/null --input-date-format "%m/%d/%Y" convert test/baseline/cmd-convert1.dat +2011/12/12=2011/12/13 * (100) Test ;test + Expenses:Unknown $10 + Equity:Unknown $-10 = $20 + +2011/12/12=2011/12/12 * ; + Expenses:Unknown $10 + Equity:Unknown +end test + +test -f /dev/null --input-date-format "%m/%d/%Y" convert test/baseline/cmd-convert2.dat +2011/01/01 * test + Expenses:Unknown 20.00 EUR + Equity:Unknown +end test + +test -f /dev/null --input-date-format "%m/%d/%Y" convert test/baseline/cmd-convert3.dat -> 1 +__ERROR__ +While parsing file "test/baseline/cmd-convert3.dat", line 1: +While parsing CSV line: + 01/01/2011,, + +Error: No quantity specified for amount +end test + +test -f /dev/null convert test/baseline/cmd-convert4.dat -> 1 +__ERROR__ +While parsing file "test/baseline/cmd-convert4.dat", line 1: +While parsing CSV line: + bogus,$10, + +Error: Invalid date: bogus +end test + diff --git a/test/baseline/cmd-convert1.dat b/test/baseline/cmd-convert1.dat new file mode 100644 index 00000000..542a19e3 --- /dev/null +++ b/test/baseline/cmd-convert1.dat @@ -0,0 +1,3 @@ +date,posted,code,payee,amount,total,note, +12/12/2011,12/13/2011,100,Test,$10,$20,test, +12/12/2011,12/12/2011,,,$10,, diff --git a/test/baseline/cmd-convert2.dat b/test/baseline/cmd-convert2.dat new file mode 100644 index 00000000..190095c3 --- /dev/null +++ b/test/baseline/cmd-convert2.dat @@ -0,0 +1,2 @@ +date,amount,desc, +01/01/2011,20.00 EUR,test, diff --git a/test/baseline/cmd-convert3.dat b/test/baseline/cmd-convert3.dat new file mode 100644 index 00000000..7c31d986 --- /dev/null +++ b/test/baseline/cmd-convert3.dat @@ -0,0 +1,2 @@ +date,amount, +01/01/2011,, diff --git a/test/baseline/cmd-convert4.dat b/test/baseline/cmd-convert4.dat new file mode 100644 index 00000000..644f6806 --- /dev/null +++ b/test/baseline/cmd-convert4.dat @@ -0,0 +1,2 @@ +date,amount, +bogus,$10, diff --git a/test/baseline/cmd-csv.test b/test/baseline/cmd-csv.test new file mode 100644 index 00000000..110e3d58 --- /dev/null +++ b/test/baseline/cmd-csv.test @@ -0,0 +1,53 @@ + +2012-01-01 * Opening balances + Assets:A 10.00 + Equity:Opening balances -10.00 + +2012-01-02 * Cleared posting + Assets:A -10.00 + Assets:B 10.00 + +2012-01-03 Uncleared posting + Assets:B -5.00 + Assets:C 5.00 + +2012-01-04=2012-01-05 * aux date + Assets:A 10.00 + Liabilities:A -10.00 + +2012-01-05 * (100) Code + Assets:A -10.00 + Liabilities:A 10.00 + +2012-01-06 * (100) Specify commodity + Assets:A $-10.00 + Liabilities:A $10.00 + +2012-01-07 * (100) Specify commodity + Assets:A -10.00 EUR + Liabilities:A 10.00 EUR + +2012-01-08 * (100) With note + ;This is an xact note + Assets:A -10.00 EUR + Liabilities:A 10.00 EUR + +test csv +"2012/01/01","","Opening balances","Assets:A","","10","*","" +"2012/01/01","","Opening balances","Equity:Opening balances","","-10","*","" +"2012/01/02","","Cleared posting","Assets:A","","-10","*","" +"2012/01/02","","Cleared posting","Assets:B","","10","*","" +"2012/01/03","","Uncleared posting","Assets:B","","-5","","" +"2012/01/03","","Uncleared posting","Assets:C","","5","","" +"2012/01/04","","aux date","Assets:A","","10","*","" +"2012/01/04","","aux date","Liabilities:A","","-10","*","" +"2012/01/05","100","Code","Assets:A","","-10","*","" +"2012/01/05","100","Code","Liabilities:A","","10","*","" +"2012/01/06","100","Specify commodity","Assets:A","$","-10","*","" +"2012/01/06","100","Specify commodity","Liabilities:A","$","10","*","" +"2012/01/07","100","Specify commodity","Assets:A","EUR","-10","*","" +"2012/01/07","100","Specify commodity","Liabilities:A","EUR","10","*","" +"2012/01/08","100","With note","Assets:A","EUR","-10","*","This is an xact note" +"2012/01/08","100","With note","Liabilities:A","EUR","10","*","This is an xact note" +end test + diff --git a/test/baseline/cmd-echo.test b/test/baseline/cmd-echo.test new file mode 100644 index 00000000..af3d06ec --- /dev/null +++ b/test/baseline/cmd-echo.test @@ -0,0 +1,12 @@ +test echo foo +foo +end test + +test echo "foo bar" +foo bar +end test + +test echo "foo\nbar" +foo\nbar +end test + diff --git a/test/baseline/cmd-entry.test b/test/baseline/cmd-entry.test new file mode 100644 index 00000000..0de39b9c --- /dev/null +++ b/test/baseline/cmd-entry.test @@ -0,0 +1,43 @@ +2012-03-23 * Test 1 + A $10.00 + B + +2012-03-24 * Test 2 + ; Payee: Test 3 + C 20.00 EUR + D + +2012-03-25 * Test 4 + E 30.00 GBP + F + +test --now 2012/03/25 entry "Test 1" +2012/03/25 Test 1 + A $10.00 + B +end test + +test --now 2012/03/25 entry "Test 2" +2012/03/25 Test 2 + C 20.00 EUR + D +end test + +; I think this output is wrong, see bug #737 +test --now 2012/03/25 entry "Test 3" +2012/03/25 Test 4 + E 30.00 GBP + F +end test + +test --now 2012/03/25 entry "Test 4" +2012/03/25 Test 4 + E 30.00 GBP + F +end test + +test entry no:such:account -> 1 +__ERROR__ +Error: No accounts, and no past transaction matching 'no:such:account' +end test + diff --git a/test/baseline/cmd-org.test b/test/baseline/cmd-org.test new file mode 100644 index 00000000..12a380f5 --- /dev/null +++ b/test/baseline/cmd-org.test @@ -0,0 +1,28 @@ +2012-03-24 * Test 2 + ; Payee: Test 3 + C 20.00 EUR + D + +2012-03-25 * (99) Test 4 + E 30.00 GBP + F + +2012-03-26 * (test) Test 5 + G 1 AAA @ $10.00 + H + +test org +|Date|Code|Payee|X|Account|Amount|Total|Note| +|-| +|||<20>|||<r>|<r>|<20>| +|12-Mar-24||Test 3|*|C|20.00 EUR|20.00 EUR| Payee: Test 3 +|||Test 3|*|D|-20.00 EUR|0.00 EUR| Payee: Test 3 +|12-Mar-25|99|Test 4|*|E|30.00 GBP|30.00 GBP| +||||*|F|-30.00 GBP|0.00 GBP| +|12-Mar-26|test|Test 5|*|G|1 AAA|0.00 GBP| +|||||||1 AAA| +||||*|H|$-10|0.00 GBP| +|||||||$-10| +|||||||1 AAA| +end test + diff --git a/test/baseline/cmd-payees.test b/test/baseline/cmd-payees.test new file mode 100644 index 00000000..64d6bcf5 --- /dev/null +++ b/test/baseline/cmd-payees.test @@ -0,0 +1,48 @@ +2011-03-01 * Z + A 10 + B + +2011-03-02 * A + C 10 + D + +2011-03-03 * 9 + B 10 + E + +2011-03-04 * B + B 10 + E + +2011-03-05 * 1 + B 10 + E + +2011-03-06 * 2 + ; Payee: 3 + E 10 + F + +test payees +1 +3 +9 +A +B +Z +end test + +test payees a +Z +end test + +test payees no:such:account +end test + +test payees "^B$" +1 +9 +B +Z +end test + diff --git a/test/baseline/cmd-pricedb.test b/test/baseline/cmd-pricedb.test new file mode 100644 index 00000000..4a220054 --- /dev/null +++ b/test/baseline/cmd-pricedb.test @@ -0,0 +1,39 @@ +2012-01-01 * Opening balance + A 10.00 GBP + B + +2012-01-02 * Test + A 10.00 GBP @@ 12.00 EUR + B + +2012-01-03 * Test + B 12.00 EUR @@ 15.80 USD + C + +2012-01-04 * Test + C 15.80 USD @ 0.63 GBP + D + +test pricedb +P 2012/01/02 00:00:00 GBP 1.20 EUR +P 2012/01/03 00:00:00 EUR 1.3166666667 USD +P 2012/01/04 00:00:00 USD 0.63 GBP +end test + +test pricedb EUR +P 2012/01/03 00:00:00 EUR 1.3166666667 USD +end test + +test pricedb GBP +P 2012/01/02 00:00:00 GBP 1.20 EUR +end test + +test pricedb USD +P 2012/01/04 00:00:00 USD 0.63 GBP +end test + +test pricedb U +P 2012/01/03 00:00:00 EUR 1.3166666667 USD +P 2012/01/04 00:00:00 USD 0.63 GBP +end test + diff --git a/test/baseline/cmd-pricemap.test b/test/baseline/cmd-pricemap.test new file mode 100644 index 00000000..6fbaa2fe --- /dev/null +++ b/test/baseline/cmd-pricemap.test @@ -0,0 +1,36 @@ +P 2012-03-25 EUR 0.83 GBP +P 2012-03-25 EUR 1.32 $ +P 2012-03-25 USD 0.75 EUR +P 2012-03-25 AAA $10.00 + +2012-03-23 * Test 1 + C 20.00 EUR @@ 16.71 GBP + D + +2012-03-24 * Test 2 + E 30.00 GBP + F + +2012-03-25 * Test 3 + G 1 AAA @ $10.00 + H + +test pricemap +graph G { +0[label=""]; +1[label="s"]; +2[label="%"]; +3[label="m"]; +4[label="h"]; +5[label="GBP"]; +6[label="EUR"]; +7[label="$"]; +8[label="USD"]; +9[label="AAA"]; +6--5 ; +6--7 ; +8--6 ; +9--7 ; +} +end test + diff --git a/test/baseline/cmd-prices.test b/test/baseline/cmd-prices.test new file mode 100644 index 00000000..ee2b4ba1 --- /dev/null +++ b/test/baseline/cmd-prices.test @@ -0,0 +1,39 @@ +2012-01-01 * Opening balance + A 10.00 GBP + B + +2012-01-02 * Test + A 10.00 GBP @@ 12.00 EUR + B + +2012-01-03 * Test + B 12.00 EUR @@ 15.80 USD + C + +2012-01-04 * Test + C 15.80 USD @ 0.63 GBP + D + +test prices +2012/01/02 GBP 1.20 EUR +2012/01/03 EUR 1.3166666667 USD +2012/01/04 USD 0.63 GBP +end test + +test prices EUR +2012/01/03 EUR 1.3166666667 USD +end test + +test prices USD +2012/01/04 USD 0.63 GBP +end test + +test prices GBP +2012/01/02 GBP 1.20 EUR +end test + +test prices U +2012/01/03 EUR 1.3166666667 USD +2012/01/04 USD 0.63 GBP +end test + diff --git a/test/baseline/cmd-register.test b/test/baseline/cmd-register.test new file mode 100644 index 00000000..afb78fb0 --- /dev/null +++ b/test/baseline/cmd-register.test @@ -0,0 +1,40 @@ +2012-01-10 * Phone expense on holidays + Expenses:Phone 12.00 EUR @@ 10.00 GBP + Assets:Cash -10.00 GBP + +2012-01-31 * Rent expense + Expenses:Rent 550.00 GBP + Assets:Cash -550.00 GBP + +2012-02-01 * Buy AAA + Assets:Investment 1 AAA @ 10.00 GBP + Assets:Cash -10.00 GBP + +test reg +12-Jan-10 Phone expense on ho.. Expenses:Phone 12.00 EUR 12.00 EUR + Assets:Cash -10.00 GBP 12.00 EUR + -10.00 GBP +12-Jan-31 Rent expense Expenses:Rent 550.00 GBP 12.00 EUR + 540.00 GBP + Assets:Cash -550.00 GBP 12.00 EUR + -10.00 GBP +12-Feb-01 Buy AAA Assets:Investment 1 AAA 1 AAA + 12.00 EUR + -10.00 GBP + Assets:Cash -10.00 GBP 1 AAA + 12.00 EUR + -20.00 GBP +end test + +test r :inve +12-Feb-01 Buy AAA Assets:Investment 1 AAA 1 AAA +end test + +test reg :inve +12-Feb-01 Buy AAA Assets:Investment 1 AAA 1 AAA +end test + +test register :inve +12-Feb-01 Buy AAA Assets:Investment 1 AAA 1 AAA +end test + diff --git a/test/baseline/cmd-script.test b/test/baseline/cmd-script.test new file mode 100644 index 00000000..b33dd82d --- /dev/null +++ b/test/baseline/cmd-script.test @@ -0,0 +1,21 @@ +test eval 'foo(w, u)=(z=w+u;z*2); (a=1 + 1; foo(10, 15))' +50 +end test + +test eval 'foo(x, y, z)=print(x, y, z); bar(x)=x; foo(1, 2, 3); bar(3)' +123 +3 +end test + +test eval 'total_expr=$100;amount_expr=$15;x=total_expr;x=x/5;x=amount_expr-x*5;x' +$-85 +end test + +test eval 'foo = x, y, z -> print(x, y, z); foo(1, 2, 3)' +123 +1 +end test + +test eval 'foo(x,y)=y(1, 2, 3);foo(amount_expr, (s,d,t -> t))' +3 +end test diff --git a/test/baseline/cmd-select.test b/test/baseline/cmd-select.test new file mode 100644 index 00000000..c8ce7008 --- /dev/null +++ b/test/baseline/cmd-select.test @@ -0,0 +1,62 @@ +2012-02-28 * Test 1 + E 20.00 EUR + F + +2012-02-29 * Test 2 + Test 10.01 EUR + F + +2012-03-24 Test 3 + C 30.00 EUR + D + +2012-03-25 (test) Test 4 + ; Payee: Test 5 + E 40.00 GBP + F + +test select "date, account, amount" from posts +12-Feb-28 E [0m 20.00 EUR +12-Feb-28 F [0m -20.00 EUR +12-Feb-29 Test [0m 10.01 EUR +12-Feb-29 F [0m -10.01 EUR +12-Mar-24 C [0m 30.00 EUR +12-Mar-24 D [0m -30.00 EUR +12-Mar-25 E [0m 40.00 GBP +12-Mar-25 F [0m -40.00 GBP +end test + +test select "date, account, amount from posts where account =~ /^e/" +12-Feb-28 E [0m 20.00 EUR +12-Mar-25 E [0m 40.00 GBP +end test + +test select "date, account, amount from posts where account =~ /e/" +12-Feb-28 E [0m 20.00 EUR +12-Feb-29 Test [0m 10.01 EUR +12-Mar-25 E [0m 40.00 GBP +end test + +; leave out "from posts" since it is the default +test select "date, account, amount where account =~ /e/" +12-Feb-28 E [0m 20.00 EUR +12-Feb-29 Test [0m 10.01 EUR +12-Mar-25 E [0m 40.00 GBP +end test + +test select "date, payee, amount from posts where account =~ /e/ and commodity =~ /GBP/" +12-Mar-25 Test 5 40.00 GBP +end test + +test select "date, payee, amount * 2 from posts where account =~ /e/ and commodity =~ /GBP/" +12-Mar-25 Test 5 80.00 GBP +end test + +test select "date, code, amount from posts where account =~ /e/ and commodity =~ /GBP/" +12-Mar-25 test 40.00 GBP +end test + +test select "date, code * 2, amount from posts where account =~ /e/ and commodity =~ /GBP/" +12-Mar-25 testtest 40.00 GBP +end test + diff --git a/test/baseline/cmd-source.test b/test/baseline/cmd-source.test new file mode 100644 index 00000000..95a10924 --- /dev/null +++ b/test/baseline/cmd-source.test @@ -0,0 +1,64 @@ +~ xxx + +2012-02-28 * Test + E 30.00 EUR + F + G + +2012-03-24 Test + C 30.00 EUR + D + C + +2012/03/xx + E 30.00 EUR + F + +2012-03-25 * Test + G AAA + H + +2012-03-26 * Test + I 1,00.00 EUR + J -100.00 EUR + +2012-03-27 * Test + K 100.00 EUR + L -200.00 EUR + +test source -> 7 +__ERROR__ +While parsing file "$FILE", line 1: +While parsing periodic transaction: +> ~ xxx +Error: Unexpected date period token 'xxx' +While parsing file "$FILE", line 6: +Error: Only one posting with null amount allowed per transaction +While parsing file "$FILE", line 11: +Error: Only one posting with null amount allowed per transaction +While parsing file "$FILE", line 13: +While parsing transaction: +> 2012/03/xx +Error: Invalid date: 2012/03/xx +While parsing file "$FILE", line 18: +While parsing posting: + G AAA + ^^^ +Error: No quantity specified for amount +While parsing file "$FILE", line 22: +While parsing posting: + I 1,00.00 EUR + ^^^^^^^^^^^ +Error: Incorrect use of thousand-mark comma +While parsing file "$FILE", line 27: +While balancing transaction from "$FILE", lines 25-27: +> 2012-03-27 * Test +> K 100.00 EUR +> L -200.00 EUR +Unbalanced remainder is: + -100.00 EUR +Amount to balance against: + 100.00 EUR +Error: Transaction does not balance +end test + diff --git a/test/baseline/cmd-stats.test b/test/baseline/cmd-stats.test new file mode 100644 index 00000000..ac8e1383 --- /dev/null +++ b/test/baseline/cmd-stats.test @@ -0,0 +1,31 @@ + +2012-02-28 * Test + E 30.00 EUR + F + +2012-02-29 * Test + E 30.00 EUR + F + +2012-03-24 Test + A 30.00 EUR + B + +test stats --now "2012-03-31" +Time period: 12-Feb-28 to 12-Mar-24 (25 days) + + Files these postings came from: + $sourcepath/test/baseline/cmd-stats.test + + Unique payees: 1 + Unique accounts: 4 + + Number of postings: 6 (0.24 per day) + Uncleared postings: 2 + + Days since last post: 7 + Posts in last 7 days: 2 + Posts in last 30 days: 2 + Posts seen this month: 2 +end test + diff --git a/test/baseline/cmd-xact.test b/test/baseline/cmd-xact.test new file mode 100644 index 00000000..6f4ee014 --- /dev/null +++ b/test/baseline/cmd-xact.test @@ -0,0 +1,43 @@ +2012-03-23 * Test 1 + A $10.00 + B + +2012-03-24 * Test 2 + ; Payee: Test 3 + C 20.00 EUR + D + +2012-03-25 * Test 4 + E 30.00 GBP + F + +test --now 2012/03/25 xact "Test 1" +2012/03/25 Test 1 + A $10.00 + B +end test + +test --now 2012/03/25 xact "Test 2" +2012/03/25 Test 2 + C 20.00 EUR + D +end test + +; I think this output is wrong, see bug #737 +test --now 2012/03/25 xact "Test 3" +2012/03/25 Test 4 + E 30.00 GBP + F +end test + +test --now 2012/03/25 xact "Test 4" +2012/03/25 Test 4 + E 30.00 GBP + F +end test + +test xact no:such:account -> 1 +__ERROR__ +Error: No accounts, and no past transaction matching 'no:such:account' +end test + diff --git a/test/baseline/dir-account.test b/test/baseline/dir-account.test new file mode 100644 index 00000000..e8c3fc54 --- /dev/null +++ b/test/baseline/dir-account.test @@ -0,0 +1,45 @@ +--explicit +--pedantic + +commodity $ + format $1,000.00 + +account Assets:Cash + check abs(amount) <= 20 + check commodity == '$' + default + +account Expenses:Food + alias food + payee KFC + +2012-02-27 KFC + Expenses:Unknown $20.00 + Assets:Cash + +2012-02-28 KFC + food $20.00 + Assets:Cash + +2012-02-29 KFC + food $25.00 + Assets:Cash + +2012-02-29 KFC + food $25.00 + Assets:Cash + +test reg +12-Feb-27 KFC Expenses:Food $20.00 $20.00 + Assets:Cash $-20.00 0 +12-Feb-28 KFC Expenses:Food $20.00 $20.00 + Assets:Cash $-20.00 0 +12-Feb-29 KFC Expenses:Food $25.00 $25.00 + Assets:Cash $-25.00 0 +12-Feb-29 KFC Expenses:Food $25.00 $25.00 + Assets:Cash $-25.00 0 +__ERROR__ +Warning: "$FILE", line 26: Transaction check failed: (abs(amount) <= {20}) +Warning: "$FILE", line 30: Transaction check failed: (abs(amount) <= {20}) +end test + diff --git a/test/baseline/dir-apply.dat b/test/baseline/dir-apply.dat new file mode 100644 index 00000000..bcdcacf1 --- /dev/null +++ b/test/baseline/dir-apply.dat @@ -0,0 +1,3 @@ +2012-03-12 KFC + Expenses:Food $40 + Assets:Cash diff --git a/test/baseline/dir-apply.test b/test/baseline/dir-apply.test new file mode 100644 index 00000000..7d9e91d9 --- /dev/null +++ b/test/baseline/dir-apply.test @@ -0,0 +1,34 @@ +apply account Master Account + +2012-03-12 KFC + Expenses:Food $20 + Assets:Cash + +end apply account + +apply account Master Account + +2012-03-12 KFC + Expenses:Food $20 + Assets:Cash + +end apply + +apply account Master Account + +2012-03-12 KFC + Expenses:Food $20 + Assets:Cash + +end + +apply account Master Account +include dir-apply.dat +end + +test reg food +12-Mar-12 KFC Master A:Expenses:Food $20 $20 +12-Mar-12 KFC Master A:Expenses:Food $20 $40 +12-Mar-12 KFC Master A:Expenses:Food $20 $60 +12-Mar-12 KFC Master A:Expenses:Food $40 $100 +end test diff --git a/test/baseline/dir-commodity-alias.test b/test/baseline/dir-commodity-alias.test new file mode 100644 index 00000000..4de7f406 --- /dev/null +++ b/test/baseline/dir-commodity-alias.test @@ -0,0 +1,23 @@ +commodity $ + alias USD + +2012-03-12 * $ + A $10.00 + B + +2012-03-12 * USD + A 15.00 USD + B + +test bal + 25.00 $ A + -25.00 $ B +-------------------- + 0 +end test + +test reg a +12-Mar-12 $ A 10.00 $ 10.00 $ +12-Mar-12 USD A 15.00 $ 25.00 $ +end test + diff --git a/test/baseline/dir-commodity-value.test b/test/baseline/dir-commodity-value.test new file mode 100644 index 00000000..5e8fe789 --- /dev/null +++ b/test/baseline/dir-commodity-value.test @@ -0,0 +1,24 @@ +commodity $ + value 10 EUR + +commodity USD + alias FOO + value 25 EUR + +2012-03-06 KFC + Expenses:Food $20.00 + Assets:Cash + +2012-03-08 KFC + Expenses:Food USD 750,00 + Assets:Cash + +2012-03-10 KFC + Expenses:Food USD 750,00 + Assets:Cash + +test reg food -X EUR --now=2012-03-15 +12-Mar-06 KFC Expenses:Food 200 EUR 200 EUR +12-Mar-08 KFC Expenses:Food 18750 EUR 18950 EUR +12-Mar-10 KFC Expenses:Food 18750 EUR 37700 EUR +end test diff --git a/test/baseline/dir-commodity.test b/test/baseline/dir-commodity.test new file mode 100644 index 00000000..fc925648 --- /dev/null +++ b/test/baseline/dir-commodity.test @@ -0,0 +1,21 @@ +account A +account B +commodity GBP + +2012-03-25 GBP + A 10.00 GBP + B + +2012-03-26 EUR + A 20.00 EUR + B + +test bal --pedantic -> 1 +__ERROR__ +While parsing file "$FILE", line 10: +While parsing posting: + A 20.00 EUR + ^^^^^^^^^ +Error: Unknown commodity 'EUR' +end test + diff --git a/test/baseline/dir-import_py.test b/test/baseline/dir-import_py.test new file mode 100644 index 00000000..ee9f6001 --- /dev/null +++ b/test/baseline/dir-import_py.test @@ -0,0 +1,23 @@ +import os + +tag PATH + check os.path.isfile(value) + +2012-02-29 KFC + ; PATH: test/baseline/feat-import_py.test + Expenses:Food $20 + Assets:Cash + +2012-02-29 KFC + ; PATH: test/baseline/feat-import_noexist.test + Expenses:Food $20 + Assets:Cash + +test reg +12-Feb-29 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +12-Feb-29 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +__ERROR__ +Warning: "$sourcepath/test/baseline/dir-import_py.test", line 14: Metadata check failed for (PATH: test/baseline/feat-import_noexist.test): ((os.path).isfile(value)) +end test diff --git a/test/baseline/dir-payee.test b/test/baseline/dir-payee.test new file mode 100644 index 00000000..b81bbc2b --- /dev/null +++ b/test/baseline/dir-payee.test @@ -0,0 +1,12 @@ +payee KFC + alias Kentucky Fried Chicken + +2012-03-25 * Kentucky Fried Chicken + A 10 + B + +test reg +12-Mar-25 KFC A 10 10 + B -10 0 +end test + diff --git a/test/baseline/dir-python_py.test b/test/baseline/dir-python_py.test new file mode 100644 index 00000000..99ff4b1b --- /dev/null +++ b/test/baseline/dir-python_py.test @@ -0,0 +1,28 @@ +python + import os + def check_path(path): + return os.path.isfile(path) + +tag PATH + check check_path(value) + check os.path.isfile(value) + +2012-02-29 KFC + ; PATH: test/baseline/feat-import_py.test + Expenses:Food $20 + Assets:Cash + +2012-02-29 KFC + ; PATH: test/baseline/feat-import_noexist.test + Expenses:Food $20 + Assets:Cash + +test reg +12-Feb-29 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +12-Feb-29 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +__ERROR__ +Warning: "$sourcepath/test/baseline/dir-python_py.test", line 18: Metadata check failed for (PATH: test/baseline/feat-import_noexist.test): check_path(value) +Warning: "$sourcepath/test/baseline/dir-python_py.test", line 18: Metadata check failed for (PATH: test/baseline/feat-import_noexist.test): ((os.path).isfile(value)) +end test diff --git a/test/baseline/dir-tag.test b/test/baseline/dir-tag.test new file mode 100644 index 00000000..cf668f29 --- /dev/null +++ b/test/baseline/dir-tag.test @@ -0,0 +1,21 @@ +tag Happy + check value == 'Valley' + +2012-02-27 * KFC + ; Happy: Valley + Expenses:Unknown $20.00 + ; Happy: Summer + Assets:Cash + +2012-02-28 * KFC + food $20.00 + Assets:Cash + +test reg +12-Feb-27 KFC Expenses:Unknown $20.00 $20.00 + Assets:Cash $-20.00 0 +12-Feb-28 KFC food $20.00 $20.00 + Assets:Cash $-20.00 0 +__ERROR__ +Warning: "$sourcepath/test/baseline/dir-tag.test", line 8: Metadata check failed for (Happy: Summer): (value == "Valley") +end test diff --git a/test/baseline/feat-annotations.test b/test/baseline/feat-annotations.test new file mode 100644 index 00000000..18f5d7d9 --- /dev/null +++ b/test/baseline/feat-annotations.test @@ -0,0 +1,37 @@ +2012-03-09 KFC + Expenses:Food 10 CHIK @ $50 + Assets:Cash + +2012-03-09 KFC + Assets:Cash $75 + Expenses:Food -10 CHIK {{$50}} @ $75 + Equity:Capital Gains $-25 + +2012-03-09 KFC + Expenses:Food 10 CHIK + Assets:Cash $-50 + +2012-03-09 KFC + Assets:Cash $75 + Expenses:Food -10 CHIK {{$50}} + Equity:Capital Gains $-25 + +test print +2012/03/09 KFC + Expenses:Food 10 CHIK @ $50 + Assets:Cash + +2012/03/09 KFC + Assets:Cash $75 + Expenses:Food -10 CHIK {$5} @ $75 + Equity:Capital Gains $-25 + +2012/03/09 KFC + Expenses:Food 10 CHIK + Assets:Cash $-50 + +2012/03/09 KFC + Assets:Cash $75 + Expenses:Food -10 CHIK {$5} + Equity:Capital Gains $-25 +end test diff --git a/test/baseline/feat-balance_assert.test b/test/baseline/feat-balance_assert.test new file mode 100644 index 00000000..a03cbb0e --- /dev/null +++ b/test/baseline/feat-balance_assert.test @@ -0,0 +1,13 @@ +2012-01-01 Opening Balance + Assets:Checking $100 + Equity + +2012-01-01 Reconciliation + [Assets:Checking] = $100 + +test balance + $100 Assets:Checking + $-100 Equity +-------------------- + 0 +end test diff --git a/test/baseline/feat-check.test b/test/baseline/feat-check.test index a5f0c8ad..a9db1ec4 100644 --- a/test/baseline/feat-check.test +++ b/test/baseline/feat-check.test @@ -13,6 +13,6 @@ test bal -------------------- 0 __ERROR__ -Warning: Transaction check failed: (account =~ /Foo/) -Warning: Check failed: account("Assets:Checking").all(account =~ /Expense/) +Warning: "$sourcepath/test/baseline/feat-check.test", line 6: Transaction check failed: (account =~ /Foo/) +Warning: "$sourcepath/test/baseline/feat-check.test", line 8: Check failed: account("Assets:Checking").all(account =~ /Expense/) end test diff --git a/test/baseline/feat-fixated-prices.test b/test/baseline/feat-fixated-prices.test index f4370870..4767d866 100644 --- a/test/baseline/feat-fixated-prices.test +++ b/test/baseline/feat-fixated-prices.test @@ -1,3 +1,5 @@ +P 1989/01/15 12:00:00 GAL $3 + 1990/01/01 Payee Expenses:Gas 100 GAL {=$2} Liabilities:MasterCard $-200 diff --git a/test/baseline/feat-fixated-prices_2.test b/test/baseline/feat-fixated-prices_2.test index b7b71c83..ecbdfe9a 100644 --- a/test/baseline/feat-fixated-prices_2.test +++ b/test/baseline/feat-fixated-prices_2.test @@ -1,10 +1,10 @@ -fixed XCD $0.374531835206 +apply fixed XCD $0.374531835206 2008/04/08 KFC Expenses:Food XCD 43.00 Assets:Cash -end fixed +end apply fixed test reg 08-Apr-08 KFC Expenses:Food XCD 43.00 XCD 43.00 diff --git a/test/baseline/feat-import_py.test b/test/baseline/feat-import_py.test new file mode 100644 index 00000000..6bd77586 --- /dev/null +++ b/test/baseline/feat-import_py.test @@ -0,0 +1,23 @@ +--import featimport.py + +tag PATH + check check_path(value) + +2012-02-29 KFC + ; PATH: test/baseline/feat-import_py.test + Expenses:Food $20 + Assets:Cash + +2012-02-29 KFC + ; PATH: test/baseline/feat-import_noexist.test + Expenses:Food $20 + Assets:Cash + +test reg +12-Feb-29 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +12-Feb-29 KFC Expenses:Food $20 $20 + Assets:Cash $-20 0 +__ERROR__ +Warning: "$sourcepath/test/baseline/feat-import_py.test", line 14: Metadata check failed for (PATH: test/baseline/feat-import_noexist.test): check_path(value) +end test diff --git a/test/baseline/feat-option_py.test b/test/baseline/feat-option_py.test new file mode 100644 index 00000000..1b2a0c79 --- /dev/null +++ b/test/baseline/feat-option_py.test @@ -0,0 +1,14 @@ +python + def option_pyfirst(context): + print "In --pyfirst (from %s)" % context + + def option_pysecond(context, val): + print "In --pysecond=%s (from %s)" % (val, context) + +--pyfirst +--pysecond Hey + +test reg +In --pyfirst (from $FILE) +In --pysecond=Hey (from $FILE) +end test diff --git a/test/baseline/feat-value-expr.test b/test/baseline/feat-value-expr.test new file mode 100644 index 00000000..01f9780b --- /dev/null +++ b/test/baseline/feat-value-expr.test @@ -0,0 +1,99 @@ +;; A valuation function receives three arguments: +;; +;; 'source' A string identifying the commodity whose price +;; is being asked for (example: "EUR") +;; +;; 'date' The reference date the price should be relative. +;; +;; 'target' A string identifying the "target" commodity, or +;; the commodity the returned price should be in. +;; This argument is null if -V was used instead of -X. +;; +;; The valuation function should return an amount. If you've written your +;; function in Python, you can return something like 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. + +define myfunc_seven(s, d, t) = 7 EUR + +;; In order to specific a fixed price, but still valuate that price into the +;; target commodity, use something like this: + +define myfunc_five(s, d, t) = market(5 EUR, d, t) + +;; The 'value' directive sets the valuation used for all commodities used in +;; the rest of the daat stream. This is the fallback, if nothing more +;; specific is found. + +value myfunc_seven + +;; You can set a specific valuation function on a per-commodity basis. +;; Instead of defining a function, you can also pass a lambda. + +commodity $ + value s, d, t -> 6 EUR + +;; Each account can also provide a default valuation function for any +;; commodities transferred to that account. + +account Expenses:Food5 + value myfunc_five + +;; The metadata field "Value", if found, overrides the valuation function on a +;; transaction-wide or per-posting basis. + += @XACT and Food + ; Value:: 8 EUR + (Equity) $1 + += @POST and Dining + (Expenses:Food9) $1 + ; Value:: 9 EUR + +;; Lastly, you can specify the valuation function/value for any specific +;; amount using the (( )) commodity annotation. + +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:Cas7 + +2012-03-08 XACT + Expenses:Food8 $1 + Assets:Cash8 + +2012-03-09 POST + Expenses:Dining9 $1 + Assets:Cash9 + +test reg -V food +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 5 EUR 14 EUR +12-Mar-06 KFC Expenses:Food6 6 EUR 20 EUR +12-Mar-07 KFC Expenses:Food7 7 EUR 27 EUR +12-Mar-08 XACT Expenses:Food8 8 EUR 35 EUR +12-Mar-09 POST (Expenses:Food9) 9 EUR 44 EUR +end test diff --git a/test/baseline/feat-value_py.test b/test/baseline/feat-value_py.test new file mode 100644 index 00000000..5efe315d --- /dev/null +++ b/test/baseline/feat-value_py.test @@ -0,0 +1,23 @@ +python + def print_type(val): + print type(val), val + +eval print_type(true) +eval print_type([2010/08/10]) +eval print_type(10) +eval print_type($10.00) +eval print_type($10.00 + CAD 30) +eval print_type("Hello!") +eval print_type(/Hello!/) +;eval print_type((1, 2, 3)) + +test reg +<type 'bool'> True +<type 'datetime.date'> 2010-08-10 +<class 'ledger.Amount'> 10 +<class 'ledger.Amount'> $10.00 +<class 'ledger.Balance'> $10.00 +CAD 30 +<type 'unicode'> Hello! +<class 'ledger.Value'> Hello! +end test diff --git a/test/baseline/featimport.py b/test/baseline/featimport.py new file mode 100644 index 00000000..9edd9ba3 --- /dev/null +++ b/test/baseline/featimport.py @@ -0,0 +1,4 @@ +import os + +def check_path(path_value): + return os.path.isfile(str(path_value)) diff --git a/test/baseline/featoption.py b/test/baseline/featoption.py new file mode 100644 index 00000000..caa4f2bc --- /dev/null +++ b/test/baseline/featoption.py @@ -0,0 +1,5 @@ +def option_pyfirst(context): + print "In --pyfirst (from %s)" % context + +def option_pysecond(context, val): + print "In --pysecond=%sh (from %s)" % (val, context) diff --git a/test/baseline/opt-auto-match.dat b/test/baseline/opt-auto-match.dat new file mode 100644 index 00000000..bfbf71eb --- /dev/null +++ b/test/baseline/opt-auto-match.dat @@ -0,0 +1,4 @@ +date,amount,desc, +2012/03/01,10,Food, +2012/03/02,10,Phone, +2012/03/02,10,Dining, diff --git a/test/baseline/opt-auto-match.test b/test/baseline/opt-auto-match.test new file mode 100644 index 00000000..7c3fb40a --- /dev/null +++ b/test/baseline/opt-auto-match.test @@ -0,0 +1,30 @@ +2012-01-01 * Opening balance + Assets:Cash 100.00 EUR + Equity:Opening Balance + +2012-01-02 * Food + Expenses:Food 25.00 EUR + Assets:Cash -25.00 EUR + +2012-01-03 * Phone + Expenses:Phone 10.00 EUR + Assets:Cash -10.00 EUR + +2012-01-04 * Dining + Expenses:Food 20.00 EUR + Liabilities:CC -20.00 EUR + +test --input-date-format "%Y-%m-%d" --auto-match convert test/baseline/opt-auto-match.dat +2012/03/01 * Food + Assets:Cash 10 + Equity:Unknown + +2012/03/02 * Phone + Assets:Cash 10 + Equity:Unknown + +2012/03/02 * Dining + Liabilities:CC 10 + Equity:Unknown +end test + diff --git a/test/baseline/opt-effective.test b/test/baseline/opt-aux-date.test index 9d1e73d0..9d1e73d0 100644 --- a/test/baseline/opt-effective.test +++ b/test/baseline/opt-aux-date.test diff --git a/test/baseline/opt-bold-if.test b/test/baseline/opt-bold-if.test index e69de29b..1f6f4c21 100644 --- a/test/baseline/opt-bold-if.test +++ b/test/baseline/opt-bold-if.test @@ -0,0 +1,16 @@ +2012-01-01 * Opening balance + Assets:Cash 100.00 EUR + Equity:Opening Balance + +2012-01-02 * Test + ; :test: + Expenses:Food 100.00 EUR + Assets:Cash -100.00 EUR + +test reg --bold-if 'has_tag("test")' +12-Jan-01 Opening balance Assets:Cash 100.00 EUR 100.00 EUR + Equity:Opening Balance -100.00 EUR 0 +[1m12-Jan-02[0m [1mTest [0m [1mExpenses:Food [0m [1m 100.00 EUR[0m [1m 100.00 EUR[0m + [1m [0m [1mAssets:Cash [0m [1m -100.00 EUR[0m [1m 0[0m +end test + diff --git a/test/baseline/opt-budget-format.test b/test/baseline/opt-budget-format.test index e69de29b..d2b84f98 100644 --- a/test/baseline/opt-budget-format.test +++ b/test/baseline/opt-budget-format.test @@ -0,0 +1,21 @@ +~ Monthly + Expenses:Phone 10.00 GBP + Expenses:Rent 550.00 GBP + Assets + +2012-02-28 * Phone expense + Expenses:Phone 20.00 GBP + Assets:Cash -20.00 GBP + +2012-02-29 * Rent expense + Expenses:Rent 530.00 GBP + Assets:Cash -530.00 GBP + +test budget --now 2012-02-29 --budget-format "%(justify(scrub(display_total), 0))\n" +(-550.00 GBP, 560.00 GBP) +(550.00 GBP, -560.00 GBP) +(20.00 GBP, -10.00 GBP) +(530.00 GBP, -550.00 GBP) +(0, 0) +end test + diff --git a/test/baseline/opt-check-payees.test b/test/baseline/opt-check-payees.test new file mode 100644 index 00000000..923729e7 --- /dev/null +++ b/test/baseline/opt-check-payees.test @@ -0,0 +1,37 @@ +account Assets:Cash +account Expenses:Phone +account Expenses:Rent +account Expenses:Food +commodity EUR +commodity GBP +payee Phone +tag food + +2012-03-20 Phone + Expenses:Phone 20.00 GBP + Assets:Cash + +2012-03-21 Rent + Expenses:Rent 550.00 GBP + Assets:Cash + +2012-03-22 Food + ; :food: + Expenses:Food 20.00 EUR + Assets:Cash + +test bal --explicit --strict --check-payees + -20.00 EUR + -570.00 GBP Assets:Cash + 20.00 EUR + 570.00 GBP Expenses + 20.00 EUR Food + 20.00 GBP Phone + 550.00 GBP Rent +-------------------- + 0 +__ERROR__ +Warning: "$FILE", line 14: Unknown payee 'Rent' +Warning: "$FILE", line 18: Unknown payee 'Food' +end test + diff --git a/test/baseline/opt-count.test b/test/baseline/opt-count.test index e69de29b..9c5495c8 100644 --- a/test/baseline/opt-count.test +++ b/test/baseline/opt-count.test @@ -0,0 +1,43 @@ +2012-02-28 Phone expense + Expenses:Phone 20.00 GBP + Assets:Cash -20.00 GBP + +2012-02-29 * Rent expense + Expenses:Rent 530.00 GBP + Assets:Cash -530.00 GBP + +2012-03-03 Phone expense + Expenses:Phone 12.00 EUR + Assets:Cash -12.00 EUR + +2012-03-04 * Bed and breakfast + ; Payee: Rent expense + ; :bnb: + Expenses:Rent 30.00 EUR + Assets:Cash -30.00 EUR + +test accounts --count +2 Expenses:Phone +4 Assets:Cash +2 Expenses:Rent +end test + +test commodities --count +4 GBP +4 EUR +end test + +test payees --count +4 Phone expense +4 Rent expense +end test + +test commodities :rent --count +1 GBP +1 EUR +end test + +test payees tag bnb --count +2 Rent expense +end test + diff --git a/test/baseline/opt-day-break.test b/test/baseline/opt-day-break.test new file mode 100644 index 00000000..18dde546 --- /dev/null +++ b/test/baseline/opt-day-break.test @@ -0,0 +1,12 @@ +i 05/10/2011 08:58:37 682 +o 05/12/2011 11:25:21 + +test reg --base +11-May-10 (682) 181604s 181604s +end test + +test reg --base --day-break +11-May-10 (682) 54083s 54083s +11-May-11 (682) 86400s 140483s +11-May-12 (682) 41121s 181604s +end test diff --git a/test/baseline/opt-dc.test b/test/baseline/opt-dc.test new file mode 100644 index 00000000..24a564dd --- /dev/null +++ b/test/baseline/opt-dc.test @@ -0,0 +1,16 @@ +2012-03-10 Employer + Assets:Cash $100 + Income:Employer + +2012-03-10 KFC + Expenses:Food $20 + Assets:Cash + +2012-03-10 KFC - Rebate + Assets:Cash + Expenses:Food $-5 + +2012-03-10 KFC - Food & Rebate + Expenses:Food $20 + Expenses:Food $-5 + Assets:Cash diff --git a/test/baseline/opt-decimal-comma.test b/test/baseline/opt-decimal-comma.test index e69de29b..e056c914 100644 --- a/test/baseline/opt-decimal-comma.test +++ b/test/baseline/opt-decimal-comma.test @@ -0,0 +1,22 @@ +2012-01-01 * Opening balance + Assets:Cash 100,00 EUR + Equity:Opening Balance + +2012-01-02 * Test + Expenses:Food 10,00 EUR + Assets:Cash -10,00 EUR + +2012-01-03 * Test + Expenses:Food €10,00 + Assets:Cash €-10,00 + +test --decimal-comma bal + 90,00 EUR + €-10,00 Assets:Cash + -100,00 EUR Equity:Opening Balance + 10,00 EUR + €10,00 Expenses:Food +-------------------- + 0 +end test + diff --git a/test/baseline/opt-deviation.test b/test/baseline/opt-deviation.test index df216b9c..a677ff6e 100644 --- a/test/baseline/opt-deviation.test +++ b/test/baseline/opt-deviation.test @@ -190,7 +190,7 @@ Expenses:Books $120.00 Assets:Cash -test reg --deviation books +test reg -A --deviation books 08-Jan-01 January Expenses:Books $10.00 0 08-Jan-31 End of January Expenses:Books $10.00 0 08-Feb-01 February Expenses:Books $20.00 $6.67 diff --git a/test/baseline/opt-equity.test b/test/baseline/opt-equity.test index d8695759..35ea6b1e 100644 --- a/test/baseline/opt-equity.test +++ b/test/baseline/opt-equity.test @@ -1,9 +1,58 @@ -2007/02/02 RD VMMXX - Assets:Investments:Vanguard:VMMXX 0.350 VMMXX @ $1.00 - Income:Dividends:Vanguard:VMMXX $-0.35 +D 1000.00 GBP + +2011-03-04 Buy shares + Assets:Broker 2 AAA @ 0.90 GBP + Assets:Bank + +2011-03-05 Buy shares + Assets:Broker 2 AAA @ 1.00 GBP + Assets:Bank + +test equity +2011/03/05 Opening Balances + Assets:Bank -3.80 GBP + Assets:Broker 4 AAA + Equity:Opening Balances -4 AAA + Equity:Opening Balances 3.80 GBP +end test test equity assets -2007/02/02 Opening Balances - Assets:Investments:Vanguard:VMMXX 0.350 VMMXX - Equity:Opening Balances -0.350 VMMXX +2011/03/05 Opening Balances + Assets:Bank -3.80 GBP + Assets:Broker 4 AAA + Equity:Opening Balances -4 AAA + Equity:Opening Balances 3.80 GBP end test + +test equity assets:bank +2011/03/05 Opening Balances + Assets:Bank -3.80 GBP + Equity:Opening Balances +end test + +test equity assets:broker +2011/03/05 Opening Balances + Assets:Broker 4 AAA + Equity:Opening Balances +end test + +test equity --lot-prices +2011/03/05 Opening Balances + Assets:Bank -3.80 GBP + Assets:Broker 2 AAA {0.90 GBP} + Assets:Broker 2 AAA {1.00 GBP} + Equity:Opening Balances -2 AAA {0.90 GBP} + Equity:Opening Balances -2 AAA {1.00 GBP} + Equity:Opening Balances 3.80 GBP +end test + +test equity --lots +2011/03/05 Opening Balances + Assets:Bank -3.80 GBP + Assets:Broker 2 AAA {0.90 GBP} [2011/03/04] + Assets:Broker 2 AAA {1.00 GBP} [2011/03/05] + Equity:Opening Balances -2 AAA {0.90 GBP} [2011/03/04] + Equity:Opening Balances -2 AAA {1.00 GBP} [2011/03/05] + Equity:Opening Balances 3.80 GBP +end test + diff --git a/test/baseline/opt-exchange.test b/test/baseline/opt-exchange.test index cfc48c3f..f5d73f78 100644 --- a/test/baseline/opt-exchange.test +++ b/test/baseline/opt-exchange.test @@ -53,6 +53,63 @@ test reg --exchange=' C, A ' Assets:Brokerage -75 A 75 A 09-Jan-01 January 1st, 2009 (3) Assets:Brokerage 100 A 175 A Assets:Brokerage -100 A 75 A +09-Jan-02 Commodities revalued <Revalued> 225 A + -1800 C 300 A + -1800 C +09-Jan-02 January 2nd, 2009 Assets:Brokerage 500 C 300 A + -1300 C + Assets:Brokerage -500 C 300 A + -1800 C +09-Jan-03 January 3rd, 2009 Assets:Brokerage 600 C 300 A + -1200 C + Assets:Brokerage -600 C 300 A + -1800 C +09-Jan-04 January 4th, 2009 Assets:Brokerage 300 A 600 A + -1800 C + Assets:Brokerage -2400 C 600 A + -4200 C +09-Jan-05 January 5th, 2009 Assets:Brokerage 1280 C 600 A + -2920 C + Assets:Brokerage -1280 C 600 A + -4200 C +09-Jan-06 Commodities revalued <Revalued> 2040 C 600 A + -2160 C +09-Jan-06 January 6th, 2009 Assets:Brokerage 155 A 755 A + -2160 C + Assets:Brokerage -186 C 755 A + -2346 C +09-Jan-07 Commodities revalued <Revalued> -86 C 755 A + -2432 C +09-Jan-07 January 7th, 2009 Assets:Brokerage 155 A 910 A + -2432 C + Assets:Brokerage -200 C 910 A + -2632 C +09-Jan-08 Commodities revalued <Revalued> -5613 C 910 A + -8245 C +09-Jan-08 January 8th, 2009 Assets:Brokerage 155 A 1065 A + -8245 C + Assets:Brokerage -200 C 1065 A + -8445 C +09-Jan-09 Commodities revalued <Revalued> -2800 C 1065 A + -11245 C +09-Jan-09 January 9th, 2009 Assets:Brokerage 200 C 1065 A + -11045 C + Assets:Brokerage -155 A 910 A + -11045 C +09-Jan-10 January 10th, 2009 Assets:Brokerage 200 C 910 A + -10845 C + Assets:Brokerage -155 A 755 A + -10845 C +end test + + +test reg --exchange=' C!, A ' +09-Jan-01 January 1st, 2009 (1) Assets:Brokerage 100 A 100 A + Assets:Brokerage -50 A 50 A +09-Jan-01 January 1st, 2009 (2) Assets:Brokerage 100 A 150 A + Assets:Brokerage -75 A 75 A +09-Jan-01 January 1st, 2009 (3) Assets:Brokerage 100 A 175 A + Assets:Brokerage -100 A 75 A 09-Jan-02 Commodities revalued <Revalued> 0 600 C 09-Jan-02 January 2nd, 2009 Assets:Brokerage 500 C 1100 C Assets:Brokerage -500 C 600 C diff --git a/test/baseline/opt-explicit.test b/test/baseline/opt-explicit.test new file mode 100644 index 00000000..defae179 --- /dev/null +++ b/test/baseline/opt-explicit.test @@ -0,0 +1,34 @@ +account Assets:Cash +account Expenses:Phone +account Expenses:Rent +commodity GBP + +2012-03-20 Phone + Expenses:Phone 20.00 GBP + Assets:Cash + +2012-03-21 Rent + Expenses:Rent 550.00 GBP + Assets:Cash + +2012-03-22 Food + ; :food: + Expenses:Food 20.00 EUR + Assets:Cash + +test bal --explicit --strict + -20.00 EUR + -570.00 GBP Assets:Cash + 20.00 EUR + 570.00 GBP Expenses + 20.00 EUR Food + 20.00 GBP Phone + 550.00 GBP Rent +-------------------- + 0 +__ERROR__ +Warning: "$FILE", line 16: Unknown account 'Expenses:Food' +Warning: "$FILE", line 16: Unknown commodity 'EUR' +Warning: "$FILE", line 17: Unknown metadata tag 'food' +end test + diff --git a/test/baseline/opt-file.test b/test/baseline/opt-file.test new file mode 100644 index 00000000..e01d929d --- /dev/null +++ b/test/baseline/opt-file.test @@ -0,0 +1,12 @@ +test -f opt-file-does-not-exist.dat bal -> 1 +__ERROR__ +Error: Cannot read journal file "opt-file-does-not-exist.dat" +end test + +test -f test/baseline/opt-file1.dat -f test/baseline/opt-file2.dat bal + 10 A + -10 C +-------------------- + 0 +end test + diff --git a/test/baseline/opt-file1.dat b/test/baseline/opt-file1.dat new file mode 100644 index 00000000..394882cd --- /dev/null +++ b/test/baseline/opt-file1.dat @@ -0,0 +1,4 @@ +2012-03-22 * Test 1 + A 10.00 + B + diff --git a/test/baseline/opt-file2.dat b/test/baseline/opt-file2.dat new file mode 100644 index 00000000..569993f8 --- /dev/null +++ b/test/baseline/opt-file2.dat @@ -0,0 +1,4 @@ +2012-03-22 * Test 2 + B 10.00 + C + diff --git a/test/baseline/opt-forecast-years.test b/test/baseline/opt-forecast-years.test index e69de29b..6b1053f5 100644 --- a/test/baseline/opt-forecast-years.test +++ b/test/baseline/opt-forecast-years.test @@ -0,0 +1,202 @@ +~ Monthly + Expenses:Rent 500.00 GBP + Assets + +test --now 2012-01-01 --forecast "T<200000.00 GBP" reg :rent +12-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 500.00 GBP +12-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 1000.00 GBP +12-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 1500.00 GBP +12-May-01 Forecast transaction Expenses:Rent 500.00 GBP 2000.00 GBP +12-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 2500.00 GBP +12-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 3000.00 GBP +12-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 3500.00 GBP +12-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 4000.00 GBP +12-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 4500.00 GBP +12-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 5000.00 GBP +12-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 5500.00 GBP +13-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 6000.00 GBP +13-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 6500.00 GBP +13-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 7000.00 GBP +13-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 7500.00 GBP +13-May-01 Forecast transaction Expenses:Rent 500.00 GBP 8000.00 GBP +13-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 8500.00 GBP +13-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 9000.00 GBP +13-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 9500.00 GBP +13-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 10000.00 GBP +13-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 10500.00 GBP +13-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 11000.00 GBP +13-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 11500.00 GBP +14-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 12000.00 GBP +14-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 12500.00 GBP +14-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 13000.00 GBP +14-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 13500.00 GBP +14-May-01 Forecast transaction Expenses:Rent 500.00 GBP 14000.00 GBP +14-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 14500.00 GBP +14-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 15000.00 GBP +14-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 15500.00 GBP +14-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 16000.00 GBP +14-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 16500.00 GBP +14-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 17000.00 GBP +14-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 17500.00 GBP +15-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 18000.00 GBP +15-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 18500.00 GBP +15-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 19000.00 GBP +15-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 19500.00 GBP +15-May-01 Forecast transaction Expenses:Rent 500.00 GBP 20000.00 GBP +15-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 20500.00 GBP +15-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 21000.00 GBP +15-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 21500.00 GBP +15-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 22000.00 GBP +15-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 22500.00 GBP +15-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 23000.00 GBP +15-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 23500.00 GBP +16-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 24000.00 GBP +16-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 24500.00 GBP +16-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 25000.00 GBP +16-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 25500.00 GBP +16-May-01 Forecast transaction Expenses:Rent 500.00 GBP 26000.00 GBP +16-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 26500.00 GBP +16-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 27000.00 GBP +16-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 27500.00 GBP +16-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 28000.00 GBP +16-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 28500.00 GBP +16-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 29000.00 GBP +16-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 29500.00 GBP +end test + +test --now 2012-01-01 --forecast-years 1 --forecast "T<200000.00 GBP" reg :rent +12-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 500.00 GBP +12-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 1000.00 GBP +12-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 1500.00 GBP +12-May-01 Forecast transaction Expenses:Rent 500.00 GBP 2000.00 GBP +12-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 2500.00 GBP +12-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 3000.00 GBP +12-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 3500.00 GBP +12-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 4000.00 GBP +12-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 4500.00 GBP +12-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 5000.00 GBP +12-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 5500.00 GBP +end test + +test --now 2012-01-01 --forecast-years 10 --forecast "T<200000.00 GBP" reg :rent +12-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 500.00 GBP +12-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 1000.00 GBP +12-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 1500.00 GBP +12-May-01 Forecast transaction Expenses:Rent 500.00 GBP 2000.00 GBP +12-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 2500.00 GBP +12-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 3000.00 GBP +12-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 3500.00 GBP +12-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 4000.00 GBP +12-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 4500.00 GBP +12-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 5000.00 GBP +12-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 5500.00 GBP +13-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 6000.00 GBP +13-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 6500.00 GBP +13-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 7000.00 GBP +13-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 7500.00 GBP +13-May-01 Forecast transaction Expenses:Rent 500.00 GBP 8000.00 GBP +13-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 8500.00 GBP +13-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 9000.00 GBP +13-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 9500.00 GBP +13-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 10000.00 GBP +13-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 10500.00 GBP +13-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 11000.00 GBP +13-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 11500.00 GBP +14-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 12000.00 GBP +14-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 12500.00 GBP +14-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 13000.00 GBP +14-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 13500.00 GBP +14-May-01 Forecast transaction Expenses:Rent 500.00 GBP 14000.00 GBP +14-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 14500.00 GBP +14-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 15000.00 GBP +14-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 15500.00 GBP +14-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 16000.00 GBP +14-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 16500.00 GBP +14-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 17000.00 GBP +14-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 17500.00 GBP +15-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 18000.00 GBP +15-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 18500.00 GBP +15-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 19000.00 GBP +15-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 19500.00 GBP +15-May-01 Forecast transaction Expenses:Rent 500.00 GBP 20000.00 GBP +15-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 20500.00 GBP +15-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 21000.00 GBP +15-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 21500.00 GBP +15-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 22000.00 GBP +15-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 22500.00 GBP +15-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 23000.00 GBP +15-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 23500.00 GBP +16-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 24000.00 GBP +16-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 24500.00 GBP +16-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 25000.00 GBP +16-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 25500.00 GBP +16-May-01 Forecast transaction Expenses:Rent 500.00 GBP 26000.00 GBP +16-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 26500.00 GBP +16-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 27000.00 GBP +16-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 27500.00 GBP +16-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 28000.00 GBP +16-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 28500.00 GBP +16-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 29000.00 GBP +16-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 29500.00 GBP +17-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 30000.00 GBP +17-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 30500.00 GBP +17-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 31000.00 GBP +17-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 31500.00 GBP +17-May-01 Forecast transaction Expenses:Rent 500.00 GBP 32000.00 GBP +17-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 32500.00 GBP +17-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 33000.00 GBP +17-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 33500.00 GBP +17-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 34000.00 GBP +17-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 34500.00 GBP +17-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 35000.00 GBP +17-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 35500.00 GBP +18-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 36000.00 GBP +18-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 36500.00 GBP +18-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 37000.00 GBP +18-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 37500.00 GBP +18-May-01 Forecast transaction Expenses:Rent 500.00 GBP 38000.00 GBP +18-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 38500.00 GBP +18-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 39000.00 GBP +18-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 39500.00 GBP +18-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 40000.00 GBP +18-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 40500.00 GBP +18-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 41000.00 GBP +18-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 41500.00 GBP +19-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 42000.00 GBP +19-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 42500.00 GBP +19-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 43000.00 GBP +19-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 43500.00 GBP +19-May-01 Forecast transaction Expenses:Rent 500.00 GBP 44000.00 GBP +19-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 44500.00 GBP +19-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 45000.00 GBP +19-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 45500.00 GBP +19-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 46000.00 GBP +19-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 46500.00 GBP +19-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 47000.00 GBP +19-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 47500.00 GBP +20-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 48000.00 GBP +20-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 48500.00 GBP +20-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 49000.00 GBP +20-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 49500.00 GBP +20-May-01 Forecast transaction Expenses:Rent 500.00 GBP 50000.00 GBP +20-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 50500.00 GBP +20-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 51000.00 GBP +20-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 51500.00 GBP +20-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 52000.00 GBP +20-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 52500.00 GBP +20-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 53000.00 GBP +20-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 53500.00 GBP +21-Jan-01 Forecast transaction Expenses:Rent 500.00 GBP 54000.00 GBP +21-Feb-01 Forecast transaction Expenses:Rent 500.00 GBP 54500.00 GBP +21-Mar-01 Forecast transaction Expenses:Rent 500.00 GBP 55000.00 GBP +21-Apr-01 Forecast transaction Expenses:Rent 500.00 GBP 55500.00 GBP +21-May-01 Forecast transaction Expenses:Rent 500.00 GBP 56000.00 GBP +21-Jun-01 Forecast transaction Expenses:Rent 500.00 GBP 56500.00 GBP +21-Jul-01 Forecast transaction Expenses:Rent 500.00 GBP 57000.00 GBP +21-Aug-01 Forecast transaction Expenses:Rent 500.00 GBP 57500.00 GBP +21-Sep-01 Forecast transaction Expenses:Rent 500.00 GBP 58000.00 GBP +21-Oct-01 Forecast transaction Expenses:Rent 500.00 GBP 58500.00 GBP +21-Nov-01 Forecast transaction Expenses:Rent 500.00 GBP 59000.00 GBP +21-Dec-01 Forecast transaction Expenses:Rent 500.00 GBP 59500.00 GBP +end test + diff --git a/test/baseline/opt-group-by.test b/test/baseline/opt-group-by.test index e69de29b..1f6c6adf 100644 --- a/test/baseline/opt-group-by.test +++ b/test/baseline/opt-group-by.test @@ -0,0 +1,114 @@ +2012-03-20 * Test GBP + A -10.00 GBP + B + +2012-03-20 * Test EUR + A -10.00 EUR + B + +2012-03-22 * Test GBP + A -10.00 GBP + B + +2012-03-22 * Test EUR + A -10.00 EUR + B + +2012-03-25 * Test GBP + A -10.00 GBP + B + +2012-03-25 * Test EUR + A -10.00 EUR + B + +test reg --group-by payee +Test EUR +12-Mar-20 Test EUR A -10.00 EUR -10.00 EUR + B 10.00 EUR 0 +12-Mar-22 Test EUR A -10.00 EUR -10.00 EUR + B 10.00 EUR 0 +12-Mar-25 Test EUR A -10.00 EUR -10.00 EUR + B 10.00 EUR 0 + +Test GBP +12-Mar-20 Test GBP A -10.00 GBP -10.00 GBP + B 10.00 GBP 0 +12-Mar-22 Test GBP A -10.00 GBP -10.00 GBP + B 10.00 GBP 0 +12-Mar-25 Test GBP A -10.00 GBP -10.00 GBP + B 10.00 GBP 0 +end test + +test reg --group-by commodity +EUR +12-Mar-20 Test EUR A -10.00 EUR -10.00 EUR + B 10.00 EUR 0 +12-Mar-22 Test EUR A -10.00 EUR -10.00 EUR + B 10.00 EUR 0 +12-Mar-25 Test EUR A -10.00 EUR -10.00 EUR + B 10.00 EUR 0 + +GBP +12-Mar-20 Test GBP A -10.00 GBP -10.00 GBP + B 10.00 GBP 0 +12-Mar-22 Test GBP A -10.00 GBP -10.00 GBP + B 10.00 GBP 0 +12-Mar-25 Test GBP A -10.00 GBP -10.00 GBP + B 10.00 GBP 0 +end test + +test bal --group-by commodity +EUR + -30.00 EUR A + 30.00 EUR B +-------------------- + 0 + +GBP + -30.00 GBP A + 30.00 GBP B +-------------------- + 0 +end test + +test bal --group-by payee +Test EUR + -30.00 EUR A + 30.00 EUR B +-------------------- + 0 + +Test GBP + -30.00 GBP A + 30.00 GBP B +-------------------- + 0 +end test + +test bal --group-by date +2012/03/20 + -10.00 EUR + -10.00 GBP A + 10.00 EUR + 10.00 GBP B +-------------------- + 0 + +2012/03/22 + -10.00 EUR + -10.00 GBP A + 10.00 EUR + 10.00 GBP B +-------------------- + 0 + +2012/03/25 + -10.00 EUR + -10.00 GBP A + 10.00 EUR + 10.00 GBP B +-------------------- + 0 +end test + diff --git a/test/baseline/opt-group-title-format.test b/test/baseline/opt-group-title-format.test index e69de29b..a2a1a984 100644 --- a/test/baseline/opt-group-title-format.test +++ b/test/baseline/opt-group-title-format.test @@ -0,0 +1,48 @@ +2012-03-20 * Test GBP + A -10.00 GBP + B + +2012-03-20 * Test EUR + A -10.00 EUR + B + +2012-03-22 * Test GBP + A -10.00 GBP + B + +2012-03-22 * Test EUR + A -10.00 EUR + B + +test bal --group-by payee --group-title-format "-%(value)-\n" +-Test EUR- + -20.00 EUR A + 20.00 EUR B +-------------------- + 0 + +-Test GBP- + -20.00 GBP A + 20.00 GBP B +-------------------- + 0 +end test + +test bal --group-by date --group-title-format "|%(value)|\n" +|2012/03/20| + -10.00 EUR + -10.00 GBP A + 10.00 EUR + 10.00 GBP B +-------------------- + 0 + +|2012/03/22| + -10.00 EUR + -10.00 GBP A + 10.00 EUR + 10.00 GBP B +-------------------- + 0 +end test + diff --git a/test/baseline/opt-historical.test b/test/baseline/opt-historical.test new file mode 100644 index 00000000..9eb3558b --- /dev/null +++ b/test/baseline/opt-historical.test @@ -0,0 +1,250 @@ +D EUR 2.000,00 + +P 2011-12-15 $ EUR 2 +P 2011-12-15 AAPL $5.00 + +2012-01-01 Broker + Assets:Stocks 10 AAPL {$1} @ $10 + Equity + +P 2012-01-15 AAPL $15.00 + +2012-02-02 Broker + Assets:Stocks 10 AAPL {$2} @ $20 + Equity + +P 2012-02-15 AAPL $25.00 + +2012-03-03 Broker + Assets:Stocks 10 AAPL {$3} @ $30 + Equity + +P 2012-03-15 AAPL $35.00 + +2012-04-04 Broker + Assets:Stocks 10 AAPL {$4} @ $40 + Equity + +P 2012-04-15 AAPL $45.00 + +2012-05-05 Broker + Assets:Stocks 10 AAPL {$5} @ $50 + Equity + +P 2012-5-15 AAPL $55.00 + +test reg stocks +12-Jan-01 Broker Assets:Stocks 10 AAPL 10 AAPL +12-Feb-02 Broker Assets:Stocks 10 AAPL 20 AAPL +12-Mar-03 Broker Assets:Stocks 10 AAPL 30 AAPL +12-Apr-04 Broker Assets:Stocks 10 AAPL 40 AAPL +12-May-05 Broker Assets:Stocks 10 AAPL 50 AAPL +end test + +test reg stocks -O +12-Jan-01 Broker Assets:Stocks 10 AAPL 10 AAPL +12-Feb-02 Broker Assets:Stocks 10 AAPL 20 AAPL +12-Mar-03 Broker Assets:Stocks 10 AAPL 30 AAPL +12-Apr-04 Broker Assets:Stocks 10 AAPL 40 AAPL +12-May-05 Broker Assets:Stocks 10 AAPL 50 AAPL +end test + +test reg stocks -B +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -I +12-Jan-01 Broker Assets:Stocks $10 $10 +12-Feb-02 Broker Assets:Stocks $20 $30 +12-Mar-03 Broker Assets:Stocks $30 $60 +12-Apr-04 Broker Assets:Stocks $40 $100 +12-May-05 Broker Assets:Stocks $50 $150 +end test + +test reg stocks -V +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Jan-15 Commodities revalued <Revalued> $50 $150 +12-Feb-02 Commodities revalued <Revalued> $50 $200 +12-Feb-02 Broker Assets:Stocks $200 $400 +12-Feb-15 Commodities revalued <Revalued> $100 $500 +12-Mar-03 Commodities revalued <Revalued> $100 $600 +12-Mar-03 Broker Assets:Stocks $300 $900 +12-Mar-15 Commodities revalued <Revalued> $150 $1050 +12-Apr-04 Commodities revalued <Revalued> $150 $1200 +12-Apr-04 Broker Assets:Stocks $400 $1600 +12-Apr-15 Commodities revalued <Revalued> $200 $1800 +12-May-05 Commodities revalued <Revalued> $200 $2000 +12-May-05 Broker Assets:Stocks $500 $2500 +end test + +test reg stocks -O -V +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Jan-15 Commodities revalued <Revalued> $50 $150 +12-Feb-02 Commodities revalued <Revalued> $50 $200 +12-Feb-02 Broker Assets:Stocks $200 $400 +12-Feb-15 Commodities revalued <Revalued> $100 $500 +12-Mar-03 Commodities revalued <Revalued> $100 $600 +12-Mar-03 Broker Assets:Stocks $300 $900 +12-Mar-15 Commodities revalued <Revalued> $150 $1050 +12-Apr-04 Commodities revalued <Revalued> $150 $1200 +12-Apr-04 Broker Assets:Stocks $400 $1600 +12-Apr-15 Commodities revalued <Revalued> $200 $1800 +12-May-05 Commodities revalued <Revalued> $200 $2000 +12-May-05 Broker Assets:Stocks $500 $2500 +end test + +test reg stocks -B -V +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -I -V +12-Jan-01 Broker Assets:Stocks $10 $10 +12-Feb-02 Broker Assets:Stocks $20 $30 +12-Mar-03 Broker Assets:Stocks $30 $60 +12-Apr-04 Broker Assets:Stocks $40 $100 +12-May-05 Broker Assets:Stocks $50 $150 +end test + +test reg stocks -X EUR +12-Jan-01 Broker Assets:Stocks EUR 200,00 EUR 200,00 +12-Feb-02 Commodities revalued <Revalued> EUR 200,00 EUR 400,00 +12-Feb-02 Broker Assets:Stocks EUR 400,00 EUR 800,00 +12-Mar-03 Commodities revalued <Revalued> EUR 400,00 EUR 1.200,00 +12-Mar-03 Broker Assets:Stocks EUR 600,00 EUR 1.800,00 +12-Apr-04 Commodities revalued <Revalued> EUR 600,00 EUR 2.400,00 +12-Apr-04 Broker Assets:Stocks EUR 800,00 EUR 3.200,00 +12-May-05 Commodities revalued <Revalued> EUR 800,00 EUR 4.000,00 +12-May-05 Broker Assets:Stocks EUR 1.000,00 EUR 5.000,00 +end test + +test reg stocks -O -X EUR +12-Jan-01 Broker Assets:Stocks EUR 200,00 EUR 200,00 +12-Feb-02 Commodities revalued <Revalued> EUR 200,00 EUR 400,00 +12-Feb-02 Broker Assets:Stocks EUR 400,00 EUR 800,00 +12-Mar-03 Commodities revalued <Revalued> EUR 400,00 EUR 1.200,00 +12-Mar-03 Broker Assets:Stocks EUR 600,00 EUR 1.800,00 +12-Apr-04 Commodities revalued <Revalued> EUR 600,00 EUR 2.400,00 +12-Apr-04 Broker Assets:Stocks EUR 800,00 EUR 3.200,00 +12-May-05 Commodities revalued <Revalued> EUR 800,00 EUR 4.000,00 +12-May-05 Broker Assets:Stocks EUR 1.000,00 EUR 5.000,00 +end test + +test reg stocks -B -X EUR +12-Jan-01 Broker Assets:Stocks EUR 200,00 EUR 200,00 +12-Feb-02 Broker Assets:Stocks EUR 400,00 EUR 600,00 +12-Mar-03 Broker Assets:Stocks EUR 600,00 EUR 1.200,00 +12-Apr-04 Broker Assets:Stocks EUR 800,00 EUR 2.000,00 +12-May-05 Broker Assets:Stocks EUR 1.000,00 EUR 3.000,00 +end test + +test reg stocks -I -X EUR +12-Jan-01 Broker Assets:Stocks EUR 20,00 EUR 20,00 +12-Feb-02 Broker Assets:Stocks EUR 40,00 EUR 60,00 +12-Mar-03 Broker Assets:Stocks EUR 60,00 EUR 120,00 +12-Apr-04 Broker Assets:Stocks EUR 80,00 EUR 200,00 +12-May-05 Broker Assets:Stocks EUR 100,00 EUR 300,00 +end test + +test reg stocks -H +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -O -H +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -B -H +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -I -H +12-Jan-01 Broker Assets:Stocks $10 $10 +12-Feb-02 Broker Assets:Stocks $20 $30 +12-Mar-03 Broker Assets:Stocks $30 $60 +12-Apr-04 Broker Assets:Stocks $40 $100 +12-May-05 Broker Assets:Stocks $50 $150 +end test + +test reg stocks -H -V +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -O -H -V +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -B -H -V +12-Jan-01 Broker Assets:Stocks $100 $100 +12-Feb-02 Broker Assets:Stocks $200 $300 +12-Mar-03 Broker Assets:Stocks $300 $600 +12-Apr-04 Broker Assets:Stocks $400 $1000 +12-May-05 Broker Assets:Stocks $500 $1500 +end test + +test reg stocks -I -H -V +12-Jan-01 Broker Assets:Stocks $10 $10 +12-Feb-02 Broker Assets:Stocks $20 $30 +12-Mar-03 Broker Assets:Stocks $30 $60 +12-Apr-04 Broker Assets:Stocks $40 $100 +12-May-05 Broker Assets:Stocks $50 $150 +end test + +test reg stocks -H -X EUR +12-Jan-01 Broker Assets:Stocks EUR 200,00 EUR 200,00 +12-Feb-02 Broker Assets:Stocks EUR 400,00 EUR 600,00 +12-Mar-03 Broker Assets:Stocks EUR 600,00 EUR 1.200,00 +12-Apr-04 Broker Assets:Stocks EUR 800,00 EUR 2.000,00 +12-May-05 Broker Assets:Stocks EUR 1.000,00 EUR 3.000,00 +end test + +test reg stocks -O -H -X EUR +12-Jan-01 Broker Assets:Stocks EUR 200,00 EUR 200,00 +12-Feb-02 Broker Assets:Stocks EUR 400,00 EUR 600,00 +12-Mar-03 Broker Assets:Stocks EUR 600,00 EUR 1.200,00 +12-Apr-04 Broker Assets:Stocks EUR 800,00 EUR 2.000,00 +12-May-05 Broker Assets:Stocks EUR 1.000,00 EUR 3.000,00 +end test + +test reg stocks -B -H -X EUR +12-Jan-01 Broker Assets:Stocks EUR 200,00 EUR 200,00 +12-Feb-02 Broker Assets:Stocks EUR 400,00 EUR 600,00 +12-Mar-03 Broker Assets:Stocks EUR 600,00 EUR 1.200,00 +12-Apr-04 Broker Assets:Stocks EUR 800,00 EUR 2.000,00 +12-May-05 Broker Assets:Stocks EUR 1.000,00 EUR 3.000,00 +end test + +test reg stocks -I -H -X EUR +12-Jan-01 Broker Assets:Stocks EUR 20,00 EUR 20,00 +12-Feb-02 Broker Assets:Stocks EUR 40,00 EUR 60,00 +12-Mar-03 Broker Assets:Stocks EUR 60,00 EUR 120,00 +12-Apr-04 Broker Assets:Stocks EUR 80,00 EUR 200,00 +12-May-05 Broker Assets:Stocks EUR 100,00 EUR 300,00 +end test diff --git a/test/baseline/opt-actual-dates.test b/test/baseline/opt-immediate.test index e69de29b..e69de29b 100644 --- a/test/baseline/opt-actual-dates.test +++ b/test/baseline/opt-immediate.test diff --git a/test/baseline/opt-inject.test b/test/baseline/opt-inject.test index e69de29b..685cf12c 100644 --- a/test/baseline/opt-inject.test +++ b/test/baseline/opt-inject.test @@ -0,0 +1,10 @@ +2012-03-20 * Test GBP + ; Expected:: -15.00 GBP + Expenses:Phone 20.00 GBP + Assets:Cash + +test --inject Expected reg Expenses:Phone +12-Mar-20 Test GBP Expected -15.00 GBP -15.00 GBP +12-Mar-20 Test GBP Expenses:Phone 20.00 GBP 5.00 GBP +end test + diff --git a/test/baseline/opt-lot-notes.test b/test/baseline/opt-lot-notes.test new file mode 100644 index 00000000..3bfa9e45 --- /dev/null +++ b/test/baseline/opt-lot-notes.test @@ -0,0 +1,31 @@ +2012-01-01 * Opening balance + Assets:Cash 100.00 GBP + Equity:Opening Balance + +2012-01-02 * Voucher 1 + Assets:Voucher 10.00 GBP (aaaa) + Assets:Cash -10.00 GBP + +2012-01-03 * Voucher 1 + Assets:Voucher 10.00 GBP (bbbb) + Assets:Cash -10.00 GBP + +2012-01-04 * Voucher 1 + Assets:Voucher 10.00 GBP (cccc) + Assets:Cash -10.00 GBP + +test bal assets:voucher --lot-notes + 10.00 GBP (aaaa) + 10.00 GBP (bbbb) + 10.00 GBP (cccc) Assets:Voucher +end test + +test reg assets:voucher --lot-notes +12-Jan-02 Voucher 1 Assets:Voucher 10.00 GBP (aaaa) 10.00 GBP (aaaa) +12-Jan-03 Voucher 1 Assets:Voucher 10.00 GBP (bbbb) 10.00 GBP (aaaa) + 10.00 GBP (bbbb) +12-Jan-04 Voucher 1 Assets:Voucher 10.00 GBP (cccc) 10.00 GBP (aaaa) + 10.00 GBP (bbbb) + 10.00 GBP (cccc) +end test + diff --git a/test/baseline/opt-meta-width.test b/test/baseline/opt-meta-width.test index ce751e24..893e175b 100644 --- a/test/baseline/opt-meta-width.test +++ b/test/baseline/opt-meta-width.test @@ -9,6 +9,6 @@ ; :AnotherTag: test reg --meta Sample --meta-width=15 -Another Value 04-May-27 Credit card com.. Liab:MasterCard $20.00 $20.00 -Value As:Ban:Checking $-20.00 0 +Another Value 04-May-27 Credit card co.. Liabi:MasterCard $20.00 $20.00 +Value As:Bank:Checking $-20.00 0 end test diff --git a/test/baseline/opt-no-pager.test b/test/baseline/opt-no-pager.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-no-pager.test diff --git a/test/baseline/opt-payee-as-account.test b/test/baseline/opt-payee-as-account.test index 113a395b..0d1f87d6 100644 --- a/test/baseline/opt-payee-as-account.test +++ b/test/baseline/opt-payee-as-account.test @@ -21,11 +21,11 @@ test reg --account=payee 08-Jan-01 January January:Expenses:Books $10.00 $10.00 08-Jan-01 January January:Assets:Cash $-10.00 0 -08-Jan-31 End of January End of :Expenses:Books $10.00 $10.00 +08-Jan-31 End of January End of:Expenses:Books $10.00 $10.00 08-Jan-31 End of January End of Jan:Assets:Cash $-10.00 0 08-Feb-01 February Februar:Expenses:Books $20.00 $20.00 08-Feb-01 February February:Assets:Cash $-20.00 0 -08-Feb-28 End of February End of :Expenses:Books $20.00 $20.00 +08-Feb-28 End of February End of:Expenses:Books $20.00 $20.00 08-Feb-28 End of February End of Feb:Assets:Cash $-20.00 0 08-Mar-01 March March:Expenses:Books $30.00 $30.00 08-Mar-01 March March:Assets:Cash $-30.00 0 diff --git a/test/baseline/opt-pedantic.test b/test/baseline/opt-pedantic.test new file mode 100644 index 00000000..fbb27b84 --- /dev/null +++ b/test/baseline/opt-pedantic.test @@ -0,0 +1,17 @@ +2012-03-20 Test GBP + Expenses:Phone 20.00 GBP + Assets:Cash + +2012-03-21 * Test GBP + Expenses:Phone 20.00 GBP + Assets:Cash + +test bal --pedantic -> 1 +__ERROR__ +While parsing file "$FILE", line 2: +While parsing posting: + Expenses:Phone 20.00 GBP + +Error: Unknown account 'Expenses:Phone' +end test + diff --git a/test/baseline/opt-period.test b/test/baseline/opt-period.test index 7268bcce..f370b404 100644 --- a/test/baseline/opt-period.test +++ b/test/baseline/opt-period.test @@ -257,7 +257,7 @@ test reg -p "weekly january 2008" 08-Jan-01 - 08-Jan-05 Assets:Cash $-20.00 $-20.00 Expenses:Books $10.00 $-10.00 Liabilities:Cards $10.00 0 -08-Jan-29 - 08-Jan-31 Assets:Cash $-20.00 $-20.00 +08-Jan-27 - 08-Jan-31 Assets:Cash $-20.00 $-20.00 Expenses:Books $10.00 $-10.00 Liabilities:Cards $10.00 0 end test diff --git a/test/baseline/opt-permissive.test b/test/baseline/opt-permissive.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-permissive.test diff --git a/test/baseline/opt-primary-date.test b/test/baseline/opt-primary-date.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-primary-date.test diff --git a/test/baseline/opt-rich-data.test b/test/baseline/opt-rich-data.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-rich-data.test diff --git a/test/baseline/opt-time-report.test b/test/baseline/opt-time-report.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-time-report.test diff --git a/test/baseline/opt-unround.test b/test/baseline/opt-unround.test index cef212ae..755bb62c 100644 --- a/test/baseline/opt-unround.test +++ b/test/baseline/opt-unround.test @@ -82,7 +82,7 @@ Expenses:Travel:Passport $127.00 Assets:Checking -test bal --unround --percent +test bal --percent --unround 100.00% Assets:Checking 100.00% Expenses:Travel 92.14958953% Airfare diff --git a/test/baseline/opt-value-expr.test b/test/baseline/opt-value-expr.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-value-expr.test diff --git a/test/baseline/opt-verify-memory.test b/test/baseline/opt-verify-memory.test new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/baseline/opt-verify-memory.test diff --git a/test/fullcheck.sh b/test/fullcheck.sh index f3c20dd2..fc89a13c 100755 --- a/test/fullcheck.sh +++ b/test/fullcheck.sh @@ -1,18 +1,21 @@ #!/bin/sh VALGRIND='' -if [ -x /usr/bin/valgrind ]; then - VALGRIND=valgrind +if [ -x /usr/bin/valgrind -o -x /opt/local/bin/valgrind ]; then + VALGRIND="valgrind -q --track-origins=yes" + if [ `uname` = "Darwin" ]; then + VALGRIND="$VALGRIND --dsymutil=yes" + fi fi -export MallocGuardEdges=1 -export MallocScribble=1 -export MallocPreScribble=1 -export MallocCheckHeapStart=100 -export MallocCheckHeapEach=100 -export DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib -export MALLOC_PROTECT_BEFORE=1 -export MALLOC_FILL_SPACE=1 -export MALLOC_STRICT_SIZE=1 +#export MallocGuardEdges=1 +#export MallocScribble=1 +#export MallocPreScribble=1 +#export MallocCheckHeapStart=100 +#export MallocCheckHeapEach=100 +#export DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib +#export MALLOC_PROTECT_BEFORE=1 +#export MALLOC_FILL_SPACE=1 +#export MALLOC_STRICT_SIZE=1 exec $VALGRIND $@ diff --git a/test/input/sample.dat b/test/input/sample.dat index 12ac4cb4..5b411edd 100644 --- a/test/input/sample.dat +++ b/test/input/sample.dat @@ -33,7 +33,7 @@ N $ Русский язык:Активы:Русский язык:Русский язык $1000.00 Income:Salary -tag foo +apply tag foo 2004/05/27 Book Store Expenses:Books $20.00 @@ -41,7 +41,7 @@ tag foo Expenses:Docs $30.00 Liabilities:MasterCard -end tag +end apply tag 2004/05/27 (100) Credit card company ; This is an xact note! @@ -52,3 +52,5 @@ end tag ; :MyTag: Assets:Bank:Checking ; :AnotherTag: + +;;; sample.dat ends here diff --git a/test/manual/transaction-codes-1.test b/test/manual/transaction-codes-1.test index 7a05b349..ff68e0ec 100644 --- a/test/manual/transaction-codes-1.test +++ b/test/manual/transaction-codes-1.test @@ -15,6 +15,6 @@ Liabilities:Credit Card test reg --columns=60 food and code xfer -09-Oct-29 Panera Bread Expenses:Food $4.50 $4.50 -09-Oct-30 Panera Bread Expenses:Food $4.50 $9.00 +09-Oct-29 Panera Bread Expenses:Food $4.50 $4.50 +09-Oct-30 Panera Bread Expenses:Food $4.50 $9.00 end test diff --git a/test/manual/transaction-notes-1.test b/test/manual/transaction-notes-1.test index 7c3d7200..05ab3412 100644 --- a/test/manual/transaction-notes-1.test +++ b/test/manual/transaction-notes-1.test @@ -17,6 +17,6 @@ Assets:Checking test reg --columns=60 food and note eat -09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 -09-Nov-01 Panera Bread Expenses:Food $4.50 $9.00 +09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 +09-Nov-01 Panera Bread Expenses:Food $4.50 $9.00 end test diff --git a/test/manual/transaction-notes-2.test b/test/manual/transaction-notes-2.test index 603fcbe1..a29eda6e 100644 --- a/test/manual/transaction-notes-2.test +++ b/test/manual/transaction-notes-2.test @@ -17,5 +17,5 @@ Assets:Checking test reg --columns=60 food and tag eating -09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 +09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 end test diff --git a/test/manual/transaction-notes-3.test b/test/manual/transaction-notes-3.test index 9b05334c..b83322b0 100644 --- a/test/manual/transaction-notes-3.test +++ b/test/manual/transaction-notes-3.test @@ -17,5 +17,5 @@ Assets:Checking test reg --columns=60 food and tag type=dining -09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 +09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 end test diff --git a/test/manual/transaction-status-1.test b/test/manual/transaction-status-1.test index 1f7ad095..8bfdd6d9 100644 --- a/test/manual/transaction-status-1.test +++ b/test/manual/transaction-status-1.test @@ -11,7 +11,7 @@ Assets test reg --columns=60 food -09-Oct-31 Panera Bread Expenses:Food $4.50 $4.50 -09-Nov-01 Panera Bread Expenses:Food $4.50 $9.00 -09-Nov-02 Panera Bread Expenses:Food $4.50 $13.50 +09-Oct-31 Panera Bread Expenses:Food $4.50 $4.50 +09-Nov-01 Panera Bread Expenses:Food $4.50 $9.00 +09-Nov-02 Panera Bread Expenses:Food $4.50 $13.50 end test diff --git a/test/manual/transaction-status-2.test b/test/manual/transaction-status-2.test index 6c6d4b8c..94c42a65 100644 --- a/test/manual/transaction-status-2.test +++ b/test/manual/transaction-status-2.test @@ -11,5 +11,5 @@ Assets test reg --columns=60 food --cleared -09-Oct-31 Panera Bread Expenses:Food $4.50 $4.50 +09-Oct-31 Panera Bread Expenses:Food $4.50 $4.50 end test diff --git a/test/manual/transaction-status-3.test b/test/manual/transaction-status-3.test index f50ea23c..f11cd0f7 100644 --- a/test/manual/transaction-status-3.test +++ b/test/manual/transaction-status-3.test @@ -11,6 +11,6 @@ Assets test reg --columns=60 food --uncleared -09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 -09-Nov-02 Panera Bread Expenses:Food $4.50 $9.00 +09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 +09-Nov-02 Panera Bread Expenses:Food $4.50 $9.00 end test diff --git a/test/manual/transaction-status-4.test b/test/manual/transaction-status-4.test index 2ae03c3e..c6f0419b 100644 --- a/test/manual/transaction-status-4.test +++ b/test/manual/transaction-status-4.test @@ -11,5 +11,5 @@ Assets test reg --columns=60 food --pending -09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 +09-Nov-01 Panera Bread Expenses:Food $4.50 $4.50 end test diff --git a/test/python/JournalTest.py b/test/python/JournalTest.py new file mode 100644 index 00000000..e65c671d --- /dev/null +++ b/test/python/JournalTest.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +import unittest + +from ledger import * + +class JournalTestCase(unittest.TestCase): + def tearDown(self): + session.close_journal_files() + + def testBasicRead(self): + journal = read_journal_from_string(""" +2012-03-01 KFC + Expenses:Food $21.34 + Assets:Cash +""") + self.assertEqual(type(journal), Journal) + + for xact in journal: + self.assertEqual(xact.payee, "KFC") + + for post in journal.query("food"): + self.assertEqual(str(post.account), "Expenses:Food") + self.assertEqual(post.amount, Amount("$21.34")) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(JournalTestCase) + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/PostingTest.py b/test/python/PostingTest.py new file mode 100644 index 00000000..f191253e --- /dev/null +++ b/test/python/PostingTest.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import unittest +import exceptions +import operator + +from ledger import * +from StringIO import * +from datetime import * + +class PostingTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_(self): + pass + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(PostingTestCase) + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/TransactionTest.py b/test/python/TransactionTest.py new file mode 100644 index 00000000..66447f87 --- /dev/null +++ b/test/python/TransactionTest.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import unittest +import exceptions +import operator + +from ledger import * +from StringIO import * +from datetime import * + +class JournalTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_(self): + pass + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(JournalTestCase) + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/UnitTests.py b/test/python/UnitTests.py new file mode 100644 index 00000000..388e2229 --- /dev/null +++ b/test/python/UnitTests.py @@ -0,0 +1,12 @@ +from unittest import TextTestRunner, TestSuite + +import JournalTest +import TransactionTest +import PostingTest + +suites = [ + JournalTest.suite(), + TransactionTest.suite(), + PostingTest.suite() +] +TextTestRunner().run(TestSuite(suites)) diff --git a/test/regress/012ADB60.test b/test/regress/012ADB60.test new file mode 100644 index 00000000..443b9e5b --- /dev/null +++ b/test/regress/012ADB60.test @@ -0,0 +1,24 @@ +2005/01/03 * Pay Credit card + Liabilities:CredCard $1,000.00 ; Electronic/ACH Debit + Assets:Current:Checking ; Electronic/ACH Debit + (Virtualaccount) $1,000.00 + +2006/01/03 Gift shop + Expenses:Gifts $46.50 + * Liabilities:CredCard + +2006/01/03 Bike shop + Expenses:Misc $199.00 + * Liabilities:CredCard + (testvirtual) $184.72 + +2006/01/04 Store + Expenses:Misc $49.95 + * Liabilities:CredCard + +test equity -e 2006 +2005/01/03 Opening Balances + Assets:Current:Checking $-1,000.00 + Liabilities:CredCard $1,000.00 + (Virtualaccount) $1,000.00 +end test diff --git a/test/regress/10D19C11.test b/test/regress/10D19C11.test new file mode 100644 index 00000000..be0469ad --- /dev/null +++ b/test/regress/10D19C11.test @@ -0,0 +1,37 @@ +; Test for: ./ledger -f doc/sample.dat -E bal liab' shows the Assets account + += /^Expenses:Books/ + (Liabilities:Taxes) -0.10 + +~ Monthly + Assets:Bank:Checking $500.00 + Income:Salary + +2004/05/01 * Checking balance + Assets:Bank:Checking $1,000.00 + Equity:Opening Balances + +2004/05/01 * Investment balance + Assets:Brokerage 50 AAPL @ $30.00 + Equity:Opening Balances + +2004/05/14 * Pay day + Assets:Bank:Checking $500.00 + Income:Salary + +2004/05/27 Book Store + Expenses:Books $20.00 + Liabilities:MasterCard + +2004/05/27 (100) Credit card company + Liabilities:MasterCard $20.00 + Assets:Bank:Checking + +test -E bal liabilities + $-2.00 Liabilities + 0 MasterCard + $-2.00 Taxes +-------------------- + $-2.00 +end test + diff --git a/test/regress/1384C1D8.test b/test/regress/1384C1D8.test new file mode 100644 index 00000000..77a07b7a --- /dev/null +++ b/test/regress/1384C1D8.test @@ -0,0 +1,27 @@ +@alias OLD1 = NEW1 + +2012-01-01 Something + OLD1 $10.00 + Other + +!alias OLD2 = NEW2 + +2012-01-01 Something + OLD2 $10.00 + Other + +account NEW3 + alias OLD3 + +2012-01-01 Something + OLD3 $10.00 + Other + +test bal + $10.00 NEW1 + $10.00 NEW2 + $10.00 NEW3 + $-30.00 Other +-------------------- + 0 +end test diff --git a/test/regress/14DB77E7.test b/test/regress/14DB77E7.test new file mode 100644 index 00000000..4d8734f9 --- /dev/null +++ b/test/regress/14DB77E7.test @@ -0,0 +1,18 @@ +D 1000.00 GBP + +;P 2011-01-01 EUR 0.8604 GBP +P 2011-02-01 EUR 0.8576 GBP + +2011-01-31 * AdSense earnings + Assets:Receivable:AdSense 11.00 EUR + Income:AdSense + +2011-02-28 * AdSense earnings + Assets:Receivable:AdSense 10.00 EUR + Income:AdSense + +test reg income:adse -X GBP -H +11-Jan-31 AdSense earnings Income:AdSense -11.00 EUR -11.00 EUR +11-Feb-28 Commodities revalued <Revalued> -9.43 GBP -9.43 GBP +11-Feb-28 AdSense earnings Income:AdSense -8.58 GBP -18.01 GBP +end test diff --git a/test/regress/15A80F68.test b/test/regress/15A80F68.test new file mode 100644 index 00000000..0b29b82d --- /dev/null +++ b/test/regress/15A80F68.test @@ -0,0 +1,15 @@ +; Test for: Confusing error message with ledger v3 with invalid input + +2008/03/03 + A (2 FOO @ 10.00 EUR) = 20.00 EUR + B + +test bal -> 1 +__ERROR__ +While parsing file "$FILE", line 4: +While parsing posting: + A (2 FOO @ 10.00 EUR) = 20.00 EUR + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Error: Invalid char '@' +end test + diff --git a/test/regress/178501DC.test b/test/regress/178501DC.test new file mode 100644 index 00000000..b5319ac8 --- /dev/null +++ b/test/regress/178501DC.test @@ -0,0 +1,21 @@ +; Test for: The bal report does not honor -r (ledger bal simon +; would show all accounts, rather than just simon and the related +; account). + +2011/10/26 trader joe's + simon $-50 + alice $-50 + expenses:food:groceries + +test bal -r simon + $-50 alice + $100 expenses:food:groceries +-------------------- + $50 +end test + +test reg -r simon +11-Oct-26 trader joe's alice $-50 $-50 + expense:food:groceries $100 $50 +end test + diff --git a/test/regress/1A546C4D.test b/test/regress/1A546C4D.test new file mode 100644 index 00000000..97adc9de --- /dev/null +++ b/test/regress/1A546C4D.test @@ -0,0 +1,13 @@ +2012/02/22 * Testing invalid amount + Assets:Cash $1,00.00 + Equity:Opening Balances + +test bal -> 1 +__ERROR__ +While parsing file "$FILE", line 2: +While parsing posting: + Assets:Cash $1,00.00 + ^^^^^^^^ +Error: Incorrect use of thousand-mark comma +end test + diff --git a/test/regress/1E192DF6.test b/test/regress/1E192DF6.test new file mode 100644 index 00000000..7d0f8182 --- /dev/null +++ b/test/regress/1E192DF6.test @@ -0,0 +1,57 @@ +; -*- ledger -*- +D 1000,00 PLN +N $ +N h +N PLN +N zł +C 1,00 PLN = 1,00 + +2010-05-19 * ŁUKASZ STELMACH + Assets:Checking:Konto24 GBP 200,00 GBP @ 4,8799 PLN ; fikimiki + Assets:Checking:Konto<30 -975,98 PLN + +2010-05-19 * ŁUKASZ STELMACH + Assets:Checking:Konto24 GBP 200,00 GBP @ 4,8799 PLN ; fikimiki + Assets:Checking:Konto<30 -975,98 PLN + +2010-05-19 * ŁUKASZ STELMACH + Assets:Checking:Konto<30 -975,98 PLN @ 0,204922 GBP + Assets:Checking:Konto24 GBP 200,00 GBP ; fikimiki + +2010-05-19 * ŁUKASZ STELMACH + Assets:Checking:Konto<30 -975,98 PLN @ 0,204922 GBP + Assets:Checking:Konto24 GBP 200,00 GBP ; fikimiki + +2010-05-19 * ŁUKASZ STELMACH + Assets:Checking:Konto24 GBP 200,00 GBP @ 4,8799 PLN ; fikimiki + Assets:Checking:Konto<30 -975,98 PLN + +2010-05-19 * ŁUKASZ STELMACH + Assets:Checking:Konto24 GBP 200,00 GBP @ 4,8799 PLN ; fikimiki + Assets:Checking:Konto<30 -975,98 PLN + +test reg +10-May-19 ŁUKASZ STELMACH As:Checkin:Konto24 GBP 200,00 GBP 200,00 GBP + Asse:Checking:Konto<30 -975.98 -975.98 + 200,00 GBP +10-May-19 ŁUKASZ STELMACH As:Checkin:Konto24 GBP 200,00 GBP -975.98 + 400,00 GBP + Asse:Checking:Konto<30 -975.98 -1951.96 + 400,00 GBP +10-May-19 ŁUKASZ STELMACH Asse:Checking:Konto<30 -975.98 -2927.94 + 400,00 GBP + As:Checkin:Konto24 GBP 200,00 GBP -2927.94 + 600,00 GBP +10-May-19 ŁUKASZ STELMACH Asse:Checking:Konto<30 -975.98 -3903.92 + 600,00 GBP + As:Checkin:Konto24 GBP 200,00 GBP -3903.92 + 800,00 GBP +10-May-19 ŁUKASZ STELMACH As:Checkin:Konto24 GBP 200,00 GBP -3903.92 + 1000,00 GBP + Asse:Checking:Konto<30 -975.98 -4879.9 + 1000,00 GBP +10-May-19 ŁUKASZ STELMACH As:Checkin:Konto24 GBP 200,00 GBP -4879.9 + 1200,00 GBP + Asse:Checking:Konto<30 -975.98 -5855.88 + 1200,00 GBP +end test diff --git a/test/regress/25A099C9.test b/test/regress/25A099C9.test index fc06449b..1ef5ebef 100644 --- a/test/regress/25A099C9.test +++ b/test/regress/25A099C9.test @@ -1,17 +1,43 @@ -test -f $sourcepath/src/amount.h reg -> 7 +test -f src/amount.h reg -> 20 __ERROR__ -While parsing file "$sourcepath/src/amount.h", line 66: +While parsing file "src/amount.h", line 2: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 33: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 37: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 66: Error: No quantity specified for amount -While parsing file "$sourcepath/src/amount.h", line 726: +While parsing file "src/amount.h", line 69: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 83: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 93: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 99: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 121: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 132: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 702: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 732: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 740: +Error: Unexpected whitespace at beginning of line +While parsing file "src/amount.h", line 743: Error: Invalid date/time: line amount_t amoun -While parsing file "$sourcepath/src/amount.h", line 732: +While parsing file "src/amount.h", line 749: Error: Invalid date/time: line string amount_ -While parsing file "$sourcepath/src/amount.h", line 738: +While parsing file "src/amount.h", line 755: Error: Invalid date/time: line string amount_ -While parsing file "$sourcepath/src/amount.h", line 744: +While parsing file "src/amount.h", line 761: Error: Invalid date/time: line string amount_ -While parsing file "$sourcepath/src/amount.h", line 750: +While parsing file "src/amount.h", line 767: Error: Invalid date/time: line std::ostream& -While parsing file "$sourcepath/src/amount.h", line 757: +While parsing file "src/amount.h", line 774: Error: Invalid date/time: line std::istream& +While parsing file "src/amount.h", line 780: +Error: Unexpected whitespace at beginning of line end test diff --git a/test/regress/2CE7DADB.test b/test/regress/2CE7DADB.test new file mode 100644 index 00000000..c6d94b96 --- /dev/null +++ b/test/regress/2CE7DADB.test @@ -0,0 +1,13 @@ +2012-01-01 * Buy AAA + A 1 AAA @ 1.00 EUR + B -1.00 EUR + +2012-02-01 * Buy AAA + A 1 AAA @ 2.00 EUR + B -2.00 EUR + +test --anon pricedb --format "%(date) %(amount)\n" +2012/01/01 1.00 A +2012/02/01 2.00 A +end test + diff --git a/test/regress/3AAB00ED.test b/test/regress/3AAB00ED.test new file mode 100644 index 00000000..217917b3 --- /dev/null +++ b/test/regress/3AAB00ED.test @@ -0,0 +1,23 @@ +; Test for: --sort d not working with -p + +2009-01-01 Opening Balances + Assets:Checking 100.00 EUR + Equity:Opening Balances + +2009-03-01 Test + Expenses:Phone 10.00 EUR + Assets:Checking + +2009-02-01 Test + Expenses:Phone 10.00 EUR + Assets:Checking + +test --sort d -p "until 2010" reg +09-Jan-01 Opening Balances Assets:Checking 100.00 EUR 100.00 EUR + Equit:Opening Balances -100.00 EUR 0 +09-Feb-01 Test Expenses:Phone 10.00 EUR 10.00 EUR + Assets:Checking -10.00 EUR 0 +09-Mar-01 Test Expenses:Phone 10.00 EUR 10.00 EUR + Assets:Checking -10.00 EUR 0 +end test + diff --git a/test/regress/3FE26304.test b/test/regress/3FE26304.test new file mode 100644 index 00000000..7c5b6026 --- /dev/null +++ b/test/regress/3FE26304.test @@ -0,0 +1,74 @@ +N $ +P 2010/09/28 20:43:24 E $3.700 +P 2010/09/28 20:43:25 A $5.230 +P 2010/09/28 20:43:26 D $34.020 +P 2010/09/28 20:43:27 C $12.370 +P 2010/09/28 20:43:28 F $39.700 +P 2010/09/28 20:43:29 B $39.430 +P 2010/09/29 13:50:15 E $3.720 +P 2010/09/29 13:50:15 A $5.240 +P 2010/09/29 13:50:17 D $33.920 +P 2010/09/29 13:50:18 C $12.310 P 2010/09/29 13:50:18 F $39.670 +P 2010/09/29 13:50:19 B $39.830 + +2010/04/04 * Opening Balance + Assets:Sub1 100 A @ $0.01 + Equity:Opening Balances + +2010/04/04 * Opening Balance + Assets:Sub1 100 B @ $32.27 + Equity:Opening Balances + +2010/04/04 * Opening Balance + Assets:Sub1 100 C @ $11.30 + Equity:Opening Balances + +2010/04/04 * Opening Balance + Assets:Sub1 100 D @ $20.30 + Equity:Opening Balances + +2010/04/04 * Opening Balance + Assets:Sub1:Leftovers $6.79 + Equity:Opening Balances + +2010/04/04 * Opening Balance + Assets:Sub1 11 D + Equity:Opening Balances + +2010/04/04 * Opening Balance + Assets:Sub1 100 E @ $2.97 + Equity:Opening Balances + +2010/05/18=2010/05/21 * FOO + Assets:Sub1 200 F @ $27.190 + Expenses:Qux $29.95 + Assets:Sub2 + +2010/07/02 * BAR + Income:D -$169.65 + Assets:Sub2 $32.50 + Assets:Sub1 6 D @ $22.64 + Assets:Sub1:Leftovers + +test bal -X \$ sub1 + $18026.74 Assets:Sub1 + $8.10 Leftovers +-------------------- + $18026.74 +end test + +test reg -X \$ sub1 --now=2012/03/14 +10-Apr-04 Opening Balance Assets:Sub1 $1.00 $1.00 +10-Apr-04 Opening Balance Assets:Sub1 $3227.00 $3228.00 +10-Apr-04 Opening Balance Assets:Sub1 $1130.00 $4358.00 +10-Apr-04 Opening Balance Assets:Sub1 $2030.00 $6388.00 +10-Apr-04 Opening Balance Assets:Sub1:Leftovers $6.79 $6394.79 +10-Apr-04 Opening Balance Assets:Sub1 $223.30 $6618.09 +10-Apr-04 Opening Balance Assets:Sub1 $297.00 $6915.09 +10-May-18 FOO Assets:Sub1 $5438.00 $12353.09 +10-Jul-02 Commodities revalued <Revalued> $259.74 $12612.83 +10-Jul-02 BAR Assets:Sub1 $135.84 $12748.67 + Assets:Sub1:Leftovers $1.31 $12749.98 +10-Sep-29 Commodities revalued <Revalued> $5251.46 $18001.44 +12-Mar-14 Commodities revalued <Revalued> $25.30 $18026.74 +end test diff --git a/test/regress/4509F714.test b/test/regress/4509F714.test new file mode 100644 index 00000000..5c5985f2 --- /dev/null +++ b/test/regress/4509F714.test @@ -0,0 +1,25 @@ +P 2008/01/01 $ €1 + +2008/01/11 LIAT + Expenses:Travel:Airfare $40.00 + Liabilities:MasterCard + +2008/02/05 CTX + Expenses:Travel:Auto €240.38 + Liabilities:MasterCard + +test bal --exchange=€ + €280.38 Expenses:Travel + €40.00 Airfare + €240.38 Auto + €-280.38 Liabilities:MasterCard +-------------------- + 0 +end test + +test bal --exchange=€ --percent + 100.00% Expenses:Travel + 14.27% Airfare + 85.73% Auto + 100.00% Liabilities:MasterCard +end test diff --git a/test/regress/47C579B8.test b/test/regress/47C579B8.test new file mode 100644 index 00000000..f1f9579f --- /dev/null +++ b/test/regress/47C579B8.test @@ -0,0 +1,92 @@ +2008/01/11 LIAT + Expenses:Travel:Airfare 40,00 € + Liabilities:MasterCard + +2008/01/14 cheaptickets.com + Expenses:Travel:Airfare 182,19 € + Liabilities:MasterCard + +2008/02/05 CTX + Expenses:Travel:Auto 240,38 € + Liabilities:MasterCard + +2008/02/05 UNITED + Expenses:Travel:Airfare 238,80 € + Liabilities:MasterCard + +2008/02/05 UNITED + Expenses:Travel:Airfare 238,80 € + Liabilities:MasterCard + +2008/02/22 BUDGET RENT-A-CAR + Expenses:Travel:Auto 40,59 € + Liabilities:MasterCard + +2008/03/16 IBERIA + Expenses:Travel:Airfare 1231,60 € + Liabilities:MasterCard + +2008/03/16 IBERIA + Expenses:Travel:Airfare 1231,60 € + Liabilities:MasterCard + +2008/04/03 AMERICAN + Expenses:Travel:Airfare 155,86 € + Liabilities:MasterCard + +2008/04/03 AMERICAN + Expenses:Travel:Airfare 155,86 € + Liabilities:MasterCard + +2008/04/30 UNITED + Expenses:Travel:Airfare 437,21 € + Liabilities:MasterCard + +2008/04/30 UNITED + Expenses:Travel:Airfare 437,21 € + Liabilities:MasterCard + +2008/08/08 BCIS I-131 FILING FEE- + Expenses:Travel:Passport 170,00 € + Liabilities:MasterCard + +2008/09/06 AMERICAN + Expenses:Travel:Airfare 912,60 € + Liabilities:MasterCard + +2008/09/06 AMERICAN + Expenses:Travel:Airfare 912,60 € + Liabilities:MasterCard + +2008/09/22 AGNT FEE + Expenses:Travel:Airfare 70,00 € + Liabilities:MasterCard + +2008/09/22 DELTA + Expenses:Travel:Airfare 806,20 € + Liabilities:MasterCard + +2008/09/22 DELTA + Expenses:Travel:Airfare 806,20 € + Liabilities:MasterCard + +2008/09/22 LIAT 1974 LIMITED + Expenses:Travel:Airfare 418,34 € + Liabilities:MasterCard + +2008/12/26 U.S. Department of State + Expenses:Travel:Passport 127,00 € + Assets:Checking + +2008/12/26 U.S. Department of State + Expenses:Travel:Passport 127,00 € + Assets:Checking + +test --decimal-comma --percent balance + 100.00% Assets:Checking + 100.00% Expenses:Travel + 92.15% Airfare + 3.13% Auto + 4.72% Passport + 100.00% Liabilities:MasterCard +end test diff --git a/test/regress/4D9288AE.dat b/test/regress/4D9288AE.dat new file mode 100644 index 00000000..758feb19 --- /dev/null +++ b/test/regress/4D9288AE.dat @@ -0,0 +1,4 @@ +2012-03-17 Payee + Expenses:Food $20 + Assets:Cash + diff --git a/test/regress/4D9288AE.py b/test/regress/4D9288AE.py new file mode 100644 index 00000000..4f9c9ba9 --- /dev/null +++ b/test/regress/4D9288AE.py @@ -0,0 +1,4 @@ +import ledger + +for post in ledger.read_journal("test/regress/4D9288AE.dat").query("^expenses:"): + print post.cost diff --git a/test/regress/4D9288AE_py.test b/test/regress/4D9288AE_py.test new file mode 100644 index 00000000..ff2874ce --- /dev/null +++ b/test/regress/4D9288AE_py.test @@ -0,0 +1,3 @@ +test python test/regress/4D9288AE.py +None +end test diff --git a/test/regress/53BCED29.test b/test/regress/53BCED29.test new file mode 100644 index 00000000..77fd39f3 --- /dev/null +++ b/test/regress/53BCED29.test @@ -0,0 +1,29 @@ +D $1,000.00 + +; payroll taxes += /^Payroll/ + Liabilities:Taxes:CFICA 0.062 + Liabilities:Taxes:CMED 0.0145 + $account:EFICA -0.062 + $account:EMED -0.0145 + +; Hourly rates for each employee, as commodity prices. +P 2010/01/01 EONE $15.00 + +; Payroll transactions +2010/05/18 Payroll from May 2nd to May 15th for Employee1 + Assets:Checking 20 EONE + Payroll:Employee1 + +test bal -V + $300.00 Assets:Checking + $-22.95 Liabilities:Taxes + $-18.60 CFICA + $-4.35 CMED + $-277.05 Payroll:Employee1 + $18.60 EFICA + $4.35 EMED +-------------------- + 0 +end test + diff --git a/test/regress/5D92A5EB.test b/test/regress/5D92A5EB.test new file mode 100644 index 00000000..1bdd7256 --- /dev/null +++ b/test/regress/5D92A5EB.test @@ -0,0 +1,34 @@ +~ Monthly from 2010/7/1 + Expenses:Auto:Gas $100.00 + Expenses:Auto:Insurance $100.00 + Expenses:Childcare $100.00 + Expenses:Entertainment:Blizzard $100.00 + Expenses:Entertainment:Netflix $100.00 + Expenses:Groceries $100.00 + Expenses:Utilities:Electric $100.00 + Expenses:Utilities:Water $100.00 + Expenses:Utilities:Sewage $100.00 + Liabilities:Education:ULL $100.00 + Liabilities:Mortgage $100.00 + Assets:Bank:Checking + +test -J reg checking -> 1 +__ERROR__ +While parsing file "$FILE", line 13: +While parsing periodic transaction: +> ~ Monthly from 2010/7/1 +> Expenses:Auto:Gas $100.00 +> Expenses:Auto:Insurance $100.00 +> Expenses:Childcare $100.00 +> Expenses:Entertainment:Blizzard $100.00 +> Expenses:Entertainment:Netflix $100.00 +> Expenses:Groceries $100.00 +> Expenses:Utilities:Electric $100.00 +> Expenses:Utilities:Water $100.00 +> Expenses:Utilities:Sewage $100.00 +> Liabilities:Education:ULL $100.00 +> Liabilities:Mortgage $100.00 +> Assets:Bank:Checking +Error: Posting with null amount's account may be mispelled: + "Expenses:Entertainment:Blizzard $100.00" +end test diff --git a/test/regress/605A410D.test b/test/regress/605A410D.test new file mode 100644 index 00000000..6943939a --- /dev/null +++ b/test/regress/605A410D.test @@ -0,0 +1,32 @@ += expr amount > 500 and account =~ /Employer:One/ + (Virtual) 1 + +2012-01-16 KFC + Employer:One $1,000.00 + Assets:Cash + += expr amount>500 and account =~ /Employer:Two/ + (Virtual) 10 + +2012-02-16 KFC + Employer:Two $1,000.00 + Assets:Cash + += Employer:Three and expr amount>500 + (Virtual) 100 + +2012-03-16 KFC + Employer:Three $1,000.00 + Assets:Cash + +test reg +12-Jan-16 KFC Employer:One $1,000.00 $1,000.00 + Assets:Cash $-1,000.00 0 + (Virtual) $1,000.00 $1,000.00 +12-Feb-16 KFC Employer:Two $1,000.00 $2,000.00 + Assets:Cash $-1,000.00 $1,000.00 + (Virtual) $10,000.00 $11,000.00 +12-Mar-16 KFC Employer:Three $1,000.00 $12,000.00 + Assets:Cash $-1,000.00 $11,000.00 + (Virtual) $100,000.00 $111,000.00 +end test diff --git a/test/regress/6188B0EC.test b/test/regress/6188B0EC.test new file mode 100644 index 00000000..b2aec910 --- /dev/null +++ b/test/regress/6188B0EC.test @@ -0,0 +1,10 @@ +; Test for: No error message if the parser cannot find an included file + +!include 6188B0EC-does-not-exist.dat + +test bal -> 1 +__ERROR__ +While parsing file "$FILE", line 3: +Error: File to include was not found: "$sourcepath/test/regress/6188B0EC-does-not-exist.dat" +end test + diff --git a/test/regress/640D3205.test b/test/regress/640D3205.test new file mode 100644 index 00000000..f43b850f --- /dev/null +++ b/test/regress/640D3205.test @@ -0,0 +1,20 @@ +; Test for: "print" command filters out the "balance assertions" + +2008/12/31 * Interest + Assets:Brokerage $800.00 + Income:Somewhere + +2008/12/31 * Interest + Assets:Brokerage $200.00 = $1,000.00 + Income:Somewhere + +test print +2008/12/31 * Interest + Assets:Brokerage $800.00 + Income:Somewhere + +2008/12/31 * Interest + Assets:Brokerage $200.00 = $1000.00 + Income:Somewhere +end test + diff --git a/test/regress/65FECA4D.test b/test/regress/65FECA4D.test new file mode 100644 index 00000000..3e575961 --- /dev/null +++ b/test/regress/65FECA4D.test @@ -0,0 +1,12 @@ +--now=2012-02-28 + +Y 2012 + +2/29 E-trade Bank + Expenses:Food $20 + Assets:Cash + +test reg +12-Feb-29 E-trade Bank Expenses:Food $20 $20 + Assets:Cash $-20 0 +end test diff --git a/test/regress/6D9066DD.test b/test/regress/6D9066DD.test new file mode 100644 index 00000000..aa885f7d --- /dev/null +++ b/test/regress/6D9066DD.test @@ -0,0 +1,17 @@ +2009/09/23 * (EFT) Elec Ext Deposit AMAZON.COM FZXXOLTQ - Retail dis payments.amazon.com + Assets:Checking $39.05 + Assets:Receivable:Amazon + +2007/10/15 * FOO ; :USA: + Assets:NRL:Checking $1,726.18 + Assets:Receivable:CEG ; [2007/10/05] + +test print +2009/09/23 * (EFT) Elec Ext Deposit AMAZON.COM FZXXOLTQ - Retail dis payments.amazon.com + Assets:Checking $39.05 + Assets:Receivable:Amazon + +2007/10/15 * FOO ; :USA: + Assets:NRL:Checking $1,726.18 + Assets:Receivable:CEG ; [2007/10/05] +end test diff --git a/test/regress/6E041C52.test b/test/regress/6E041C52.test new file mode 100644 index 00000000..0a56dd70 --- /dev/null +++ b/test/regress/6E041C52.test @@ -0,0 +1,8 @@ +2012-03-16 KFC + Expenses:E of March: End of April $100.00 + Assets:Cash + +test reg +12-Mar-16 KFC Ex:E of: End of April $100.00 $100.00 + Assets:Cash $-100.00 0 +end test diff --git a/test/regress/751B2357.test b/test/regress/751B2357.test new file mode 100644 index 00000000..2b0f9a16 --- /dev/null +++ b/test/regress/751B2357.test @@ -0,0 +1,17 @@ +01.10.2011 4b4e2a89 + ef9d9585:efa1fb7b:22845e93:0e3763f0 2,00 A + 2c166ff7:d34e3aa1:8a5075b3:56f3c726 + +01.10.2011 15983995 + eb78b6c0:a2857de3:d6d8ea07:6688fc4e 2,58 A + ba3ffe56:c3ba36a5:aa63399f:e9e1d043 + +test print --date-format=%d.%m.%Y --input-date-format=%d.%m.%Y +01.10.2011 4b4e2a89 + ef9d9585:efa1fb7b:22845e93:0e3763f0 2,00 A + 2c166ff7:d34e3aa1:8a5075b3:56f3c726 + +01.10.2011 15983995 + eb78b6c0:a2857de3:d6d8ea07:6688fc4e 2,58 A + ba3ffe56:c3ba36a5:aa63399f:e9e1d043 +end test diff --git a/test/regress/786A3DD0.test b/test/regress/786A3DD0.test new file mode 100644 index 00000000..051f6382 --- /dev/null +++ b/test/regress/786A3DD0.test @@ -0,0 +1,17 @@ +D 1000.00 EUR + +2011-02-27 * Australia + A -100.00 AUD @ 0.746 EUR + B + +2012-03-12 * Withdrawal + Assets:Cash USD 200.00 + Expenses:Banking:Fees USD 2.50 + Assets:Chequing CAD -203.42 + Epenses:Banking:Fees CAD 2.00 + Assets:Chqeuing CAD -2.00 + +test pricedb +P 2011/02/27 00:00:00 AUD 0.746 EUR +P 2012/03/12 00:00:00 USD CAD 1.00454320987654321 +end test diff --git a/test/regress/78AB4B87.dat b/test/regress/78AB4B87.dat new file mode 100644 index 00000000..45b3028a --- /dev/null +++ b/test/regress/78AB4B87.dat @@ -0,0 +1,14 @@ +D 1000.00 EUR +P 2011-01-01 GBP 1.2 EUR + +2011-01-01 * Opening balance + Assets:Bank 10.00 GBP + Equity:Opening balance + +2012-01-02 * Test + Assets:Bank 5.00 GBP + Income:Whatever + +2012-01-03 * Test + Assets:Bank + Income:Whatever -5.00 EUR @ 0.8733 GBP diff --git a/test/regress/78AB4B87.py b/test/regress/78AB4B87.py new file mode 100644 index 00000000..fed95b54 --- /dev/null +++ b/test/regress/78AB4B87.py @@ -0,0 +1,27 @@ +import ledger + +eur = ledger.commodities.find_or_create('EUR') + +total_eur = ledger.Amount("0.00 EUR") +total_gbp = ledger.Amount("0.00 GBP") +total = ledger.Amount("0.00 EUR") + +for post in ledger.read_journal("test/regress/78AB4B87.dat").query("^income:"): + print post.amount + print post.amount.commodity + if post.amount.commodity == "EUR": + total_eur += post.amount + elif post.amount.commodity == "GBP": + total_gbp += post.amount + + a = post.amount.value(eur) + if a: + print "Total is presently: (%s)" % total + print "Converted to EUR: (%s)" % a + total += a + print "Total is now: (%s)" % total + else: + print "Cannot convert '%s'" % post.amount + print + +print total diff --git a/test/regress/78AB4B87_py.test b/test/regress/78AB4B87_py.test new file mode 100644 index 00000000..8f847145 --- /dev/null +++ b/test/regress/78AB4B87_py.test @@ -0,0 +1,15 @@ +test python test/regress/78AB4B87.py +-5.00 GBP +GBP +Total is presently: (0.00 EUR) +Converted to EUR: (-5.73 EUR) +Total is now: (-5.73 EUR) + +-5.00 EUR {0.8733 GBP} [2012/01/03] +EUR +Total is presently: (-5.73 EUR) +Converted to EUR: (-5.00 EUR) +Total is now: (-10.73 EUR) + +-10.73 EUR +end test diff --git a/test/regress/82763D86.test b/test/regress/82763D86.test new file mode 100644 index 00000000..e580077d --- /dev/null +++ b/test/regress/82763D86.test @@ -0,0 +1,35 @@ +; Test for: 'ledger -f doc/sample.dat reg -s -n liab' elides too much +; It collapses the account down to "<Total>", even though there was +; only one account! + += /^Expenses:Books/ + (Liabilities:Taxes) -0.10 + +~ Monthly + Assets:Bank:Checking $500.00 + Income:Salary + +2004/05/01 * Checking balance + Assets:Bank:Checking $1,000.00 + Equity:Opening Balances + +2004/05/01 * Investment balance + Assets:Brokerage 50 AAPL @ $30.00 + Equity:Opening Balances + +2004/05/14 * Pay day + Assets:Bank:Checking $500.00 + Income:Salary + +2004/05/27 Book Store + Expenses:Books $20.00 + Liabilities:MasterCard + +2004/05/27 (100) Credit card company + Liabilities:MasterCard $20.00 + Assets:Bank:Checking + +test -s reg liabilities +04-May-27 - 04-May-27 (Liabilities:Taxes) $-2.00 $-2.00 +end test + diff --git a/test/regress/83B4A0E5.test b/test/regress/83B4A0E5.test new file mode 100644 index 00000000..f9402a2d --- /dev/null +++ b/test/regress/83B4A0E5.test @@ -0,0 +1,43 @@ +P 2012-03-01 EUR $2 +P 2012-03-01 GBP $2 + +2012-03-05 KFC + Expenses:Food 10 EUR + Assets:Cash + +2012-03-10 KFC + Expenses:Food 10 GBP + Assets:Cash + +test reg food +12-Mar-05 KFC Expenses:Food 10 EUR 10 EUR +12-Mar-10 KFC Expenses:Food 10 GBP 10 EUR + 10 GBP +end test + +test reg food -V +12-Mar-05 KFC Expenses:Food $20 $20 +12-Mar-10 KFC Expenses:Food $20 $40 +end test + +test reg food -X '$' +12-Mar-05 KFC Expenses:Food $20 $20 +12-Mar-10 KFC Expenses:Food $20 $40 +end test + +test reg food -X '$,GBP' +12-Mar-05 KFC Expenses:Food $20 $20 +12-Mar-10 KFC Expenses:Food 10 GBP $20 + 10 GBP +end test + +test reg food -X '$!,GBP' +12-Mar-05 KFC Expenses:Food $20 $20 +12-Mar-10 KFC Expenses:Food $20 $40 +end test + +test reg food -X '$,EUR' +12-Mar-05 KFC Expenses:Food 10 EUR 10 EUR +12-Mar-10 KFC Expenses:Food $20 $20 + 10 EUR +end test diff --git a/test/regress/854150DF.test b/test/regress/854150DF.test new file mode 100644 index 00000000..7133e183 --- /dev/null +++ b/test/regress/854150DF.test @@ -0,0 +1,25 @@ +2011-11-10 * test + A:B:C 12.50 GBP + A:C + +test bal --flat -d "depth>=2" + 12.50 GBP A:B:C + -12.50 GBP A:C +-------------------- + 0 +end test + +test bal --flat -d "depth>1" + 12.50 GBP A:B:C + -12.50 GBP A:C +-------------------- + 0 +end test + +test bal --flat -d "depth>2" + 12.50 GBP A:B:C +end test + +test bal --flat -d "depth==2" + -12.50 GBP A:C +end test diff --git a/test/regress/889BB167.test b/test/regress/889BB167.test new file mode 100644 index 00000000..02e25ab6 --- /dev/null +++ b/test/regress/889BB167.test @@ -0,0 +1,17 @@ +D 1000.00 GBP + +P 2011-01-01 EUR 0.8604 GBP +P 2011-02-01 EUR 0.8576 GBP + +2011-01-31 * AdSense earnings + Assets:Receivable:AdSense 11.00 EUR + Income:AdSense + +2011-02-28 * AdSense earnings + Assets:Receivable:AdSense 10.00 EUR + Income:AdSense + +test reg income:adse -X GBP -H +11-Jan-31 AdSense earnings Income:AdSense -9.46 GBP -9.46 GBP +11-Feb-28 AdSense earnings Income:AdSense -8.58 GBP -18.04 GBP +end test diff --git a/test/regress/89233B6D-a.dat b/test/regress/89233B6D-a.dat new file mode 100644 index 00000000..01d00e9a --- /dev/null +++ b/test/regress/89233B6D-a.dat @@ -0,0 +1,4 @@ +1994/01/02 * Salary + Asssets:Bank:Checking 200.00 + Income:Salary -200.00 + diff --git a/test/regress/89233B6D-b.dat b/test/regress/89233B6D-b.dat new file mode 100644 index 00000000..f54dc66f --- /dev/null +++ b/test/regress/89233B6D-b.dat @@ -0,0 +1,4 @@ +1994/01/02 * Rent + Expenses:Rent 100.00 + Asssets:Bank:Checking -100.00 + diff --git a/test/regress/89233B6D.test b/test/regress/89233B6D.test new file mode 100644 index 00000000..e6bd38f5 --- /dev/null +++ b/test/regress/89233B6D.test @@ -0,0 +1,13 @@ +!apply account A +!include 89233B6D-a.dat +!end + +!apply account B +!include 89233B6D-b.dat +!end + +test reg "^A:" +94-Jan-02 Salary A:Assset:Bank:Checking 200 200 + A:Income:Salary -200 0 +end test + diff --git a/test/regress/8CE88DB4.test b/test/regress/8CE88DB4.test new file mode 100644 index 00000000..52fe0a9b --- /dev/null +++ b/test/regress/8CE88DB4.test @@ -0,0 +1,11 @@ +2010-01-01 * Test + Expenses:Food 100.00 EUR + Assets:Cash -100.00 EUR + +2011-07-30 * Exchange EUR to BAM + Assets:Cash -22.00 EUR + Assets:Cash 44.00 BAM + +test pricedb +P 2011/07/30 00:00:00 EUR 2.00 BAM +end test diff --git a/test/regress/8EAF77C0.test b/test/regress/8EAF77C0.test new file mode 100644 index 00000000..f0a2829c --- /dev/null +++ b/test/regress/8EAF77C0.test @@ -0,0 +1,17 @@ +2011/08/05 Rehab Donation + Asset:Bank:Boi:Current:Dk 10 + Expense:Misc:Charity + + 2011/08/07 Net Salary + Asset:Bank:Boi:Savings:Dk -3016.24 + Income:NetSalary:Dk + +2011/08/30 Net Salary + Asset:Bank:Boi:Savings:Dk -3016.24 + Income:NetSalary:Dk + +test reg -> 1 +__ERROR__ +While parsing file "$FILE", line 5: +Error: Unexpected whitespace at beginning of line +end test diff --git a/test/regress/9188F587.py b/test/regress/9188F587.py new file mode 100644 index 00000000..50195252 --- /dev/null +++ b/test/regress/9188F587.py @@ -0,0 +1,27 @@ +import ledger + +eur = ledger.commodities.find_or_create('EUR') + +total_eur = ledger.Amount("0.00 EUR") +total_gbp = ledger.Amount("0.00 GBP") +total = ledger.Amount("0.00 EUR") + +for post in ledger.read_journal("test/regress/78AB4B87.dat").query("^income:"): + print post.amount + print post.amount.commodity + if post.amount.commodity == "EUR": + total_eur += post.amount + elif post.amount.commodity == "GBP": + total_gbp += post.amount + + a = post.amount.value(eur, post.date) + if a: + print "Total is presently: (%s)" % total + print "Converted to EUR: (%s)" % a + total += a + print "Total is now: (%s)" % total + else: + print "Cannot convert '%s'" % post.amount + print + +print total diff --git a/test/regress/9188F587_py.test b/test/regress/9188F587_py.test new file mode 100644 index 00000000..28bb34ff --- /dev/null +++ b/test/regress/9188F587_py.test @@ -0,0 +1,15 @@ +test python test/regress/9188F587.py +-5.00 GBP +GBP +Total is presently: (0.00 EUR) +Converted to EUR: (-6.00 EUR) +Total is now: (-6.00 EUR) + +-5.00 EUR {0.8733 GBP} [2012/01/03] +EUR +Total is presently: (-6.00 EUR) +Converted to EUR: (-5.00 EUR) +Total is now: (-11.00 EUR) + +-11.00 EUR +end test diff --git a/test/regress/95350193.test b/test/regress/95350193.test new file mode 100644 index 00000000..dadb39cf --- /dev/null +++ b/test/regress/95350193.test @@ -0,0 +1,6 @@ +2011-11-08 * Test + Assets:Voucher:Amazon 137.87 GBP (48H5) + Assets:Cash -137.87 GBP + +test pricedb +end test diff --git a/test/regress/96A8E4A1.test b/test/regress/96A8E4A1.test new file mode 100644 index 00000000..93fb55d2 --- /dev/null +++ b/test/regress/96A8E4A1.test @@ -0,0 +1,10 @@ +2011-01-31 * Test + Expenses:Travel 1 "Spr MegaBonus" + Assets:Voucher + +test -X EUR -H bal + -1 "Spr MegaBonus" Assets:Voucher + 1 "Spr MegaBonus" Expenses:Travel +-------------------- + 0 +end test diff --git a/test/regress/9E0E606D.test b/test/regress/9E0E606D.test new file mode 100644 index 00000000..86b8e36f --- /dev/null +++ b/test/regress/9E0E606D.test @@ -0,0 +1,19 @@ +D 1000.00 GBP + +P 2011-02-01 EUR 0.8576 GBP +P 2011-03-01 EUR 0.8612 GBP +P 2011-04-01 EUR 0.8510 GBP + +2011-01-31 * AdSense earnings + Assets:Receivable:AdSense 11.00 EUR + Income:AdSense + +2011-02-28 * AdSense earnings + Assets:Receivable:AdSense 10.00 EUR + Income:AdSense + +test reg income:ad -X GBP -H +11-Jan-31 AdSense earnings Income:AdSense -11.00 EUR -11.00 EUR +11-Feb-28 Commodities revalued <Revalued> -9.43 GBP -9.43 GBP +11-Feb-28 AdSense earnings Income:AdSense -8.58 GBP -18.01 GBP +end test diff --git a/test/regress/A560FDAD.test b/test/regress/A560FDAD.test new file mode 100644 index 00000000..ee19e71e --- /dev/null +++ b/test/regress/A560FDAD.test @@ -0,0 +1,85 @@ +2012-01-01 * Opening balance + Assets:Current 17.43 EUR + Assets:Investments 200 "LU02" @ 24.77 EUR + Assets:Investments 58 "LU02" @ 24.79900855 EUR + Equity:Opening balance + +2012-01-01 * Opening balance + Assets:Pension 785.44 GBP + Assets:Pension 97.0017 "H2" @ 5.342999720204 GBP + Assets:Pension 4.3441 "H1" @ 5.289999915108 GBP + Equity:Opening balance + +2012-01-01 * Opening balance: misc + Assets:Piggy bank 3.51 GBP + Equity:Opening balance + +2012-01-01 * Opening balance + Assets:Rewards 9836 AAdvantage + Equity:Opening balance + +2012-01-03 * Receivable + Assets:Current + Assets:Receivable -161.06 EUR + Assets:Receivable -9.99 GBP @@ 11.65 EUR + +2012-01-27 * Test + Income:Test -2759.50 GBP + Income:Test -110.76 GBP + Assets:Foo 345.57 GBP + Expenses:Test 16.47 GBP + Expenses:Test 6.33 GBP + Expenses:Test 261.39 GBP + Assets:Current + +test reg -X EUR -H +12-Jan-01 Opening balance Assets:Current 17.43 EUR 17.43 EUR + Assets:Investments 4959.80 EUR 4977.23 EUR + Assets:Investments 1438.34 EUR 6415.57 EUR + Equity:Opening balance -6409.77 EUR 5.80 EUR +12-Jan-01 Opening balance Assets:Pension 785.44 GBP 5.80 EUR + 785.44 GBP + Assets:Pension 97.0017 H2 5.80 EUR + 785.44 GBP + 97.0017 H2 + Assets:Pension 4.3441 H1 5.80 EUR + 785.44 GBP + 4.3441 H1 + 97.0017 H2 + Equity:Opening balance -1326.70 GBP 5.80 EUR + -541.26 GBP + 4.3441 H1 + 97.0017 H2 +12-Jan-01 Opening balance: misc Assets:Piggy bank 3.51 GBP 5.80 EUR + -537.75 GBP + 4.3441 H1 + 97.0017 H2 + Equity:Opening balance -3.51 GBP 5.80 EUR + -541.26 GBP + 4.3441 H1 + 97.0017 H2 +12-Jan-01 Opening balance Assets:Rewards 9836 AAdvantage 9836 AAdvantage + 5.80 EUR + -541.26 GBP + 4.3441 H1 + 97.0017 H2 + Equity:Opening balance -9836 AAdvantage 5.80 EUR + -541.26 GBP + 4.3441 H1 + 97.0017 H2 +12-Jan-03 Commodities revalued <Revalued> 0 5.80 EUR +12-Jan-03 Receivable Assets:Current 172.71 EUR 178.51 EUR + Assets:Receivable -161.06 EUR 17.45 EUR + Assets:Receivable -11.65 EUR 5.80 EUR +12-Jan-27 Test <Adjustment> 0.01 EUR 5.81 EUR + Income:Test -3218.04 EUR -3212.23 EUR + <Adjustment> -0.01 EUR -3212.24 EUR + Income:Test -129.16 EUR -3341.40 EUR + Assets:Foo 402.99 EUR -2938.41 EUR + Expenses:Test 19.21 EUR -2919.20 EUR + Expenses:Test 7.38 EUR -2911.82 EUR + <Adjustment> 0.01 EUR -2911.81 EUR + Expenses:Test 304.82 EUR -2606.99 EUR + <Adjustment> -0.01 EUR -2607.00 EUR + Assets:Current 2612.80 EUR 5.80 EUR +end test diff --git a/test/regress/A8FCC765.dat b/test/regress/A8FCC765.dat new file mode 100644 index 00000000..abc51a0a --- /dev/null +++ b/test/regress/A8FCC765.dat @@ -0,0 +1,2 @@ +P 2012-03-16 06:47:12 CAD $2.50 +P 2012-03-17 06:47:12 CAD $3.50 diff --git a/test/regress/A8FCC765.test b/test/regress/A8FCC765.test new file mode 100644 index 00000000..1adf6053 --- /dev/null +++ b/test/regress/A8FCC765.test @@ -0,0 +1,8 @@ +2012-03-17 KFC + Expenses:Food 20 CAD + Assets:Cash + +test pricedb --price-db test/regress/A8FCC765.dat +P 2012/03/16 06:47:12 CAD $2.5 +P 2012/03/17 06:47:12 CAD $3.5 +end test diff --git a/test/regress/ACE05ECE.test b/test/regress/ACE05ECE.test new file mode 100644 index 00000000..72ea562b --- /dev/null +++ b/test/regress/ACE05ECE.test @@ -0,0 +1,6 @@ +i 2011/07/20 17:00:00 Hello Work project +o 2011/07/21 01:00:00 Hello + +test reg Hello +11-Jul-20 Work project (Hello) 8.00h 8.00h +end test diff --git a/test/regress/AEDE9734.test b/test/regress/AEDE9734.test new file mode 100644 index 00000000..cd2245b8 --- /dev/null +++ b/test/regress/AEDE9734.test @@ -0,0 +1,12 @@ +2011-02-23 Rocket Fuel + Expense:Travel $100000000.00 ; trip: Moon + Asset:NASA + +2011-02-23 Liquid Oxygen + Expense:Travel $232233223.00 ; trip: Moon + Asset:NASA + +test bal --group-by "tag('trip')" +Moon + $332233223.00 Expense:Travel +end test diff --git a/test/regress/AFAFB804.test b/test/regress/AFAFB804.test new file mode 100644 index 00000000..472540fb --- /dev/null +++ b/test/regress/AFAFB804.test @@ -0,0 +1,57 @@ +; Test for: ledger should allow sorting by multiple criteria, like: +; -S date,payee + +2010-02-09 * Z + A $10 + B + +2010-02-09 * Y + B $10 + C + +2010-02-09 * X + C $10 + D + +2010-02-10 * Z + A $15 + B + +2010-02-10 * Y + B $15 + C + +2010-02-10 * X + C $15 + D + +test reg -S date,payee +10-Feb-09 X C $10 $10 + D $-10 0 +10-Feb-09 Y B $10 $10 + C $-10 0 +10-Feb-09 Z A $10 $10 + B $-10 0 +10-Feb-10 X C $15 $15 + D $-15 0 +10-Feb-10 Y B $15 $15 + C $-15 0 +10-Feb-10 Z A $15 $15 + B $-15 0 +end test + +test reg -S payee,date +10-Feb-09 X C $10 $10 + D $-10 0 +10-Feb-10 X C $15 $15 + D $-15 0 +10-Feb-09 Y B $10 $10 + C $-10 0 +10-Feb-10 Y B $15 $15 + C $-15 0 +10-Feb-09 Z A $10 $10 + B $-10 0 +10-Feb-10 Z A $15 $15 + B $-15 0 +end test + diff --git a/test/regress/BFD3FBE1.test b/test/regress/BFD3FBE1.test new file mode 100644 index 00000000..0dbda2c7 --- /dev/null +++ b/test/regress/BFD3FBE1.test @@ -0,0 +1,16 @@ +2011-01-01 * Opening balance + Assets:Investment 100 "AAA" @ 16.58900489 EUR + Assets:Investments 5 "BBB" @ 24.79900855 EUR + Equity:Opening balance + +2011-02-10 * Reimbursement: Taxi / Subway / Bus / Train + Assets:A 1.59 GBP + Assets:B -1.80 EUR @ 0.884955752212389381 GBP + +test reg -X EUR -H +11-Jan-01 Opening balance Assets:Investment 1658.90 EUR 1658.90 EUR + Assets:Investments 124.00 EUR 1782.90 EUR + Equity:Opening balance -1782.90 EUR 0 +11-Feb-10 Reimbursement: Taxi.. Assets:A 1.80 EUR 1.80 EUR + Assets:B -1.80 EUR 0 +end test diff --git a/test/regress/C19E4E9B.test b/test/regress/C19E4E9B.test new file mode 100644 index 00000000..4837b4cd --- /dev/null +++ b/test/regress/C19E4E9B.test @@ -0,0 +1,18 @@ +2012-01-01=2012-01-02 * Buy AAA + A 1 AAA @ 1.00 EUR + B -1.00 EUR + +2012-02-01 * Buy AAA + A 1 AAA @ 2.00 EUR + B -2.00 EUR + +test reg --format "%S: %d %P %t %T\n" +$FILE: 2012/01/01 Buy AAA 1 AAA 1 AAA +$FILE: 2012/01/01 Buy AAA -1.00 EUR 1 AAA +-1.00 EUR +$FILE: 2012/02/01 Buy AAA 1 AAA 2 AAA +-1.00 EUR +$FILE: 2012/02/01 Buy AAA -2.00 EUR 2 AAA +-3.00 EUR +end test + diff --git a/test/regress/C927CFFE.test b/test/regress/C927CFFE.test new file mode 100644 index 00000000..d455b480 --- /dev/null +++ b/test/regress/C927CFFE.test @@ -0,0 +1,43 @@ + +2010/02/09 * Test 1 + A $10 + B + +2010/02/10 * Test 2 + B $10 + C + +2010/02/11 * Test 3 + C $10 + D + +test reg +test -l "date>=[2010/02/10]" reg +10-Feb-10 Test 2 B $10 $10 + C $-10 0 +10-Feb-11 Test 3 C $10 $10 + D $-10 0 +end test + +test -l "date<=[2010/02/10]" reg +10-Feb-09 Test 1 A $10 $10 + B $-10 0 +10-Feb-10 Test 2 B $10 $10 + C $-10 0 +end test + +test -l "date==[2010/02/10]" reg +10-Feb-10 Test 2 B $10 $10 + C $-10 0 +end test + +test -l "date>[2010/02/10]" reg +10-Feb-11 Test 3 C $10 $10 + D $-10 0 +end test + +test -l "date<[2010/02/10]" reg +10-Feb-09 Test 1 A $10 $10 + B $-10 0 +end test + diff --git a/test/regress/C9D593B3.test b/test/regress/C9D593B3.test new file mode 100644 index 00000000..1cb73080 --- /dev/null +++ b/test/regress/C9D593B3.test @@ -0,0 +1,23 @@ +2012-03-16 KFC + Expenses:Food $20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food $20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food $20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food $20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food $20 + Assets:Cash + +test payees +KFC +end test diff --git a/test/regress/CEECC0B0.test b/test/regress/CEECC0B0.test new file mode 100644 index 00000000..1465b8c3 --- /dev/null +++ b/test/regress/CEECC0B0.test @@ -0,0 +1,19 @@ +2012-01-01 * Opening Balances + Assets:Cash 100.00 EUR + Equity:Opening balances -100.00 EUR + +2012-01-02 * Buy AAA + Assets:Investments 1 AAA @ 10.00 EUR + Assets:Cash -10.00 EUR + +2012-01-03 * Sell AAA + Assets:Investments -1 AAA @ 10.00 EUR + Assets:Cash 10.00 EUR + + +test equity +2012/01/03 Opening Balances + Assets:Cash 100.00 EUR + Equity:Opening balances +end test + diff --git a/test/regress/CFE5D8AA.test b/test/regress/CFE5D8AA.test new file mode 100644 index 00000000..857dad13 --- /dev/null +++ b/test/regress/CFE5D8AA.test @@ -0,0 +1,20 @@ +~ monthly + assets:checking $1,000.00 + income:work:salary $-1,000.00 + +~ monthly + ; note + assets:checking $1,000.00 + income:work:salary $-1,000.00 + +~ monthly + assets:checking $1,000.00 + income:work:salary + +~ monthly + ; note + assets:checking $1,000.00 + income:work:salary + +test reg +end test diff --git a/test/regress/D51BFF74.test b/test/regress/D51BFF74.test new file mode 100644 index 00000000..a13af897 --- /dev/null +++ b/test/regress/D51BFF74.test @@ -0,0 +1,24 @@ +2012-03-16 KFC + Expenses:Food $-20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food $- 20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food -$20 + Assets:Cash + +2012-03-16 KFC + Expenses:Food - $20 + Assets:Cash + +test reg -> 1 +__ERROR__ +While parsing file "$FILE", line 6: +While parsing posting: + Expenses:Food $- 20 + ^^^^^ +Error: No quantity specified for amount +end test diff --git a/test/regress/D943AE0F.test b/test/regress/D943AE0F.test index 960fbe13..10082f75 100644 --- a/test/regress/D943AE0F.test +++ b/test/regress/D943AE0F.test @@ -6,7 +6,7 @@ D 1000.00 EUR P 2008/04/20 00:00:00 CAD 1.20 EUR -test reg -V +test reg -V --now=2008/04/20 08-Apr-15 Paid expenses back .. Exp:Cie-Reimbursements 2200.00 EUR 2200.00 EUR Assets:Checking -2200.00 EUR 0 08-Apr-20 Commodities revalued <Revalued> 200.00 EUR 200.00 EUR diff --git a/test/regress/D9C8EB08.test b/test/regress/D9C8EB08.test new file mode 100644 index 00000000..fa02431b --- /dev/null +++ b/test/regress/D9C8EB08.test @@ -0,0 +1,16 @@ +; Test for: Using ! erroneously in a data file causes a segfault + +! Assets:Cash + +2008/01/01 January + Expenses:Books $10.00 + Assets:Cash + +!end + +test bal -> 1 +__ERROR__ +While parsing file "$FILE", line 9: +Error: 'end' or 'end apply' found, but no enclosing 'apply' directive +end test + diff --git a/test/regress/DB490507.test b/test/regress/DB490507.test new file mode 100644 index 00000000..24443d2a --- /dev/null +++ b/test/regress/DB490507.test @@ -0,0 +1,25 @@ +2001/11/07=2001/11/04 * Autoroutes du Sud de la France + Dépense:Vacances:Voyage ; 14F Tlse-Montauban, 8F Montauban-Caussade, 8F Caussade-Montauban, 14F Montauban-Tlse + Actif:Courant:BnpCc -6,71 € + +2008/01/20 * La Poste + Equity + Actif:Courant:LaPosteLivretA 10,00 € + +2008/01/20 * La Poste + Revenu:Invest:Exonéré + Actif:Courant:LaPosteLivretA 25,24 € = 35,24 € + +test print --decimal-comma --columns=999 +2001/11/07=2001/11/04 * Autoroutes du Sud de la France + Dépense:Vacances:Voyage ; 14F Tlse-Montauban, 8F Montauban-Caussade, 8F Caussade-Montauban, 14F Montauban-Tlse + Actif:Courant:BnpCc -6,71 € + +2008/01/20 * La Poste + Equity + Actif:Courant:LaPosteLivretA 10,00 € + +2008/01/20 * La Poste + Revenu:Invest:Exonéré + Actif:Courant:LaPosteLivretA 25,24 € = 35,24 € +end test diff --git a/test/regress/DDB54BB8.test b/test/regress/DDB54BB8.test new file mode 100644 index 00000000..7d72043c --- /dev/null +++ b/test/regress/DDB54BB8.test @@ -0,0 +1,18 @@ +~ Monthly + Aufwand:Bargeld 0,30€ + Aktiva:Bank:Girokonto -0,40€ + +test bal -> 1 +__ERROR__ +While parsing file "$FILE", line 3: +Unbalanced remainder is: + -0,10€ +Amount to balance against: + 0,30€ +While parsing periodic transaction: +> ~ Monthly +> Aufwand:Bargeld 0,30€ +> Aktiva:Bank:Girokonto -0,40€ +Error: Transaction does not balance +end test + diff --git a/test/regress/E2E479BC.test b/test/regress/E2E479BC.test new file mode 100644 index 00000000..8216028a --- /dev/null +++ b/test/regress/E2E479BC.test @@ -0,0 +1,17 @@ +; Test for: ledger used to show multiple "Income:Unknown" in this +; case in the past, which it shouldn't. + +2009/01/01 Sample + Expenses:Alpha 10 A + Expenses:Beta 10 B + Expenses:Gamma 10 C + Income:Unknown + +test print +2009/01/01 Sample + Expenses:Alpha 10 A + Expenses:Beta 10 B + Expenses:Gamma 10 C + Income:Unknown +end test + diff --git a/test/regress/EA18D948.test b/test/regress/EA18D948.test new file mode 100644 index 00000000..a63d4c35 --- /dev/null +++ b/test/regress/EA18D948.test @@ -0,0 +1,14 @@ +2012-01-01 * Buy AAA + A 1 AAA @ 1.00 EUR + B -1.00 EUR + +2012-02-01 * Buy AAA + A 1 AAA @ 2.00 EUR + B -2.00 EUR + +test reg A -V -A +12-Jan-01 Buy AAA A 1.00 EUR 1.00 EUR +12-Feb-01 Commodities revalued <Revalued> 1.00 EUR 0 +12-Feb-01 Buy AAA A 2.00 EUR 2.00 EUR +end test + diff --git a/test/regress/F06D5554.test b/test/regress/F06D5554.test new file mode 100644 index 00000000..4541b791 --- /dev/null +++ b/test/regress/F06D5554.test @@ -0,0 +1,552 @@ +2011/04/01 serveraxis.com + Expenses:Computer:Internet $15.00 + Expenses:Computer:Internet $1.10 + Liabilities:MasterCard + +2011/04/05 Pennsylvania toll booth + Expenses:Auto:Fees $13.00 + Expenses:Cash + +2011/04/05 iTunes + Expenses:Music $1.29 + Expenses:Taxes:Sales $0.09 + Liabilities:MasterCard $-1.38 + +2011/04/19 iTunes + Expenses:Computer:Software $4.99 + Expenses:Taxes:Sales $0.35 + Liabilities:MasterCard $-5.34 + +2011/04/24 iTunes + Expenses:Movies $1.99 + Expenses:Movies $2.99 + Expenses:Taxes:Sales $0.35 + Liabilities:MasterCard $-5.33 + +2011/04/29 iTunes + Expenses:Computer:Movies $0.99 + Expenses:Taxes:Sales $0.07 + Liabilities:MasterCard $-1.06 + +2011/05/01 serveraxis.com + Expenses:Computer:Internet $15.00 + Expenses:Computer:Internet $1.10 + Liabilities:MasterCard + +2011/05/18 iTunes + Expenses:Computer:Software $6.99 + Expenses:Taxes:Sales $0.49 + Liabilities:MasterCard $-7.48 + +2011/05/20 DynDNS.com + Expenses:Computer:Internet $15.00 + Liabilities:MasterCard + +2011/05/20 DynDNS.com + Expenses:Computer:Internet $15.00 + Liabilities:MasterCard + +2011/05/27 iTunes + Expenses:Movies $1.99 + Expenses:Movies $1.99 + Expenses:Movies $1.99 + Expenses:Taxes:Sales $0.42 + Liabilities:MasterCard $-6.39 + +2011/05/26 Valero + Expenses:Auto:Gas $26.79 + Liabilities:MasterCard + +2011/05/26 Starbucks + Expenses:Food $2.20 + Expenses:Taxes:Sales $0.15 + Liabilities:MasterCard $-2.35 + +2011/05/26 La Mex + Expenses:Food $17.70 + Expenses:Taxes:Sales $1.11 + Expenses:Tips $3.00 + Liabilities:MasterCard $-21.81 + +2011/05/27 Leaves N Beans + Expenses:Food:Dining $20.98 + Expenses:Taxes:Sales $1.63 + Expenses:Tips $2.00 + Liabilities:MasterCard $-24.61 + +2011/05/27 Wal*Mart + Expenses:Home:Supplies $7.97 + Expenses:Food:Grocery $3.25 + Expenses:Food:Grocery $3.18 + Expenses:Food:Grocery $3.18 + Expenses:Food:Grocery $2.98 + Expenses:Food:Grocery $1.98 + Expenses:Food:Grocery $3.98 + Expenses:Food:Grocery $3.58 + Expenses:Food:Grocery $3.58 + Expenses:Food:Grocery $1.58 + Expenses:Food:Grocery $1.88 + Expenses:Food:Grocery $2.50 + Expenses:Food:Grocery $1.26 + Expenses:Food:Grocery $2.62 + Expenses:Food:Grocery $3.48 + Expenses:Home:Supplies $1.37 + Expenses:Home:Supplies $2.92 + Expenses:Beauty $3.38 + Expenses:Beauty $0.97 + Expenses:Beauty $4.64 + Expenses:Beauty $1.97 + Expenses:Beauty $1.97 + Expenses:Beauty $5.98 + Expenses:Home:Supplies $9.98 + Expenses:Bedding $4.00 + Expenses:Bedding $4.00 + Expenses:Home:Supplies $2.88 + Expenses:Home:Supplies $2.88 + Expenses:Home:Supplies $2.88 + Expenses:Home:Supplies $2.88 + Expenses:Clothing $2.96 + Expenses:Supplies $0.84 + Expenses:Food:Grocery $1.38 + Expenses:Food:Grocery $1.38 + Expenses:Food:Grocery $2.32 + Expenses:Food:Grocery $2.00 + Expenses:Food:Grocery $2.98 + Expenses:Food:Grocery $3.00 + Expenses:Food:Grocery $2.14 + Expenses:Food:Grocery $2.14 + Expenses:Food:Grocery $2.50 + Expenses:Food:Grocery $2.50 + Expenses:Food:Grocery $3.48 + Expenses:Home:Supplies $1.17 + Expenses:Supplies $3.00 + Expenses:Bedding $34.88 + Expenses:Home $6.00 + Expenses:Home $6.00 + Expenses:Home:Supplies $3.97 + Expenses:Food:Grocery $0.78 + Expenses:Food:Grocery $0.78 + Expenses:Food:Grocery $0.78 + Expenses:Food:Grocery $0.78 + Expenses:Home $4.00 + Expenses:Home $4.00 + Expenses:Home $10.87 + Expenses:Home $4.00 + Expenses:Bedding $65.96 + Expenses:Taxes:Sales $16.89 + Expenses:Taxes:Sales $0.65 + Liabilities:MasterCard $-293.83 + +2011/05/27 Asia Grill + Expenses:Food:Dining $28.63 + Expenses:Tips $4.00 + Liabilities:MasterCard $-32.63 + +2011/05/28 Shell + Expenses:Auto:Gas $43.41 + Liabilities:MasterCard + +2011/05/28 Sears + Expenses:Home $1,728.96 + Expenses:Taxes:Sales $136.87 + Liabilities:MasterCard $-1,865.83 + +2011/05/28 Sears + Expenses:Home $99.61 + Expenses:Taxes:Sales $8.22 + Liabilities:MasterCard $-107.83 + +2011/05/28 Buffalo Wild Wings + Expenses:Food:Dining $22.98 + Expenses:Tips $2.35 + Expenses:Taxes:Sales $3.50 + Liabilities:MasterCard $-28.83 + +2011/05/28 Cold Stone Creamery + Expenses:Food:Dining $5.73 + Expenses:Tips $0.50 + Liabilities:MasterCard $-6.23 + +2011/05/29 Hy Vee + Expenses:Supplies $2.00 + Expenses:Supplies $7.99 + Expenses:Supplies $7.99 + Expenses:Food:Grocery $157.64 + Expenses:Taxes:Sales $5.74 + Liabilities:MasterCard $-181.36 + +2011/05/30 Allied movers, Fidel & Manny + Expenses:Tips $97.00 + Expenses:Cash + +2011/05/30 Starbucks + Expenses:Food:Dining $6.90 + Expenses:Taxes:Sales $0.71 + Liabilities:MasterCard $-7.61 + +2011/05/31 Wal*Mart + Expenses:Home $108.13 + Expenses:Taxes:Sales $8.65 + Liabilities:MasterCard $-116.78 + +test reg -p "apr 2011" Expenses +11-Apr-01 serveraxis.com Expe:Computer:Internet $15.00 $15.00 + Expe:Computer:Internet $1.10 $16.10 +11-Apr-05 Pennsylvania toll b.. Expenses:Auto:Fees $13.00 $29.10 + Expenses:Cash $-13.00 $16.10 +11-Apr-05 iTunes Expenses:Music $1.29 $17.39 + Expenses:Taxes:Sales $0.09 $17.48 +11-Apr-19 iTunes Expe:Computer:Software $4.99 $22.47 + Expenses:Taxes:Sales $0.35 $22.82 +11-Apr-24 iTunes Expenses:Movies $1.99 $24.81 + Expenses:Movies $2.99 $27.80 + Expenses:Taxes:Sales $0.35 $28.15 +11-Apr-29 iTunes Expens:Computer:Movies $0.99 $29.14 + Expenses:Taxes:Sales $0.07 $29.21 +end test + +test reg -p "apr 2011" Expenses --monthly +11-Apr-01 - 11-Apr-30 Expenses:Auto:Fees $13.00 $13.00 + Expenses:Cash $-13.00 0 + Expe:Computer:Internet $16.10 $16.10 + Expens:Computer:Movies $0.99 $17.09 + Expe:Computer:Software $4.99 $22.08 + Expenses:Movies $4.98 $27.06 + Expenses:Music $1.29 $28.35 + Expenses:Taxes:Sales $0.86 $29.21 +end test + +test reg -p "apr 2011" Expenses --monthly --exact +11-Apr-01 - 11-Apr-29 Expenses:Auto:Fees $13.00 $13.00 + Expenses:Cash $-13.00 0 + Expe:Computer:Internet $16.10 $16.10 + Expens:Computer:Movies $0.99 $17.09 + Expe:Computer:Software $4.99 $22.08 + Expenses:Movies $4.98 $27.06 + Expenses:Music $1.29 $28.35 + Expenses:Taxes:Sales $0.86 $29.21 +end test + +test reg -p "apr 2011" Expenses --weekly +11-Apr-01 - 11-Apr-02 Expe:Computer:Internet $16.10 $16.10 +11-Apr-03 - 11-Apr-09 Expenses:Auto:Fees $13.00 $29.10 + Expenses:Cash $-13.00 $16.10 + Expenses:Music $1.29 $17.39 + Expenses:Taxes:Sales $0.09 $17.48 +11-Apr-17 - 11-Apr-23 Expe:Computer:Software $4.99 $22.47 + Expenses:Taxes:Sales $0.35 $22.82 +11-Apr-24 - 11-Apr-30 Expens:Computer:Movies $0.99 $23.81 + Expenses:Movies $4.98 $28.79 + Expenses:Taxes:Sales $0.42 $29.21 +end test + +test reg -p "apr 2011" Expenses --weekly --exact +11-Apr-01 - 11-Apr-01 Expe:Computer:Internet $16.10 $16.10 +11-Apr-05 - 11-Apr-05 Expenses:Auto:Fees $13.00 $29.10 + Expenses:Cash $-13.00 $16.10 + Expenses:Music $1.29 $17.39 + Expenses:Taxes:Sales $0.09 $17.48 +11-Apr-19 - 11-Apr-19 Expe:Computer:Software $4.99 $22.47 + Expenses:Taxes:Sales $0.35 $22.82 +11-Apr-24 - 11-Apr-29 Expens:Computer:Movies $0.99 $23.81 + Expenses:Movies $4.98 $28.79 + Expenses:Taxes:Sales $0.42 $29.21 +end test + +test reg -p "apr 2011" Expenses --weekly --empty +11-Apr-01 - 11-Apr-02 Expe:Computer:Internet $16.10 $16.10 +11-Apr-03 - 11-Apr-09 Expenses:Auto:Fees $13.00 $29.10 + Expenses:Cash $-13.00 $16.10 + Expenses:Music $1.29 $17.39 + Expenses:Taxes:Sales $0.09 $17.48 +11-Apr-10 - 11-Apr-16 <None> 0 $17.48 +11-Apr-17 - 11-Apr-23 Expe:Computer:Software $4.99 $22.47 + Expenses:Taxes:Sales $0.35 $22.82 +11-Apr-24 - 11-Apr-30 Expens:Computer:Movies $0.99 $23.81 + Expenses:Movies $4.98 $28.79 + Expenses:Taxes:Sales $0.42 $29.21 +end test + +test reg -p "apr 2011" Expenses --weekly --empty --exact +11-Apr-01 - 11-Apr-01 Expe:Computer:Internet $16.10 $16.10 +11-Apr-05 - 11-Apr-05 Expenses:Auto:Fees $13.00 $29.10 + Expenses:Cash $-13.00 $16.10 + Expenses:Music $1.29 $17.39 + Expenses:Taxes:Sales $0.09 $17.48 +11-Apr-16 - 11-Apr-16 <None> 0 $17.48 +11-Apr-19 - 11-Apr-19 Expe:Computer:Software $4.99 $22.47 + Expenses:Taxes:Sales $0.35 $22.82 +11-Apr-24 - 11-Apr-29 Expens:Computer:Movies $0.99 $23.81 + Expenses:Movies $4.98 $28.79 + Expenses:Taxes:Sales $0.42 $29.21 +end test + +test reg -p "may 2011" +11-May-01 serveraxis.com Expe:Computer:Internet $15.00 $15.00 + Expe:Computer:Internet $1.10 $16.10 + Liabilities:MasterCard $-16.10 0 +11-May-18 iTunes Expe:Computer:Software $6.99 $6.99 + Expenses:Taxes:Sales $0.49 $7.48 + Liabilities:MasterCard $-7.48 0 +11-May-20 DynDNS.com Expe:Computer:Internet $15.00 $15.00 + Liabilities:MasterCard $-15.00 0 +11-May-20 DynDNS.com Expe:Computer:Internet $15.00 $15.00 + Liabilities:MasterCard $-15.00 0 +11-May-27 iTunes Expenses:Movies $1.99 $1.99 + Expenses:Movies $1.99 $3.98 + Expenses:Movies $1.99 $5.97 + Expenses:Taxes:Sales $0.42 $6.39 + Liabilities:MasterCard $-6.39 0 +11-May-26 Valero Expenses:Auto:Gas $26.79 $26.79 + Liabilities:MasterCard $-26.79 0 +11-May-26 Starbucks Expenses:Food $2.20 $2.20 + Expenses:Taxes:Sales $0.15 $2.35 + Liabilities:MasterCard $-2.35 0 +11-May-26 La Mex Expenses:Food $17.70 $17.70 + Expenses:Taxes:Sales $1.11 $18.81 + Expenses:Tips $3.00 $21.81 + Liabilities:MasterCard $-21.81 0 +11-May-27 Leaves N Beans Expenses:Food:Dining $20.98 $20.98 + Expenses:Taxes:Sales $1.63 $22.61 + Expenses:Tips $2.00 $24.61 + Liabilities:MasterCard $-24.61 0 +11-May-27 Wal*Mart Expenses:Home:Supplies $7.97 $7.97 + Expenses:Food:Grocery $3.25 $11.22 + Expenses:Food:Grocery $3.18 $14.40 + Expenses:Food:Grocery $3.18 $17.58 + Expenses:Food:Grocery $2.98 $20.56 + Expenses:Food:Grocery $1.98 $22.54 + Expenses:Food:Grocery $3.98 $26.52 + Expenses:Food:Grocery $3.58 $30.10 + Expenses:Food:Grocery $3.58 $33.68 + Expenses:Food:Grocery $1.58 $35.26 + Expenses:Food:Grocery $1.88 $37.14 + Expenses:Food:Grocery $2.50 $39.64 + Expenses:Food:Grocery $1.26 $40.90 + Expenses:Food:Grocery $2.62 $43.52 + Expenses:Food:Grocery $3.48 $47.00 + Expenses:Home:Supplies $1.37 $48.37 + Expenses:Home:Supplies $2.92 $51.29 + Expenses:Beauty $3.38 $54.67 + Expenses:Beauty $0.97 $55.64 + Expenses:Beauty $4.64 $60.28 + Expenses:Beauty $1.97 $62.25 + Expenses:Beauty $1.97 $64.22 + Expenses:Beauty $5.98 $70.20 + Expenses:Home:Supplies $9.98 $80.18 + Expenses:Bedding $4.00 $84.18 + Expenses:Bedding $4.00 $88.18 + Expenses:Home:Supplies $2.88 $91.06 + Expenses:Home:Supplies $2.88 $93.94 + Expenses:Home:Supplies $2.88 $96.82 + Expenses:Home:Supplies $2.88 $99.70 + Expenses:Clothing $2.96 $102.66 + Expenses:Supplies $0.84 $103.50 + Expenses:Food:Grocery $1.38 $104.88 + Expenses:Food:Grocery $1.38 $106.26 + Expenses:Food:Grocery $2.32 $108.58 + Expenses:Food:Grocery $2.00 $110.58 + Expenses:Food:Grocery $2.98 $113.56 + Expenses:Food:Grocery $3.00 $116.56 + Expenses:Food:Grocery $2.14 $118.70 + Expenses:Food:Grocery $2.14 $120.84 + Expenses:Food:Grocery $2.50 $123.34 + Expenses:Food:Grocery $2.50 $125.84 + Expenses:Food:Grocery $3.48 $129.32 + Expenses:Home:Supplies $1.17 $130.49 + Expenses:Supplies $3.00 $133.49 + Expenses:Bedding $34.88 $168.37 + Expenses:Home $6.00 $174.37 + Expenses:Home $6.00 $180.37 + Expenses:Home:Supplies $3.97 $184.34 + Expenses:Food:Grocery $0.78 $185.12 + Expenses:Food:Grocery $0.78 $185.90 + Expenses:Food:Grocery $0.78 $186.68 + Expenses:Food:Grocery $0.78 $187.46 + Expenses:Home $4.00 $191.46 + Expenses:Home $4.00 $195.46 + Expenses:Home $10.87 $206.33 + Expenses:Home $4.00 $210.33 + Expenses:Bedding $65.96 $276.29 + Expenses:Taxes:Sales $16.89 $293.18 + Expenses:Taxes:Sales $0.65 $293.83 + Liabilities:MasterCard $-293.83 0 +11-May-27 Asia Grill Expenses:Food:Dining $28.63 $28.63 + Expenses:Tips $4.00 $32.63 + Liabilities:MasterCard $-32.63 0 +11-May-28 Shell Expenses:Auto:Gas $43.41 $43.41 + Liabilities:MasterCard $-43.41 0 +11-May-28 Sears Expenses:Home $1,728.96 $1,728.96 + Expenses:Taxes:Sales $136.87 $1,865.83 + Liabilities:MasterCard $-1,865.83 0 +11-May-28 Sears Expenses:Home $99.61 $99.61 + Expenses:Taxes:Sales $8.22 $107.83 + Liabilities:MasterCard $-107.83 0 +11-May-28 Buffalo Wild Wings Expenses:Food:Dining $22.98 $22.98 + Expenses:Tips $2.35 $25.33 + Expenses:Taxes:Sales $3.50 $28.83 + Liabilities:MasterCard $-28.83 0 +11-May-28 Cold Stone Creamery Expenses:Food:Dining $5.73 $5.73 + Expenses:Tips $0.50 $6.23 + Liabilities:MasterCard $-6.23 0 +11-May-29 Hy Vee Expenses:Supplies $2.00 $2.00 + Expenses:Supplies $7.99 $9.99 + Expenses:Supplies $7.99 $17.98 + Expenses:Food:Grocery $157.64 $175.62 + Expenses:Taxes:Sales $5.74 $181.36 + Liabilities:MasterCard $-181.36 0 +11-May-30 Allied movers, Fide.. Expenses:Tips $97.00 $97.00 + Expenses:Cash $-97.00 0 +11-May-30 Starbucks Expenses:Food:Dining $6.90 $6.90 + Expenses:Taxes:Sales $0.71 $7.61 + Liabilities:MasterCard $-7.61 0 +11-May-31 Wal*Mart Expenses:Home $108.13 $108.13 + Expenses:Taxes:Sales $8.65 $116.78 + Liabilities:MasterCard $-116.78 0 +end test + +test reg -p "may 2011" --monthly +11-May-01 - 11-May-31 Expenses:Auto:Gas $70.20 $70.20 + Expenses:Beauty $18.91 $89.11 + Expenses:Bedding $108.84 $197.95 + Expenses:Cash $-97.00 $100.95 + Expenses:Clothing $2.96 $103.91 + Expe:Computer:Internet $46.10 $150.01 + Expe:Computer:Software $6.99 $157.00 + Expenses:Food $19.90 $176.90 + Expenses:Food:Dining $85.22 $262.12 + Expenses:Food:Grocery $225.61 $487.73 + Expenses:Home $1,971.57 $2,459.30 + Expenses:Home:Supplies $38.90 $2,498.20 + Expenses:Movies $5.97 $2,504.17 + Expenses:Supplies $21.82 $2,525.99 + Expenses:Taxes:Sales $185.03 $2,711.02 + Expenses:Tips $108.85 $2,819.87 + Liabilities:MasterCard $-2,819.87 0 +end test + +test reg -p "may 2011" --weekly +11-May-01 - 11-May-07 Expe:Computer:Internet $16.10 $16.10 + Liabilities:MasterCard $-16.10 0 +11-May-15 - 11-May-21 Expe:Computer:Internet $30.00 $30.00 + Expe:Computer:Software $6.99 $36.99 + Expenses:Taxes:Sales $0.49 $37.48 + Liabilities:MasterCard $-37.48 0 +11-May-22 - 11-May-28 Expenses:Auto:Gas $70.20 $70.20 + Expenses:Beauty $18.91 $89.11 + Expenses:Bedding $108.84 $197.95 + Expenses:Clothing $2.96 $200.91 + Expenses:Food $19.90 $220.81 + Expenses:Food:Dining $78.32 $299.13 + Expenses:Food:Grocery $67.97 $367.10 + Expenses:Home $1,863.44 $2,230.54 + Expenses:Home:Supplies $38.90 $2,269.44 + Expenses:Movies $5.97 $2,275.41 + Expenses:Supplies $3.84 $2,279.25 + Expenses:Taxes:Sales $169.44 $2,448.69 + Expenses:Tips $11.85 $2,460.54 + Liabilities:MasterCard $-2,460.54 0 +11-May-29 - 11-May-31 Expenses:Cash $-97.00 $-97.00 + Expenses:Food:Dining $6.90 $-90.10 + Expenses:Food:Grocery $157.64 $67.54 + Expenses:Home $108.13 $175.67 + Expenses:Supplies $17.98 $193.65 + Expenses:Taxes:Sales $15.10 $208.75 + Expenses:Tips $97.00 $305.75 + Liabilities:MasterCard $-305.75 0 +end test + +test reg -p "may 2011" --weekly --exact +11-May-01 - 11-May-01 Expe:Computer:Internet $16.10 $16.10 + Liabilities:MasterCard $-16.10 0 +11-May-18 - 11-May-20 Expe:Computer:Internet $30.00 $30.00 + Expe:Computer:Software $6.99 $36.99 + Expenses:Taxes:Sales $0.49 $37.48 + Liabilities:MasterCard $-37.48 0 +11-May-26 - 11-May-28 Expenses:Auto:Gas $70.20 $70.20 + Expenses:Beauty $18.91 $89.11 + Expenses:Bedding $108.84 $197.95 + Expenses:Clothing $2.96 $200.91 + Expenses:Food $19.90 $220.81 + Expenses:Food:Dining $78.32 $299.13 + Expenses:Food:Grocery $67.97 $367.10 + Expenses:Home $1,863.44 $2,230.54 + Expenses:Home:Supplies $38.90 $2,269.44 + Expenses:Movies $5.97 $2,275.41 + Expenses:Supplies $3.84 $2,279.25 + Expenses:Taxes:Sales $169.44 $2,448.69 + Expenses:Tips $11.85 $2,460.54 + Liabilities:MasterCard $-2,460.54 0 +11-May-29 - 11-May-31 Expenses:Cash $-97.00 $-97.00 + Expenses:Food:Dining $6.90 $-90.10 + Expenses:Food:Grocery $157.64 $67.54 + Expenses:Home $108.13 $175.67 + Expenses:Supplies $17.98 $193.65 + Expenses:Taxes:Sales $15.10 $208.75 + Expenses:Tips $97.00 $305.75 + Liabilities:MasterCard $-305.75 0 +end test + +test reg -p "may 2011" --weekly --empty +11-May-01 - 11-May-07 Expe:Computer:Internet $16.10 $16.10 + Liabilities:MasterCard $-16.10 0 +11-May-08 - 11-May-14 <None> 0 0 +11-May-15 - 11-May-21 Expe:Computer:Internet $30.00 $30.00 + Expe:Computer:Software $6.99 $36.99 + Expenses:Taxes:Sales $0.49 $37.48 + Liabilities:MasterCard $-37.48 0 +11-May-22 - 11-May-28 Expenses:Auto:Gas $70.20 $70.20 + Expenses:Beauty $18.91 $89.11 + Expenses:Bedding $108.84 $197.95 + Expenses:Clothing $2.96 $200.91 + Expenses:Food $19.90 $220.81 + Expenses:Food:Dining $78.32 $299.13 + Expenses:Food:Grocery $67.97 $367.10 + Expenses:Home $1,863.44 $2,230.54 + Expenses:Home:Supplies $38.90 $2,269.44 + Expenses:Movies $5.97 $2,275.41 + Expenses:Supplies $3.84 $2,279.25 + Expenses:Taxes:Sales $169.44 $2,448.69 + Expenses:Tips $11.85 $2,460.54 + Liabilities:MasterCard $-2,460.54 0 +11-May-29 - 11-May-31 Expenses:Cash $-97.00 $-97.00 + Expenses:Food:Dining $6.90 $-90.10 + Expenses:Food:Grocery $157.64 $67.54 + Expenses:Home $108.13 $175.67 + Expenses:Supplies $17.98 $193.65 + Expenses:Taxes:Sales $15.10 $208.75 + Expenses:Tips $97.00 $305.75 + Liabilities:MasterCard $-305.75 0 +end test + +test reg -p "may 2011" --weekly --empty --exact +11-May-01 - 11-May-01 Expe:Computer:Internet $16.10 $16.10 + Liabilities:MasterCard $-16.10 0 +11-May-14 - 11-May-14 <None> 0 0 +11-May-18 - 11-May-20 Expe:Computer:Internet $30.00 $30.00 + Expe:Computer:Software $6.99 $36.99 + Expenses:Taxes:Sales $0.49 $37.48 + Liabilities:MasterCard $-37.48 0 +11-May-26 - 11-May-28 Expenses:Auto:Gas $70.20 $70.20 + Expenses:Beauty $18.91 $89.11 + Expenses:Bedding $108.84 $197.95 + Expenses:Clothing $2.96 $200.91 + Expenses:Food $19.90 $220.81 + Expenses:Food:Dining $78.32 $299.13 + Expenses:Food:Grocery $67.97 $367.10 + Expenses:Home $1,863.44 $2,230.54 + Expenses:Home:Supplies $38.90 $2,269.44 + Expenses:Movies $5.97 $2,275.41 + Expenses:Supplies $3.84 $2,279.25 + Expenses:Taxes:Sales $169.44 $2,448.69 + Expenses:Tips $11.85 $2,460.54 + Liabilities:MasterCard $-2,460.54 0 +11-May-29 - 11-May-31 Expenses:Cash $-97.00 $-97.00 + Expenses:Food:Dining $6.90 $-90.10 + Expenses:Food:Grocery $157.64 $67.54 + Expenses:Home $108.13 $175.67 + Expenses:Supplies $17.98 $193.65 + Expenses:Taxes:Sales $15.10 $208.75 + Expenses:Tips $97.00 $305.75 + Liabilities:MasterCard $-305.75 0 +end test diff --git a/test/regress/F524E251.test b/test/regress/F524E251.test new file mode 100644 index 00000000..d2d2f049 --- /dev/null +++ b/test/regress/F524E251.test @@ -0,0 +1,37 @@ +; Test for: ledger -f doc/sample.dat -n reg' shows $0.00 on first post + += /^Expenses:Books/ + (Liabilities:Taxes) -0.10 + +~ Monthly + Assets:Bank:Checking $500.00 + Income:Salary + +2004/05/01 * Checking balance + Assets:Bank:Checking $1,000.00 + Equity:Opening Balances + +2004/05/01 * Investment balance + Assets:Brokerage 50 AAPL @ $30.00 + Equity:Opening Balances + +2004/05/14 * Pay day + Assets:Bank:Checking $500.00 + Income:Salary + +2004/05/27 Book Store + Expenses:Books $20.00 + Liabilities:MasterCard + +2004/05/27 (100) Credit card company + Liabilities:MasterCard $20.00 + Assets:Bank:Checking + +test -n reg +04-May-01 Investment balance <Total> $-1,500.00 + 50 AAPL $-1,500.00 + 50 AAPL +04-May-27 Book Store <Total> $-2.00 $-1,502.00 + 50 AAPL +end test + diff --git a/test/regress/FCE11C8D.test b/test/regress/FCE11C8D.test new file mode 100644 index 00000000..595edc2d --- /dev/null +++ b/test/regress/FCE11C8D.test @@ -0,0 +1,7 @@ +2012-03-17 Payee + Expenses:Food $20 + Assets:Cash + +test reg --monthly --invert exp +12-Mar-01 - 12-Mar-31 Expenses:Food $-20 $-20 +end test diff --git a/test/regress/FDFBA165.test b/test/regress/FDFBA165.test new file mode 100644 index 00000000..55074bed --- /dev/null +++ b/test/regress/FDFBA165.test @@ -0,0 +1,23 @@ +; Test for: automated transactions didn't show up in the balance report + += Income:Clients: + (Liabilities:Taxes:VAT) ((1,00 / 1,19) * 0,19) + +2009/07/27 * Invoice + Assets:Bank:Checking €1.190,00 + Income:Clients:ACME_Inc + +test --decimal-comma bal + €1.190,00 Assets:Bank:Checking + €-1.190,00 Income:Clients:ACME_Inc + €-190,00 Liabilities:Taxes:VAT +-------------------- + €-190,00 +end test + +test --decimal-comma reg +09-Jul-27 Invoice Assets:Bank:Checking €1.190,00 €1.190,00 + Incom:Clients:ACME_Inc €-1.190,00 0 + (Liabilitie:Taxes:VAT) €-190,00 €-190,00 +end test + diff --git a/test/unit/t_commodity.cc b/test/unit/t_commodity.cc index dc64dcfb..8caeb694 100644 --- a/test/unit/t_commodity.cc +++ b/test/unit/t_commodity.cc @@ -92,18 +92,18 @@ BOOST_AUTO_TEST_CASE(testPriceHistory) BOOST_CHECK_EQUAL(string("$2124.122"), amt->to_fullstring()); #endif - amt = x1.value(CURRENT_TIME(), euro); + amt = x1.value(CURRENT_TIME(), &euro); BOOST_CHECK(amt); - BOOST_CHECK_EQUAL(string("EUR 1366.87"), amt->rounded().to_string()); + BOOST_CHECK_EQUAL(string("EUR 1787.50"), amt->rounded().to_string()); // Add a newer Euro pricing aapl.add_price(jan17_07, amount_t("EUR 23.00")); - amt = x1.value(CURRENT_TIME(), euro); + amt = x1.value(CURRENT_TIME(), &euro); BOOST_CHECK(amt); BOOST_CHECK_EQUAL(string("EUR 2302.30"), amt->to_string()); - amt = x1.value(CURRENT_TIME(), cad); + amt = x1.value(CURRENT_TIME(), &cad); BOOST_CHECK(amt); BOOST_CHECK_EQUAL(string("CAD 3223.22"), amt->to_string()); #endif // NOT_FOR_PYTHON diff --git a/test/unit/t_expr.cc b/test/unit/t_expr.cc index f882f3a1..c10ee029 100644 --- a/test/unit/t_expr.cc +++ b/test/unit/t_expr.cc @@ -1,5 +1,5 @@ #define BOOST_TEST_DYN_LINK -#define BOOST_TEST_MODULE expr +//#define BOOST_TEST_MODULE expr #include <boost/test/unit_test.hpp> #include <system.hh> diff --git a/tools/Makefile.am b/tools/Makefile.am index 5bb41cc0..04f64530 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -8,7 +8,6 @@ DISTCLEANFILES = .timestamp lib_LTLIBRARIES = \ libledger_report.la \ libledger_data.la \ - libledger_expr.la \ libledger_math.la \ libledger_util.la @@ -28,8 +27,17 @@ libledger_util_la_CPPFLAGS = $(lib_cppflags) libledger_util_la_LDFLAGS = -release $(LIBVERSION) libledger_math_la_SOURCES = \ + src/format.cc \ + src/query.cc \ + src/scope.cc \ + src/expr.cc \ + src/op.cc \ + src/parser.cc \ + src/token.cc \ + src/value.cc \ src/balance.cc \ src/quotes.cc \ + src/history.cc \ src/pool.cc \ src/annotate.cc \ src/commodity.cc \ @@ -38,22 +46,8 @@ libledger_math_la_SOURCES = \ libledger_math_la_CPPFLAGS = $(lib_cppflags) libledger_math_la_LDFLAGS = -release $(LIBVERSION) -libledger_expr_la_SOURCES = \ - src/option.cc \ - src/format.cc \ - src/query.cc \ - src/predicate.cc \ - src/scope.cc \ - src/expr.cc \ - src/op.cc \ - src/parser.cc \ - src/token.cc \ - src/value.cc - -libledger_expr_la_CPPFLAGS = $(lib_cppflags) -libledger_expr_la_LDFLAGS = -release $(LIBVERSION) - libledger_data_la_SOURCES = \ + src/option.cc \ src/lookup.cc \ src/compare.cc \ src/iterators.cc \ @@ -85,6 +79,8 @@ libledger_report_la_SOURCES = \ src/chain.cc \ src/filters.cc \ src/report.cc \ + src/views.cc \ + src/select.cc \ src/session.cc libledger_report_la_CPPFLAGS = $(lib_cppflags) @@ -104,6 +100,7 @@ pkginclude_HEADERS = \ src/amount.h \ src/commodity.h \ src/annotate.h \ + src/history.h \ src/pool.h \ src/quotes.h \ src/balance.h \ @@ -119,13 +116,13 @@ pkginclude_HEADERS = \ src/query.h \ src/format.h \ src/option.h \ - src/series.h \ \ src/item.h \ src/post.h \ src/xact.h \ src/account.h \ src/journal.h \ + src/context.h \ src/temps.h \ src/archive.h \ src/timelog.h \ @@ -134,6 +131,8 @@ pkginclude_HEADERS = \ src/lookup.h \ \ src/session.h \ + src/select.h \ + src/views.h \ src/report.h \ src/filters.h \ src/chain.h \ @@ -202,7 +201,6 @@ DISTCLEANFILES += ledger.elc timeclock.elc all_sources = $(libledger_util_la_SOURCES) \ $(libledger_math_la_SOURCES) \ - $(libledger_expr_la_SOURCES) \ $(libledger_data_la_SOURCES) \ $(libledger_report_la_SOURCES) \ $(libledger_python_la_SOURCES) \ @@ -226,6 +224,7 @@ libledger_python_la_SOURCES = \ src/py_expr.cc \ src/py_format.cc \ src/py_item.cc \ + src/py_session.cc \ src/py_journal.cc \ src/py_post.cc \ src/py_times.cc \ @@ -256,8 +255,7 @@ TESTS = RegressTests BaselineTests ManualTests ConfirmTests \ if HAVE_BOOST_TEST TESTS += \ UtilTests \ - MathTests \ - ExprTests + MathTests # DataTests \ # ReportTests endif @@ -282,6 +280,7 @@ UtilTests_CPPFLAGS = -I$(srcdir)/test $(lib_cppflags) UtilTests_LDADD = libledger_util.la $(TESTLIBS) MathTests_SOURCES = \ + test/unit/t_expr.cc \ test/unit/t_commodity.cc \ test/unit/t_amount.cc \ test/unit/t_balance.cc @@ -289,16 +288,10 @@ MathTests_SOURCES = \ MathTests_CPPFLAGS = -I$(srcdir)/test $(lib_cppflags) MathTests_LDADD = libledger_math.la $(UtilTests_LDADD) -ExprTests_SOURCES = \ - test/unit/t_expr.cc - -ExprTests_CPPFLAGS = -I$(srcdir)/test $(lib_cppflags) -ExprTests_LDADD = libledger_expr.la $(MathTests_LDADD) - DataTests_SOURCES = DataTests_CPPFLAGS = -I$(srcdir)/test $(lib_cppflags) -DataTests_LDADD = libledger_data.la $(ExprTests_LDADD) +DataTests_LDADD = libledger_data.la $(MathTests_LDADD) ReportTests_SOURCES = @@ -308,7 +301,6 @@ ReportTests_LDADD = libledger_report.la $(DataTests_LDADD) all_tests_sources = \ $(UtilTests_SOURCES) \ $(MathTests_SOURCES) \ - $(ExprTests_SOURCES) \ $(DataTests_SOURCES) \ $(ReportTests_SOURCES) @@ -321,7 +313,7 @@ all_py_tests_sources = \ test/python/%.py: test/unit/%.cc test/convert.py $(PYTHON) $(srcdir)/test/convert.py $< $@ -test/python/UnitTests.py: $(all_py_tests_sources) +test/python/ConvertedTests.py: $(all_py_tests_sources) @echo "from unittest import TextTestRunner, TestSuite" > $@ @for file in $$(ls $(srcdir)/test/unit/*.cc); do \ base=$$(basename $$file); \ @@ -346,7 +338,7 @@ ESC_distdir=`echo "$(distdir)" | sed 's/\//\\\\\//g'` # jww (2007-05-10): This rule will not be triggered on systems that # define an EXEEXT. -PyUnitTests: test/PyUnitTests.py test/python/UnitTests.py +PyUnitTests: test/PyUnitTests.py test/python/ConvertedTests.py @cat $(srcdir)/test/PyUnitTests.py \ | sed "s/%python%/$(ESC_python)/" \ | sed "s/%srcdir%/$(ESC_srcdir)/g" \ @@ -357,8 +349,15 @@ RegressTests_SOURCES = test/RegressTests.py EXTRA_DIST += test/regress test/convert.py test/LedgerHarness.py +if HAVE_BOOST_PYTHON +TEST_PYTHON_FLAGS = --python +EXTRA_DIST += test/python +else +TEST_PYTHON_FLAGS = +endif + RegressTests: $(srcdir)/test/RegressTests.py - echo "$(PYTHON) $(srcdir)/test/RegressTests.py -j$(JOBS) $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/regress \"\$$@\"" > $@ + echo "$(PYTHON) $(srcdir)/test/RegressTests.py -j$(JOBS) $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/regress $(TEST_PYTHON_FLAGS) \"\$$@\"" > $@ chmod 755 $@ BaselineTests_SOURCES = test/RegressTests.py @@ -366,7 +365,7 @@ BaselineTests_SOURCES = test/RegressTests.py EXTRA_DIST += test/baseline BaselineTests: $(srcdir)/test/RegressTests.py - echo "$(PYTHON) $(srcdir)/test/RegressTests.py -j$(JOBS) $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/baseline \"\$$@\"" > $@ + echo "$(PYTHON) $(srcdir)/test/RegressTests.py -j$(JOBS) $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/baseline $(TEST_PYTHON_FLAGS) \"\$$@\"" > $@ chmod 755 $@ ManualTests_SOURCES = test/RegressTests.py @@ -374,7 +373,7 @@ ManualTests_SOURCES = test/RegressTests.py EXTRA_DIST += test/manual ManualTests: $(srcdir)/test/RegressTests.py - echo "$(PYTHON) $(srcdir)/test/RegressTests.py -j$(JOBS) $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/manual \"\$$@\"" > $@ + echo "$(PYTHON) $(srcdir)/test/RegressTests.py -j$(JOBS) $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/manual $(TEST_PYTHON_FLAGS) \"\$$@\"" > $@ chmod 755 $@ ConfirmTests_SOURCES = test/ConfirmTests.py @@ -389,7 +388,7 @@ test/input/mondo.dat: test/input/standard.dat done ConfirmTests: $(srcdir)/test/ConfirmTests.py - echo "$(PYTHON) $(srcdir)/test/ConfirmTests.py $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/input \"\$$@\"" > $@ + echo "$(PYTHON) $(srcdir)/test/ConfirmTests.py $(top_builddir)/ledger$(EXEEXT) $(srcdir) $(srcdir)/test/input $(TEST_PYTHON_FLAGS) \"\$$@\"" > $@ chmod 755 $@ GenerateTests_SOURCES = test/GenerateTests.py @@ -412,8 +411,6 @@ unittests: check 2>&1 | grep -v '^GuardMalloc:' @sh $(FULLCHECK) $(top_builddir)/MathTests$(EXEEXT) --verify \ 2>&1 | grep -v '^GuardMalloc:' - @sh $(FULLCHECK) $(top_builddir)/ExprTests$(EXEEXT) --verify \ - 2>&1 | grep -v '^GuardMalloc:' # @sh $(FULLCHECK) $(top_builddir)/DataTests$(EXEEXT) --verify \ # 2>&1 | grep -v '^GuardMalloc:' # @sh $(FULLCHECK) $(top_builddir)/ReportTests$(EXEEXT) --verify \ @@ -429,10 +426,10 @@ fullcheck: unittests @$(top_builddir)/ManualTests --verify @$(top_builddir)/ConfirmTests --verify @$(top_builddir)/GenerateTests 20 --verify - @$(top_builddir)/RegressTests --gmalloc - @$(top_builddir)/BaselineTests --gmalloc - @$(top_builddir)/ManualTests --gmalloc - @$(top_builddir)/ConfirmTests --gmalloc +# @$(top_builddir)/RegressTests --gmalloc +# @$(top_builddir)/BaselineTests --gmalloc +# @$(top_builddir)/ManualTests --gmalloc +# @$(top_builddir)/ConfirmTests --gmalloc @$(top_builddir)/GenerateTests 10000 # @$(top_builddir)/GenerateTests --gmalloc diff --git a/tools/build.sh b/tools/build.sh new file mode 100755 index 00000000..8be7335a --- /dev/null +++ b/tools/build.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +time ( \ + cd ~/src/ledger ; \ + PATH=/usr/local/bin:/opt/local/bin:$PATH \ + nice -n 20 \ + ./acprep --debug --python --doxygen --cache --clang -j20 make -- check \ +)
\ No newline at end of file diff --git a/tools/configure.ac b/tools/configure.ac index e078ebc4..f7623f62 100644 --- a/tools/configure.ac +++ b/tools/configure.ac @@ -332,37 +332,143 @@ else fi # check for boost_serialization -#AC_CACHE_CHECK( -# [if boost_serialization is available], -# [boost_serialization_cpplib_avail_cv_], -# [boost_serialization_save_libs=$LIBS -# LIBS="-lboost_serialization$BOOST_SUFFIX -lboost_system$BOOST_SUFFIX $LIBS" -# AC_LANG_PUSH(C++) -# AC_LINK_IFELSE( -# [AC_LANG_PROGRAM( -# [[#include <boost/archive/binary_oarchive.hpp> -# #include <iostream> -# struct foo { -# int a; -# template<class Archive> -# void serialize(Archive & ar, const unsigned int) { -# ar & a; -# } -# };]], -# [[boost::archive::binary_oarchive oa(std::cout); -# foo x; -# oa << x;]])], -# [boost_serialization_cpplib_avail_cv_=true], -# [boost_serialization_cpplib_avail_cv_=false]) -# AC_LANG_POP -# LIBS=$boost_serialization_save_libs]) -# -#if [test x$boost_serialization_cpplib_avail_cv_ = xtrue -a x$cache = xtrue]; then -# AC_DEFINE([HAVE_BOOST_SERIALIZATION], [1], [Whether Boost.Serialization is available]) -# LIBS="-lboost_serialization$BOOST_SUFFIX $LIBS" -#fi -#AM_CONDITIONAL(HAVE_BOOST_SERIALIZATION, test x$boost_serialization_cpplib_avail_cv_ = xtrue -a x$cache = xtrue) -AM_CONDITIONAL(HAVE_BOOST_SERIALIZATION, false) +AC_CACHE_CHECK( + [if boost_serialization is available], + [boost_serialization_cpplib_avail_cv_], + [boost_serialization_save_libs=$LIBS + LIBS="-lboost_serialization$BOOST_SUFFIX -lboost_system$BOOST_SUFFIX $LIBS" + AC_LANG_PUSH(C++) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[#include <boost/archive/binary_oarchive.hpp> + #include <iostream> + struct foo { + int a; + template<class Archive> + void serialize(Archive & ar, const unsigned int) { + ar & a; + } + };]], + [[boost::archive::binary_oarchive oa(std::cout); + foo x; + oa << x;]])], + [boost_serialization_cpplib_avail_cv_=true], + [boost_serialization_cpplib_avail_cv_=false]) + AC_LANG_POP + LIBS=$boost_serialization_save_libs]) + +if [test x$boost_serialization_cpplib_avail_cv_ = xtrue -a x$cache = xtrue]; then + AC_DEFINE([HAVE_BOOST_SERIALIZATION], [1], [Whether Boost.Serialization is available]) + LIBS="-lboost_serialization$BOOST_SUFFIX $LIBS" +fi +AM_CONDITIONAL(HAVE_BOOST_SERIALIZATION, test x$boost_serialization_cpplib_avail_cv_ = xtrue -a x$cache = xtrue) + +# check for expat or xmlparse +AC_ARG_ENABLE(xml, + [ --enable-xml Turn on support for XML parsing], + [case "${enableval}" in + yes) xml=true ;; + no) xml=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-xml) ;; + esac],[xml=true]) +AM_CONDITIONAL(USE_XML, test x$xml = xtrue) + +if [test x$xml = xtrue ]; then + AC_CACHE_CHECK( + [if libexpat is available], + [libexpat_avail_cv_], + [libexpat_save_libs=$LIBS + LIBS="-lexpat $LIBS" + AC_LANG_PUSH(C++) + AC_TRY_LINK( + [#include <stdio.h> + extern "C" { + #include <expat.h> // expat XML parser + }], + [XML_Parser parser = XML_ParserCreate(NULL); + return parser != NULL;], + [libexpat_avail_cv_=true], + [libexpat_avail_cv_=false]) + AC_LANG_POP + LIBS=$libexpat_save_libs]) + + if [test x$libexpat_avail_cv_ = xtrue ]; then + AM_CONDITIONAL(HAVE_EXPAT, true) + LIBS="-lexpat $LIBS" + else + AM_CONDITIONAL(HAVE_EXPAT, false) + fi +else + AM_CONDITIONAL(HAVE_EXPAT, false) +fi + +if [test x$xml = xtrue ]; then + if [test x$libexpat_avail_cv_ = xfalse ]; then + AC_CACHE_CHECK( + [if libxmlparse is available], + [libxmlparse_avail_cv_], + [libxmlparse_save_libs=$LIBS + LIBS="-lxmlparse -lxmltok $LIBS" + AC_LANG_PUSH(C++) + AC_TRY_LINK( + [#include <stdio.h> + extern "C" { + #include <xmlparse.h> // expat XML parser + }], + [XML_Parser parser = XML_ParserCreate(NULL); + return parser != NULL;], + [libxmlparse_avail_cv_=true], + [libxmlparse_avail_cv_=false]) + AC_LANG_POP + LIBS=$libxmlparse_save_libs]) + + if [test x$libxmlparse_avail_cv_ = xtrue ]; then + AM_CONDITIONAL(HAVE_XMLPARSE, true) + LIBS="-lxmlparse -lxmltok $LIBS" + else + AM_CONDITIONAL(HAVE_XMLPARSE, false) + fi + else + AM_CONDITIONAL(HAVE_XMLPARSE, false) + fi +else + AM_CONDITIONAL(HAVE_XMLPARSE, false) +fi + +# check for libofx +AC_ARG_ENABLE(ofx, + [ --enable-ofx Turn on support for OFX/OCF parsing], + [case "${enableval}" in + yes) ofx=true ;; + no) ofx=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-ofx) ;; + esac],[ofx=true]) +AM_CONDITIONAL(USE_OFX, test x$ofx = xtrue) + +if [test x$ofx = xtrue ]; then + AC_CACHE_CHECK( + [if libofx is available], + [libofx_avail_cv_], + [libofx_save_libs=$LIBS + LIBS="-lofx $LIBS" + AC_LANG_PUSH(C++) + AC_TRY_LINK( + [#include <libofx.h>], + [ LibofxContextPtr libofx_context = libofx_get_new_context();], + [libofx_avail_cv_=true], + [libofx_avail_cv_=false]) + AC_LANG_POP + LIBS=$libofx_save_libs]) + + if [test x$libofx_avail_cv_ = xtrue ]; then + AM_CONDITIONAL(HAVE_LIBOFX, true) + LIBS="-lofx $LIBS" + else + AM_CONDITIONAL(HAVE_LIBOFX, false) + fi +else + AM_CONDITIONAL(HAVE_LIBOFX, false) +fi # check for Python if [ test x$python = xtrue ]; then diff --git a/tools/proof b/tools/proof index 284f4e85..00c7f7d1 100755 --- a/tools/proof +++ b/tools/proof @@ -2,40 +2,62 @@ set -e -cd ~/src/ledger +ledger_proof() { + SRC="$1" + DEST="$2" + LOGDIR="$3" -VERSION=$(git describe --all --long) + cd "$SRC" + VERSION=$(git describe --all --long) -if [[ -f ~/Products/last-proofed && \ - $(< ~/Products/last-proofed) = $VERSION ]]; then - echo "No need to run tools/proof again" - exit 0 -fi + if [[ -f $DEST/last-proofed && $(< $DEST/last-proofed) = $VERSION ]]; then + echo "No need to run tools/proof again" + exit 0 + fi -rm -fr ~/Products/ledger-proof + sudo rm -fr $DEST/ledger-proof + date > $LOGDIR/ledger-proof.log + + time nice -n 20 \ + ./acprep --debug --enable-doxygen --universal --gcc47 -j16 proof 2>&1 | \ + tee -a $LOGDIR/ledger-proof.log + + time nice -n 20 \ + ./acprep --debug --enable-doxygen --universal --python --gcc47 -j16 proof 2>&1 | \ + tee -a $LOGDIR/ledger-proof.log + + time nice -n 20 \ + ./acprep --debug --enable-doxygen --universal --clang -j16 proof 2>&1 | \ + tee -a $LOGDIR/ledger-proof.log + + time nice -n 20 \ + ./acprep --debug --enable-doxygen --universal --python --clang -j16 proof 2>&1 | \ + tee -a $LOGDIR/ledger-proof.log + + if egrep -q '(ERROR|CRITICAL)' $LOGDIR/ledger-proof.log; then + mutt -a $LOGDIR/ledger-proof.log \ + -s '[ledger] Proof build FAILED' johnw@newartisans.com <<EOF +Ledger proof build FAILED, at commit $VERSION. +EOF + if [[ "$1" = "--alert" ]]; then + notify "Ledger proof build FAILED" + else + echo "Ledger proof build FAILED" + exit 1 + fi + else + echo $VERSION > $DEST/last-proofed -#time nice -n 20 \ -# ./acprep --enable-doxygen --universal -j16 --gcc46 --warn proof 2>&1 | \ -# tee ~/Desktop/proof.log -time nice -n 20 \ - ./acprep --universal -j16 --gcc46 --warn proof 2>&1 | \ - tee ~/Desktop/proof.log + cd $DEST/ledger-proof/debug; make docs + cd $DEST/ledger-proof/gcov; make report -if egrep -q '(ERROR|CRITICAL)' ~/Desktop/proof.log; then - if [[ "$1" = "--alert" ]]; then - notify "Ledger proof build FAILED" - else - echo "Ledger proof build FAILED" - exit 1 + mutt -s '[ledger] Proof build succeeded' johnw@newartisans.com <<EOF +Ledger proof build succeeded! at commit $VERSION. +EOF + echo "Ledger proof build succeeded" fi -else - echo $VERSION > ~/Products/last-proofed - mv ~/Desktop/proof.log /tmp - - cd ~/Products/ledger-proof/debug; make docs - cd ~/Products/ledger-proof/gcov; make report +} - echo "Ledger proof build succeeded" -fi +ledger_proof ${1:-$HOME/src/ledger} ${2:-$HOME/Products} ${3:-$HOME/Library/Logs} exit 0 diff --git a/tools/times.sh b/tools/times.sh index 444da993..d15431bc 100755 --- a/tools/times.sh +++ b/tools/times.sh @@ -2,5 +2,5 @@ time test/RegressTests.py ./ledger test/regress time test/RegressTests.py ./ledger test/regress --verify -time test/RegressTests.py ./ledger test/regress --gmalloc -time test/RegressTests.py ./ledger test/regress --verify --gmalloc +#time test/RegressTests.py ./ledger test/regress --gmalloc +#time test/RegressTests.py ./ledger test/regress --verify --gmalloc @@ -1 +1 @@ -m4_define([VERSION_NUMBER], [3.0.0-20120217]) +m4_define([VERSION_NUMBER], [3.0.0-20120426]) |