diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/account.cc | 551 | ||||
-rw-r--r-- | src/account.h | 273 | ||||
-rw-r--r-- | src/accum.cc | 76 | ||||
-rw-r--r-- | src/accum.h | 86 | ||||
-rw-r--r-- | src/amount.cc | 1278 | ||||
-rw-r--r-- | src/amount.h | 761 | ||||
-rw-r--r-- | src/annotate.cc | 205 | ||||
-rw-r--r-- | src/annotate.h | 265 | ||||
-rw-r--r-- | src/archive.cc | 295 | ||||
-rw-r--r-- | src/archive.h | 92 | ||||
-rw-r--r-- | src/balance.cc | 310 | ||||
-rw-r--r-- | src/balance.h | 602 | ||||
-rw-r--r-- | src/chain.cc | 241 | ||||
-rw-r--r-- | src/chain.h | 91 | ||||
-rw-r--r-- | src/commodity.cc | 705 | ||||
-rw-r--r-- | src/commodity.h | 431 | ||||
-rw-r--r-- | src/compare.cc | 114 | ||||
-rw-r--r-- | src/compare.h | 98 | ||||
-rw-r--r-- | src/draft.cc | 527 | ||||
-rw-r--r-- | src/draft.h | 113 | ||||
-rw-r--r-- | src/emacs.cc | 108 | ||||
-rw-r--r-- | src/emacs.h | 79 | ||||
-rw-r--r-- | src/error.cc | 118 | ||||
-rw-r--r-- | src/error.h | 104 | ||||
-rw-r--r-- | src/expr.cc | 143 | ||||
-rw-r--r-- | src/expr.h | 147 | ||||
-rw-r--r-- | src/exprbase.h | 253 | ||||
-rw-r--r-- | src/filters.cc | 1055 | ||||
-rw-r--r-- | src/filters.h | 721 | ||||
-rw-r--r-- | src/flags.h | 209 | ||||
-rw-r--r-- | src/format.cc | 488 | ||||
-rw-r--r-- | src/format.h | 161 | ||||
-rw-r--r-- | src/generate.cc | 385 | ||||
-rw-r--r-- | src/generate.h | 129 | ||||
-rw-r--r-- | src/global.cc | 480 | ||||
-rw-r--r-- | src/global.h | 163 | ||||
-rw-r--r-- | src/interactive.cc | 194 | ||||
-rw-r--r-- | src/interactive.h | 151 | ||||
-rw-r--r-- | src/item.cc | 467 | ||||
-rw-r--r-- | src/item.h | 215 | ||||
-rw-r--r-- | src/iterators.cc | 258 | ||||
-rw-r--r-- | src/iterators.h | 230 | ||||
-rw-r--r-- | src/journal.cc | 251 | ||||
-rw-r--r-- | src/journal.h | 197 | ||||
-rw-r--r-- | src/main.cc | 217 | ||||
-rw-r--r-- | src/mask.cc | 55 | ||||
-rw-r--r-- | src/mask.h | 157 | ||||
-rw-r--r-- | src/op.cc | 732 | ||||
-rw-r--r-- | src/op.h | 343 | ||||
-rw-r--r-- | src/option.cc | 249 | ||||
-rw-r--r-- | src/option.h | 294 | ||||
-rw-r--r-- | src/output.cc | 254 | ||||
-rw-r--r-- | src/output.h | 110 | ||||
-rw-r--r-- | src/parser.cc | 517 | ||||
-rw-r--r-- | src/parser.h | 107 | ||||
-rw-r--r-- | src/pool.cc | 364 | ||||
-rw-r--r-- | src/pool.h | 156 | ||||
-rw-r--r-- | src/post.cc | 576 | ||||
-rw-r--r-- | src/post.h | 224 | ||||
-rw-r--r-- | src/precmd.cc | 211 | ||||
-rw-r--r-- | src/precmd.h | 59 | ||||
-rw-r--r-- | src/predicate.cc | 46 | ||||
-rw-r--r-- | src/predicate.h | 106 | ||||
-rw-r--r-- | src/pstream.h | 109 | ||||
-rw-r--r-- | src/py_account.cc | 235 | ||||
-rw-r--r-- | src/py_amount.cc | 314 | ||||
-rw-r--r-- | src/py_balance.cc | 238 | ||||
-rw-r--r-- | src/py_commodity.cc | 451 | ||||
-rw-r--r-- | src/py_expr.cc | 68 | ||||
-rw-r--r-- | src/py_format.cc | 64 | ||||
-rw-r--r-- | src/py_item.cc | 166 | ||||
-rw-r--r-- | src/py_journal.cc | 321 | ||||
-rw-r--r-- | src/py_post.cc | 184 | ||||
-rw-r--r-- | src/py_times.cc | 253 | ||||
-rw-r--r-- | src/py_utils.cc | 254 | ||||
-rw-r--r-- | src/py_value.cc | 380 | ||||
-rw-r--r-- | src/py_xact.cc | 153 | ||||
-rw-r--r-- | src/pyfstream.h | 202 | ||||
-rw-r--r-- | src/pyinterp.cc | 505 | ||||
-rw-r--r-- | src/pyinterp.h | 124 | ||||
-rw-r--r-- | src/pyledger.cc | 52 | ||||
-rw-r--r-- | src/pyutils.h | 189 | ||||
-rw-r--r-- | src/query.cc | 455 | ||||
-rw-r--r-- | src/query.h | 305 | ||||
-rw-r--r-- | src/quotes.cc | 109 | ||||
-rw-r--r-- | src/quotes.h | 53 | ||||
-rw-r--r-- | src/report.cc | 1307 | ||||
-rw-r--r-- | src/report.h | 958 | ||||
-rw-r--r-- | src/scope.cc | 74 | ||||
-rw-r--r-- | src/scope.h | 348 | ||||
-rw-r--r-- | src/session.cc | 248 | ||||
-rw-r--r-- | src/session.h | 151 | ||||
-rw-r--r-- | src/stats.cc | 121 | ||||
-rw-r--r-- | src/stats.h | 55 | ||||
-rw-r--r-- | src/stream.cc | 139 | ||||
-rw-r--r-- | src/stream.h | 142 | ||||
-rw-r--r-- | src/system.hh.in | 256 | ||||
-rw-r--r-- | src/temps.cc | 137 | ||||
-rw-r--r-- | src/temps.h | 76 | ||||
-rw-r--r-- | src/textual.cc | 1396 | ||||
-rw-r--r-- | src/timelog.cc | 158 | ||||
-rw-r--r-- | src/timelog.h | 103 | ||||
-rw-r--r-- | src/times.cc | 1587 | ||||
-rw-r--r-- | src/times.h | 632 | ||||
-rw-r--r-- | src/token.cc | 485 | ||||
-rw-r--r-- | src/token.h | 135 | ||||
-rw-r--r-- | src/unistring.h | 128 | ||||
-rw-r--r-- | src/utils.cc | 821 | ||||
-rw-r--r-- | src/utils.h | 713 | ||||
-rw-r--r-- | src/value.cc | 1864 | ||||
-rw-r--r-- | src/value.h | 998 | ||||
-rw-r--r-- | src/xact.cc | 781 | ||||
-rw-r--r-- | src/xact.h | 232 | ||||
-rw-r--r-- | src/xml.cc | 124 | ||||
-rw-r--r-- | src/xml.h | 90 |
115 files changed, 38040 insertions, 0 deletions
diff --git a/src/account.cc b/src/account.cc new file mode 100644 index 00000000..e6c7af56 --- /dev/null +++ b/src/account.cc @@ -0,0 +1,551 @@ +/* + * Copyright (c) 2003-2009, 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 "account.h" +#include "post.h" +#include "xact.h" +#include "interactive.h" + +namespace ledger { + +account_t::~account_t() +{ + TRACE_DTOR(account_t); + + foreach (accounts_map::value_type& pair, accounts) + if (! pair.second->has_flags(ACCOUNT_TEMP)) + checked_delete(pair.second); +} + +account_t * account_t::find_account(const string& name, + const bool auto_create) +{ + accounts_map::const_iterator i = accounts.find(name); + if (i != accounts.end()) + return (*i).second; + + char buf[8192]; + + string::size_type sep = name.find(':'); + assert(sep < 256|| sep == string::npos); + + const char * first, * rest; + if (sep == string::npos) { + first = name.c_str(); + rest = NULL; + } else { + std::strncpy(buf, name.c_str(), sep); + buf[sep] = '\0'; + + first = buf; + rest = name.c_str() + sep + 1; + } + + account_t * account; + + i = accounts.find(first); + if (i == accounts.end()) { + if (! auto_create) + return NULL; + + account = new account_t(this, first); + std::pair<accounts_map::iterator, bool> result + = accounts.insert(accounts_map::value_type(first, account)); + assert(result.second); + } else { + account = (*i).second; + } + + if (rest) + account = account->find_account(rest, auto_create); + + return account; +} + +namespace { + account_t * find_account_re_(account_t * account, const mask_t& regexp) + { + if (regexp.match(account->fullname())) + return account; + + foreach (accounts_map::value_type& pair, account->accounts) + if (account_t * a = find_account_re_(pair.second, regexp)) + return a; + + return NULL; + } +} + +account_t * account_t::find_account_re(const string& regexp) +{ + return find_account_re_(this, mask_t(regexp)); +} + +bool account_t::remove_post(post_t * post) +{ + assert(! posts.empty()); + posts.remove(post); + post->account = NULL; + return true; +} + +string account_t::fullname() const +{ + if (! _fullname.empty()) { + return _fullname; + } else { + const account_t * first = this; + string fullname = name; + + while (first->parent) { + first = first->parent; + if (! first->name.empty()) + fullname = first->name + ":" + fullname; + } + + _fullname = fullname; + + return fullname; + } +} + +string account_t::partial_name(bool flat) const +{ + string pname = name; + + for (const account_t * acct = parent; + acct && acct->parent; + acct = acct->parent) { + if (! flat) { + std::size_t count = acct->children_with_flags(ACCOUNT_EXT_TO_DISPLAY); + assert(count > 0); + if (count > 1 || acct->has_xflags(ACCOUNT_EXT_TO_DISPLAY)) + break; + } + pname = acct->name + ":" + pname; + } + return pname; +} + +std::ostream& operator<<(std::ostream& out, const account_t& account) +{ + out << account.fullname(); + return out; +} + +namespace { + value_t get_partial_name(call_scope_t& scope) + { + in_context_t<account_t> env(scope, "&b"); + return string_value(env->partial_name(env.has(0) ? + env.get<bool>(0) : false)); + } + + value_t get_account(account_t& account) { // this gets the name + return string_value(account.fullname()); + } + + value_t get_account_base(account_t& account) { + return string_value(account.name); + } + + value_t get_amount(account_t& account) { + return SIMPLIFIED_VALUE_OR_ZERO(account.amount()); + } + + value_t get_total(account_t& account) { + return SIMPLIFIED_VALUE_OR_ZERO(account.total()); + } + + value_t get_subcount(account_t& account) { + return long(account.self_details().posts_count); + } + + value_t get_count(account_t& account) { + return long(account.family_details().posts_count); + } + + value_t get_depth(account_t& account) { + return long(account.depth); + } + + value_t ignore(account_t&) { + return false; + } + + value_t get_true(account_t&) { + return true; + } + + value_t get_depth_spacer(account_t& account) + { + std::size_t depth = 0; + for (const account_t * acct = account.parent; + acct && acct->parent; + acct = acct->parent) { + std::size_t count = acct->children_with_flags(ACCOUNT_EXT_TO_DISPLAY); + assert(count > 0); + if (count > 1 || acct->has_xflags(ACCOUNT_EXT_TO_DISPLAY)) + depth++; + } + + std::ostringstream out; + for (std::size_t i = 0; i < depth; i++) + out << " "; + + return string_value(out.str()); + } + + value_t get_latest_cleared(account_t& account) + { + return account.self_details().latest_cleared_post; + } + + template <value_t (*Func)(account_t&)> + value_t get_wrapper(call_scope_t& scope) { + return (*Func)(find_scope<account_t>(scope)); + } + + value_t get_parent(account_t& account) { + return value_t(static_cast<scope_t *>(account.parent)); + } +} + +expr_t::ptr_op_t account_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (kind != symbol_t::FUNCTION) + return NULL; + + switch (name[0]) { + case 'a': + if (name[1] == '\0' || name == "amount") + return WRAP_FUNCTOR(get_wrapper<&get_amount>); + else if (name == "account") + return WRAP_FUNCTOR(get_wrapper<&get_account>); + else if (name == "account_base") + return WRAP_FUNCTOR(get_wrapper<&get_account_base>); + break; + + case 'c': + if (name == "count") + return WRAP_FUNCTOR(get_wrapper<&get_count>); + break; + + case 'd': + if (name == "depth") + return WRAP_FUNCTOR(get_wrapper<&get_depth>); + else if (name == "depth_spacer") + return WRAP_FUNCTOR(get_wrapper<&get_depth_spacer>); + break; + + case 'i': + if (name == "is_account") + return WRAP_FUNCTOR(get_wrapper<&get_true>); + else if (name == "is_index") + return WRAP_FUNCTOR(get_wrapper<&get_subcount>); + break; + + case 'l': + if (name == "latest_cleared") + return WRAP_FUNCTOR(get_wrapper<&get_latest_cleared>); + else if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_depth>); + break; + + case 'n': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_subcount>); + break; + + case 'p': + if (name == "partial_account") + return WRAP_FUNCTOR(get_partial_name); + else if (name == "parent") + return WRAP_FUNCTOR(get_wrapper<&get_parent>); + break; + + case 's': + if (name == "subcount") + return WRAP_FUNCTOR(get_wrapper<&get_subcount>); + break; + + case 't': + if (name == "total") + return WRAP_FUNCTOR(get_wrapper<&get_total>); + break; + + case 'u': + if (name == "use_direct_amount") + return WRAP_FUNCTOR(get_wrapper<&ignore>); + break; + + case 'N': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_count>); + break; + + case 'O': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_total>); + break; + } + + return NULL; +} + +bool account_t::valid() const +{ + if (depth > 256) { + DEBUG("ledger.validate", "account_t: depth > 256"); + return false; + } + + foreach (const accounts_map::value_type& pair, accounts) { + if (this == pair.second) { + DEBUG("ledger.validate", "account_t: parent refers to itself!"); + return false; + } + + if (! pair.second->valid()) { + DEBUG("ledger.validate", "account_t: child not valid"); + return false; + } + } + + return true; +} + +bool account_t::children_with_xdata() const +{ + foreach (const accounts_map::value_type& pair, accounts) + if (pair.second->has_xdata() || + pair.second->children_with_xdata()) + return true; + + return false; +} + +std::size_t account_t::children_with_flags(xdata_t::flags_t flags) const +{ + std::size_t count = 0; + bool grandchildren_visited = false; + + foreach (const accounts_map::value_type& pair, accounts) + if (pair.second->has_xflags(flags) || + pair.second->children_with_flags(flags)) + count++; + + // Although no immediately children were visited, if any progeny at all were + // visited, it counts as one. + if (count == 0 && grandchildren_visited) + count = 1; + + return count; +} + +account_t::xdata_t::details_t& +account_t::xdata_t::details_t::operator+=(const details_t& other) +{ + posts_count += other.posts_count; + posts_virtuals_count += other.posts_virtuals_count; + posts_cleared_count += other.posts_cleared_count; + posts_last_7_count += other.posts_last_7_count; + posts_last_30_count += other.posts_last_30_count; + posts_this_month_count += other.posts_this_month_count; + + if (! is_valid(earliest_post) || + (is_valid(other.earliest_post) && + other.earliest_post < earliest_post)) + earliest_post = other.earliest_post; + if (! is_valid(earliest_cleared_post) || + (is_valid(other.earliest_cleared_post) && + other.earliest_cleared_post < earliest_cleared_post)) + earliest_cleared_post = other.earliest_cleared_post; + + if (! is_valid(latest_post) || + (is_valid(other.latest_post) && + other.latest_post > latest_post)) + latest_post = other.latest_post; + if (! is_valid(latest_cleared_post) || + (is_valid(other.latest_cleared_post) && + other.latest_cleared_post > latest_cleared_post)) + latest_cleared_post = other.latest_cleared_post; + + filenames.insert(other.filenames.begin(), other.filenames.end()); + accounts_referenced.insert(other.accounts_referenced.begin(), + other.accounts_referenced.end()); + payees_referenced.insert(other.payees_referenced.begin(), + other.payees_referenced.end()); + return *this; +} + +void account_t::clear_xdata() +{ + xdata_ = none; + + foreach (accounts_map::value_type& pair, accounts) + if (! pair.second->has_flags(ACCOUNT_TEMP)) + pair.second->clear_xdata(); +} + +value_t account_t::amount(const optional<expr_t&>& expr) const +{ + if (xdata_ && xdata_->has_flags(ACCOUNT_EXT_VISITED)) { + posts_list::const_iterator i; + if (xdata_->self_details.last_post) + i = *xdata_->self_details.last_post; + else + i = posts.begin(); + + for (; i != posts.end(); i++) { + if ((*i)->xdata().has_flags(POST_EXT_VISITED)) { + if (! (*i)->xdata().has_flags(POST_EXT_CONSIDERED)) { + (*i)->add_to_value(xdata_->self_details.total, expr); + (*i)->xdata().add_flags(POST_EXT_CONSIDERED); + } + } + xdata_->self_details.last_post = i; + } + + if (xdata_->self_details.last_reported_post) + i = *xdata_->self_details.last_reported_post; + else + i = xdata_->reported_posts.begin(); + + for (; i != xdata_->reported_posts.end(); i++) { + if ((*i)->xdata().has_flags(POST_EXT_VISITED)) { + if (! (*i)->xdata().has_flags(POST_EXT_CONSIDERED)) { + (*i)->add_to_value(xdata_->self_details.total, expr); + (*i)->xdata().add_flags(POST_EXT_CONSIDERED); + } + } + xdata_->self_details.last_reported_post = i; + } + + return xdata_->self_details.total; + } else { + return NULL_VALUE; + } +} + +value_t account_t::total(const optional<expr_t&>& expr) const +{ + if (! (xdata_ && xdata_->family_details.calculated)) { + const_cast<account_t&>(*this).xdata().family_details.calculated = true; + + value_t temp; + foreach (const accounts_map::value_type& pair, accounts) { + temp = pair.second->total(expr); + if (! temp.is_null()) + add_or_set_value(xdata_->family_details.total, temp); + } + + temp = amount(expr); + if (! temp.is_null()) + add_or_set_value(xdata_->family_details.total, temp); + } + return xdata_->family_details.total; +} + +const account_t::xdata_t::details_t& +account_t::self_details(bool gather_all) const +{ + if (! (xdata_ && xdata_->self_details.gathered)) { + const_cast<account_t&>(*this).xdata().self_details.gathered = true; + + foreach (const post_t * post, posts) + xdata_->self_details.update(const_cast<post_t&>(*post), gather_all); + } + return xdata_->self_details; +} + +const account_t::xdata_t::details_t& +account_t::family_details(bool gather_all) const +{ + if (! (xdata_ && xdata_->family_details.gathered)) { + const_cast<account_t&>(*this).xdata().family_details.gathered = true; + + foreach (const accounts_map::value_type& pair, accounts) + xdata_->family_details += pair.second->family_details(gather_all); + + xdata_->family_details += self_details(gather_all); + } + return xdata_->family_details; +} + +void account_t::xdata_t::details_t::update(post_t& post, + bool gather_all) +{ + posts_count++; + + if (post.has_flags(POST_VIRTUAL)) + posts_virtuals_count++; + + if (gather_all) + filenames.insert(post.pos->pathname); + + date_t date = post.date(); + + if (date.year() == CURRENT_DATE().year() && + date.month() == CURRENT_DATE().month()) + posts_this_month_count++; + + if ((CURRENT_DATE() - date).days() <= 30) + posts_last_30_count++; + if ((CURRENT_DATE() - date).days() <= 7) + posts_last_7_count++; + + if (! is_valid(earliest_post) || post.date() < earliest_post) + earliest_post = post.date(); + if (! is_valid(latest_post) || post.date() > latest_post) + latest_post = post.date(); + + if (post.state() == item_t::CLEARED) { + posts_cleared_count++; + + if (! is_valid(earliest_cleared_post) || + post.date() < earliest_cleared_post) + earliest_cleared_post = post.date(); + if (! is_valid(latest_cleared_post) || + post.date() > latest_cleared_post) + latest_cleared_post = post.date(); + } + + if (gather_all) { + accounts_referenced.insert(post.account->fullname()); + payees_referenced.insert(post.xact->payee); + } +} + +} // namespace ledger diff --git a/src/account.h b/src/account.h new file mode 100644 index 00000000..73cd35ac --- /dev/null +++ b/src/account.h @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup data + */ + +/** + * @file account.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _ACCOUNT_H +#define _ACCOUNT_H + +#include "scope.h" + +namespace ledger { + +class account_t; +class xact_t; +class post_t; + +typedef std::list<post_t *> posts_list; +typedef std::map<const string, account_t *> accounts_map; + +class account_t : public supports_flags<>, public scope_t +{ +#define ACCOUNT_NORMAL 0x00 // no flags at all, a basic account +#define ACCOUNT_KNOWN 0x01 +#define ACCOUNT_TEMP 0x02 // account is a temporary object +#define ACCOUNT_GENERATED 0x04 // account never actually existed + +public: + account_t * parent; + string name; + optional<string> note; + unsigned short depth; + accounts_map accounts; + posts_list posts; + + mutable string _fullname; + + 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)) { + TRACE_CTOR(account_t, "account_t *, const string&, const string&"); + } + account_t(const account_t& other) + : supports_flags<>(other.flags()), scope_t(), + parent(other.parent), + name(other.name), + note(other.note), + depth(other.depth), + accounts(other.accounts) { + TRACE_CTOR(account_t, "copy"); + } + ~account_t(); + + operator string() const { + return fullname(); + } + string fullname() const; + string partial_name(bool flat = false) const; + + void add_account(account_t * acct) { + accounts.insert(accounts_map::value_type(acct->name, acct)); + } + bool remove_account(account_t * acct) { + accounts_map::size_type n = accounts.erase(acct->name); + return n > 0; + } + + account_t * find_account(const string& name, bool auto_create = true); + account_t * find_account_re(const string& regexp); + + typedef transform_iterator<function<account_t *(accounts_map::value_type&)>, + accounts_map::iterator> + accounts_map_seconds_iterator; + + accounts_map_seconds_iterator accounts_begin() { + return make_transform_iterator + (accounts.begin(), bind(&accounts_map::value_type::second, _1)); + } + accounts_map_seconds_iterator accounts_end() { + return make_transform_iterator + (accounts.end(), bind(&accounts_map::value_type::second, _1)); + } + + void add_post(post_t * post) { + posts.push_back(post); + } + bool remove_post(post_t * post); + + posts_list::iterator posts_begin() { + return posts.begin(); + } + posts_list::iterator posts_end() { + return posts.end(); + } + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + bool valid() const; + + friend class journal_t; + + struct xdata_t : public supports_flags<> + { +#define ACCOUNT_EXT_SORT_CALC 0x01 +#define ACCOUNT_EXT_HAS_NON_VIRTUALS 0x02 +#define ACCOUNT_EXT_HAS_UNB_VIRTUALS 0x04 +#define ACCOUNT_EXT_AUTO_VIRTUALIZE 0x08 +#define ACCOUNT_EXT_VISITED 0x10 +#define ACCOUNT_EXT_MATCHING 0x20 +#define ACCOUNT_EXT_TO_DISPLAY 0x40 +#define ACCOUNT_EXT_DISPLAYED 0x80 + + struct details_t + { + value_t total; + bool calculated; + bool gathered; + + std::size_t posts_count; + std::size_t posts_virtuals_count; + std::size_t posts_cleared_count; + std::size_t posts_last_7_count; + std::size_t posts_last_30_count; + std::size_t posts_this_month_count; + + date_t earliest_post; + date_t earliest_cleared_post; + date_t latest_post; + date_t latest_cleared_post; + + std::set<path> filenames; + std::set<string> accounts_referenced; + std::set<string> payees_referenced; + + optional<posts_list::const_iterator> last_post; + optional<posts_list::const_iterator> last_reported_post; + + details_t() + : calculated(false), + gathered(false), + + posts_count(0), + posts_virtuals_count(0), + posts_cleared_count(0), + posts_last_7_count(0), + posts_last_30_count(0), + posts_this_month_count(0) {} + + details_t& operator+=(const details_t& other); + + void update(post_t& post, bool gather_all = false); + }; + + details_t self_details; + details_t family_details; + posts_list reported_posts; + + std::list<sort_value_t> sort_values; + + xdata_t() : supports_flags<>() + { + TRACE_CTOR(account_t::xdata_t, ""); + } + xdata_t(const xdata_t& other) + : supports_flags<>(other.flags()), + self_details(other.self_details), + family_details(other.family_details), + sort_values(other.sort_values) + { + TRACE_CTOR(account_t::xdata_t, "copy"); + } + ~xdata_t() throw() { + TRACE_DTOR(account_t::xdata_t); + } + }; + + // This variable holds optional "extended data" which is usually produced + // only during reporting, and only for the posting set being reported. + // It's a memory-saving measure to delay allocation until the last possible + // moment. + mutable optional<xdata_t> xdata_; + + bool has_xdata() const { + return xdata_; + } + void clear_xdata(); + xdata_t& xdata() { + if (! xdata_) + xdata_ = xdata_t(); + return *xdata_; + } + const xdata_t& xdata() const { + assert(xdata_); + return *xdata_; + } + + value_t amount(const optional<expr_t&>& expr = none) const; + value_t total(const optional<expr_t&>& expr = none) const; + + const xdata_t::details_t& self_details(bool gather_all = true) const; + const xdata_t::details_t& family_details(bool gather_all = true) const; + + bool has_xflags(xdata_t::flags_t flags) const { + return xdata_ && xdata_->has_flags(flags); + } + bool children_with_xdata() const; + std::size_t children_with_flags(xdata_t::flags_t flags) const; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<supports_flags<> >(*this); + ar & boost::serialization::base_object<scope_t>(*this); + ar & parent; + ar & name; + ar & note; + ar & depth; + ar & accounts; + ar & posts; + ar & _fullname; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +std::ostream& operator<<(std::ostream& out, const account_t& account); + +} // namespace ledger + +#endif // _ACCOUNT_H diff --git a/src/accum.cc b/src/accum.cc new file mode 100644 index 00000000..b918c76a --- /dev/null +++ b/src/accum.cc @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2003-2009, 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 "utils.h" + +namespace ledger { + +std::streamsize straccbuf::xsputn(const char * s, std::streamsize num) +{ + if (index == 0) { + // The first item received is the format string + str = std::string(s, num); + index++; + return num; + } + else { + std::ostringstream buf; + + // Every item thereafter is an argument that substitutes for %# in the + // format string + bool matched = false; + for (const char * p = str.c_str(); *p; p++) { + if (*p == '%') { + const char * q = p + 1; + if (*q && *q != '%' && std::isdigit(*q) && + std::string::size_type(*q - '0') == index) { + p++; + buf << std::string(s, num); + matched = true; + } else { + buf << *p; + } + } else { + buf << *p; + } + } + if (! matched) + buf << std::string(s, num); + + str = buf.str(); + index++; + return num; + } +} + +} // namespace ledger diff --git a/src/accum.h b/src/accum.h new file mode 100644 index 00000000..878c2b7c --- /dev/null +++ b/src/accum.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file accum.h + * @author John Wiegley + * + * @ingroup util + */ +#ifndef _ACCUM_H +#define _ACCUM_H + +namespace ledger { + +class straccbuf : public std::streambuf +{ +protected: + std::string str; // accumulator + std::string::size_type index; + +public: + straccbuf() : index(0) {} + +protected: + virtual std::streamsize xsputn(const char * s, std::streamsize num); + + friend class straccstream; +}; + +class straccstream : public std::ostream +{ +protected: + straccbuf buf; + +public: + straccstream() : std::ostream(0) { + rdbuf(&buf); + } + + void clear() { + buf.str.clear(); + buf.index = 0; + } + + std::string str() const { + return buf.str; + } +}; + +#define ACCUM(obj) (static_cast<straccstream&>(obj).str()) + +} // namespace ledger + +#endif // _ACCUM_H diff --git a/src/amount.cc b/src/amount.cc new file mode 100644 index 00000000..eddbca18 --- /dev/null +++ b/src/amount.cc @@ -0,0 +1,1278 @@ +/* + * Copyright (c) 2003-2009, 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 "amount.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +bool amount_t::stream_fullstrings = false; + +#if !defined(THREADSAFE) +// These global temporaries are pre-initialized for the sake of +// efficiency, and are reused over and over again. +static mpz_t temp; +static mpq_t tempq; +static mpfr_t tempf; +static mpfr_t tempfb; +#endif + +struct amount_t::bigint_t : public supports_flags<> +{ +#define BIGINT_BULK_ALLOC 0x01 +#define BIGINT_KEEP_PREC 0x02 + + mpq_t val; + precision_t prec; + uint_least32_t refc; + +#define MP(bigint) ((bigint)->val) + + bigint_t() : prec(0), refc(1) { + TRACE_CTOR(bigint_t, ""); + mpq_init(val); + } + 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); + } + ~bigint_t() { + TRACE_DTOR(bigint_t); + assert(refc == 0); + mpq_clear(val); + } + + bool valid() const { + if (prec > 1024) { + DEBUG("ledger.validate", "amount_t::bigint_t: prec > 1024"); + return false; + } + if (flags() & ~(BIGINT_BULK_ALLOC | BIGINT_KEEP_PREC)) { + DEBUG("ledger.validate", + "amount_t::bigint_t: flags() & ~(BULK_ALLOC | KEEP_PREC)"); + return false; + } + return true; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) + { + ar & boost::serialization::base_object<supports_flags<> >(*this); + ar & val; + ar & prec; + ar & refc; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +bool amount_t::is_initialized = false; + +namespace { + void stream_out_mpq(std::ostream& out, + mpq_t quant, + amount_t::precision_t prec, + int zeros_prec = -1, + const optional<commodity_t&>& comm = none) + { + char * buf = NULL; + try { + IF_DEBUG("amount.convert") { + char * tbuf = mpq_get_str(NULL, 10, quant); + DEBUG("amount.convert", "Rational to convert = " << tbuf); + std::free(tbuf); + } + + // Convert the rational number to a floating-point, extending the + // floating-point to a large enough size to get a precise answer. + const std::size_t bits = (mpz_sizeinbase(mpq_numref(quant), 2) + + mpz_sizeinbase(mpq_denref(quant), 2)); + mpfr_set_prec(tempfb, bits + amount_t::extend_by_digits*8); + mpfr_set_q(tempfb, quant, GMP_RNDN); + + mpfr_asprintf(&buf, "%.*Rf", prec, tempfb); + DEBUG("amount.convert", + "mpfr_print = " << buf << " (precision " << prec << ")"); + + if (zeros_prec >= 0) { + string::size_type index = std::strlen(buf); + string::size_type point = 0; + for (string::size_type i = 0; i < index; i++) { + if (buf[i] == '.') { + point = i; + break; + } + } + if (point > 0) { + while (--index >= (point + 1 + zeros_prec) && buf[index] == '0') + buf[index] = '\0'; + if (index >= (point + zeros_prec) && buf[index] == '.') + buf[index] = '\0'; + } + } + + if (comm) { + int integer_digits = 0; + if (comm && comm->has_flags(COMMODITY_STYLE_THOUSANDS)) { + // Count the number of integer digits + for (const char * p = buf; *p; p++) { + if (*p == '.') + break; + else if (*p != '-') + integer_digits++; + } + } + + for (const char * p = buf; *p; p++) { + if (*p == '.') { + if (commodity_t::european_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_EUROPEAN))) + out << ','; + else + out << *p; + assert(integer_digits <= 3); + } + else if (*p == '-') { + out << *p; + } + else { + out << *p; + + if (integer_digits > 3 && --integer_digits % 3 == 0) { + if (commodity_t::european_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_EUROPEAN))) + out << '.'; + else + out << ','; + } + } + } + } else { + out << buf; + } + } + catch (...) { + if (buf != NULL) + mpfr_free_str(buf); + throw; + } + if (buf != NULL) + mpfr_free_str(buf); + } +} + +void amount_t::initialize() +{ + if (! is_initialized) { + mpz_init(temp); + mpq_init(tempq); + mpfr_init(tempf); + mpfr_init(tempfb); + + commodity_pool_t::current_pool.reset(new commodity_pool_t); + + // Add time commodity conversions, so that timelog's may be parsed + // in terms of seconds, but reported as minutes or hours. + if (commodity_t * commodity = commodity_pool_t::current_pool->create("s")) + commodity->add_flags(COMMODITY_BUILTIN | COMMODITY_NOMARKET); + else + assert(false); + + // Add a "percentile" commodity + if (commodity_t * commodity = commodity_pool_t::current_pool->create("%")) + commodity->add_flags(COMMODITY_BUILTIN | COMMODITY_NOMARKET); + else + assert(false); + + is_initialized = true; + } +} + +void amount_t::shutdown() +{ + if (is_initialized) { + mpz_clear(temp); + mpq_clear(tempq); + mpfr_clear(tempf); + mpfr_clear(tempfb); + + commodity_pool_t::current_pool.reset(); + + is_initialized = false; + } +} + +void amount_t::_copy(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (quantity != amt.quantity) { + if (quantity) + _release(); + + // Never maintain a pointer into a bulk allocation pool; such + // pointers are not guaranteed to remain. + if (amt.quantity->has_flags(BIGINT_BULK_ALLOC)) { + quantity = new bigint_t(*amt.quantity); + } else { + quantity = amt.quantity; + DEBUG("amounts.refs", + quantity << " refc++, now " << (quantity->refc + 1)); + quantity->refc++; + } + } + commodity_ = amt.commodity_; + + VERIFY(valid()); +} + +void amount_t::_dup() +{ + VERIFY(valid()); + + if (quantity->refc > 1) { + bigint_t * q = new bigint_t(*quantity); + _release(); + quantity = q; + } + + VERIFY(valid()); +} + +void amount_t::_clear() +{ + if (quantity) { + _release(); + quantity = NULL; + commodity_ = NULL; + } else { + assert(! commodity_); + } +} + +void amount_t::_release() +{ + VERIFY(valid()); + + DEBUG("amounts.refs", quantity << " refc--, now " << (quantity->refc - 1)); + + if (--quantity->refc == 0) { + if (quantity->has_flags(BIGINT_BULK_ALLOC)) + quantity->~bigint_t(); + else + checked_delete(quantity); + quantity = NULL; + commodity_ = NULL; + } + + VERIFY(valid()); +} + + +amount_t::amount_t(const double val) : commodity_(NULL) +{ + TRACE_CTOR(amount_t, "const double"); + quantity = new bigint_t; + mpq_set_d(MP(quantity), val); + quantity->prec = extend_by_digits; // an approximation +} + +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); +} + +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); +} + + +amount_t& amount_t::operator=(const amount_t& amt) +{ + if (this != &amt) { + if (amt.quantity) + _copy(amt); + else if (quantity) + _clear(); + } + return *this; +} + + +int amount_t::compare(const amount_t& amt) const +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot compare an amount to an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot compare an uninitialized amount to an amount")); + else + throw_(amount_error, _("Cannot compare two uninitialized amounts")); + } + + if (has_commodity() && amt.has_commodity() && + commodity() != amt.commodity()) + throw_(amount_error, + _("Cannot compare amounts with different commodities: %1 and %2") + << commodity().symbol() << amt.commodity().symbol()); + + return mpq_cmp(MP(quantity), MP(amt.quantity)); +} + +bool amount_t::operator==(const amount_t& amt) const +{ + if ((quantity && ! amt.quantity) || (! quantity && amt.quantity)) + return false; + else if (! quantity && ! amt.quantity) + return true; + else if (commodity() != amt.commodity()) + return false; + + return mpq_equal(MP(quantity), MP(amt.quantity)); +} + + +amount_t& amount_t::operator+=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot add an uninitialized amount to an amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot add an amount to an uninitialized amount")); + else + throw_(amount_error, _("Cannot add two uninitialized amounts")); + } + + if (has_commodity() && amt.has_commodity() && + commodity() != amt.commodity()) + throw_(amount_error, + _("Adding amounts with different commodities: %1 != %2") + << (has_commodity() ? commodity().symbol() : _("NONE")) + << (amt.has_commodity() ? amt.commodity().symbol() : _("NONE"))); + + _dup(); + + mpq_add(MP(quantity), MP(quantity), MP(amt.quantity)); + + if (has_commodity() == amt.has_commodity()) + if (quantity->prec < amt.quantity->prec) + quantity->prec = amt.quantity->prec; + + return *this; +} + +amount_t& amount_t::operator-=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot subtract an amount from an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot subtract an uninitialized amount from an amount")); + else + throw_(amount_error, _("Cannot subtract two uninitialized amounts")); + } + + if (has_commodity() && amt.has_commodity() && + commodity() != amt.commodity()) + throw_(amount_error, + _("Subtracting amounts with different commodities: %1 != %2") + << (has_commodity() ? commodity().symbol() : _("NONE")) + << (amt.has_commodity() ? amt.commodity().symbol() : _("NONE"))); + + _dup(); + + mpq_sub(MP(quantity), MP(quantity), MP(amt.quantity)); + + if (has_commodity() == amt.has_commodity()) + if (quantity->prec < amt.quantity->prec) + quantity->prec = amt.quantity->prec; + + return *this; +} + +amount_t& amount_t::operator*=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot multiply an amount by an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot multiply an uninitialized amount by an amount")); + else + throw_(amount_error, _("Cannot multiply two uninitialized amounts")); + } + + _dup(); + + mpq_mul(MP(quantity), MP(quantity), MP(amt.quantity)); + quantity->prec = + static_cast<precision_t>(quantity->prec + amt.quantity->prec); + + if (! has_commodity()) + commodity_ = amt.commodity_; + + if (has_commodity() && ! keep_precision()) { + precision_t comm_prec = commodity().precision(); + if (quantity->prec > comm_prec + extend_by_digits) + quantity->prec = static_cast<precision_t>(comm_prec + extend_by_digits); + } + + return *this; +} + +amount_t& amount_t::operator/=(const amount_t& amt) +{ + VERIFY(amt.valid()); + + if (! quantity || ! amt.quantity) { + if (quantity) + throw_(amount_error, _("Cannot divide an amount by an uninitialized amount")); + else if (amt.quantity) + throw_(amount_error, _("Cannot divide an uninitialized amount by an amount")); + else + throw_(amount_error, _("Cannot divide two uninitialized amounts")); + } + + if (! amt) + throw_(amount_error, _("Divide by zero")); + + _dup(); + + // Increase the value's precision, to capture fractional parts after + // the divide. Round up in the last position. + + mpq_div(MP(quantity), MP(quantity), MP(amt.quantity)); + quantity->prec = + static_cast<precision_t>(quantity->prec + amt.quantity->prec + + extend_by_digits); + + if (! has_commodity()) + commodity_ = amt.commodity_; + + // If this amount has a commodity, and we're not dealing with plain + // numbers, or internal numbers (which keep full precision at all + // times), then round the number to within the commodity's precision + // plus six places. + + if (has_commodity() && ! keep_precision()) { + precision_t comm_prec = commodity().precision(); + if (quantity->prec > comm_prec + extend_by_digits) + quantity->prec = static_cast<precision_t>(comm_prec + extend_by_digits); + } + + return *this; +} + +amount_t::precision_t amount_t::precision() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine precision of an uninitialized amount")); + + return quantity->prec; +} + +bool amount_t::keep_precision() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine if precision of an uninitialized amount is kept")); + + return quantity->has_flags(BIGINT_KEEP_PREC); +} + +void amount_t::set_keep_precision(const bool keep) const +{ + if (! quantity) + throw_(amount_error, + _("Cannot set whether to keep the precision of an uninitialized amount")); + + if (keep) + quantity->add_flags(BIGINT_KEEP_PREC); + else + quantity->drop_flags(BIGINT_KEEP_PREC); +} + +amount_t::precision_t amount_t::display_precision() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine display precision of an uninitialized amount")); + + commodity_t& comm(commodity()); + + if (! comm || keep_precision()) + return quantity->prec; + else if (comm.precision() != quantity->prec) + return comm.precision(); + else + return quantity->prec; +} + +void amount_t::in_place_negate() +{ + if (quantity) { + _dup(); + mpq_neg(MP(quantity), MP(quantity)); + } else { + throw_(amount_error, _("Cannot negate an uninitialized amount")); + } +} + +amount_t amount_t::inverted() const +{ + 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; +} + +void amount_t::in_place_round() +{ + if (! quantity) + throw_(amount_error, _("Cannot set rounding for an uninitialized amount")); + else if (! keep_precision()) + return; + + _dup(); + set_keep_precision(false); +} + +void amount_t::in_place_floor() +{ + if (! quantity) + throw_(amount_error, _("Cannot floor an uninitialized amount")); + + _dup(); + + std::ostringstream out; + stream_out_mpq(out, MP(quantity), 0); + + mpq_set_str(MP(quantity), out.str().c_str(), 10); +} + +void amount_t::in_place_unround() +{ + if (! quantity) + throw_(amount_error, _("Cannot unround an uninitialized amount")); + else if (keep_precision()) + return; + + _dup(); + + DEBUG("amount.unround", "Unrounding " << *this); + set_keep_precision(true); + DEBUG("amount.unround", "Unrounded = " << *this); +} + +void amount_t::in_place_reduce() +{ + if (! quantity) + throw_(amount_error, _("Cannot reduce an uninitialized amount")); + + while (commodity_ && commodity().smaller()) { + *this *= commodity().smaller()->number(); + commodity_ = commodity().smaller()->commodity_; + } +} + +void amount_t::in_place_unreduce() +{ + if (! quantity) + throw_(amount_error, _("Cannot unreduce an uninitialized amount")); + + amount_t temp = *this; + commodity_t * comm = commodity_; + bool shifted = false; + + while (comm && comm->larger()) { + amount_t next_temp = temp / comm->larger()->number(); + if (next_temp.abs() < amount_t(1L)) + break; + temp = next_temp; + comm = comm->larger()->commodity_; + shifted = true; + } + + if (shifted) { + *this = temp; + commodity_ = comm; + } +} + +optional<amount_t> +amount_t::value(const bool primary_only, + const optional<datetime_t>& moment, + const optional<commodity_t&>& in_terms_of) const +{ + if (quantity) { +#if defined(DEBUG_ON) + DEBUG("commodity.prices.find", + "amount_t::value of " << commodity().symbol()); + if (moment) + DEBUG("commodity.prices.find", + "amount_t::value: moment = " << *moment); + if (in_terms_of) + DEBUG("commodity.prices.find", + "amount_t::value: in_terms_of = " << in_terms_of->symbol()); +#endif + if (has_commodity() && + (! primary_only || ! commodity().has_flags(COMMODITY_PRIMARY))) { + if (in_terms_of && commodity() == *in_terms_of) { + return *this; + } + else if (has_annotation() && annotation().price && + annotation().has_flags(ANNOTATION_PRICE_FIXATED)) { + return (*annotation().price * number()).rounded(); + } + else { + optional<price_point_t> point = + commodity().find_price(in_terms_of, moment); + + // Whether a price was found or not, check whether we should attempt + // to download a price from the Internet. This is done if (a) no + // price was found, or (b) the price is "stale" according to the + // setting of --price-exp. + point = commodity().check_for_updated_price(point, moment, in_terms_of); + if (point) + return (point->price * number()).rounded(); + } + } + } else { + throw_(amount_error, + _("Cannot determine value of an uninitialized amount")); + } + return none; +} + +amount_t amount_t::price() const +{ + if (has_annotation() && annotation().price) { + amount_t temp(*annotation().price); + temp *= *this; + DEBUG("amount.price", "Returning price of " << *this << " = " << temp); + return temp; + } + return *this; +} + + +int amount_t::sign() const +{ + if (! quantity) + throw_(amount_error, _("Cannot determine sign of an uninitialized amount")); + + return mpq_sgn(MP(quantity)); +} + +bool amount_t::is_zero() const +{ + if (! quantity) + throw_(amount_error, _("Cannot determine if an uninitialized amount is zero")); + + if (has_commodity()) { + if (keep_precision() || quantity->prec <= commodity().precision()) { + return is_realzero(); + } + else if (is_realzero()) { + return true; + } + else if (mpz_cmp(mpq_numref(MP(quantity)), + mpq_denref(MP(quantity))) > 0) { + DEBUG("amount.is_zero", "Numerator is larger than the denominator"); + return false; + } + else { + DEBUG("amount.is_zero", "We have to print the number to check for zero"); + + std::ostringstream out; + stream_out_mpq(out, MP(quantity), commodity().precision()); + + string output = out.str(); + if (! output.empty()) { + for (const char * p = output.c_str(); *p; p++) + if (*p != '0' && *p != '.' && *p != '-') + return false; + } + return true; + } + } + return is_realzero(); +} + + +double amount_t::to_double() const +{ + if (! quantity) + throw_(amount_error, _("Cannot convert an uninitialized amount to a double")); + + mpfr_set_q(tempf, MP(quantity), GMP_RNDN); + return mpfr_get_d(tempf, GMP_RNDN); +} + +long amount_t::to_long() const +{ + if (! quantity) + throw_(amount_error, _("Cannot convert an uninitialized amount to a long")); + + mpfr_set_q(tempf, MP(quantity), GMP_RNDN); + return mpfr_get_si(tempf, GMP_RNDN); +} + +bool amount_t::fits_in_long() const +{ + mpfr_set_q(tempf, MP(quantity), GMP_RNDN); + return mpfr_fits_slong_p(tempf, GMP_RNDN); +} + +commodity_t& amount_t::commodity() const +{ + return (has_commodity() ? + *commodity_ : *commodity_pool_t::current_pool->null_commodity); +} + +bool amount_t::has_commodity() const +{ + return commodity_ && commodity_ != commodity_->pool().null_commodity; +} + +void amount_t::annotate(const annotation_t& details) +{ + commodity_t * this_base; + annotated_commodity_t * this_ann = NULL; + + if (! quantity) + throw_(amount_error, _("Cannot annotate the commodity of an uninitialized amount")); + else if (! has_commodity()) + return; // ignore attempt to annotate a "bare commodity + + if (commodity().has_annotation()) { + this_ann = &as_annotated_commodity(commodity()); + this_base = &this_ann->referent(); + } else { + this_base = &commodity(); + } + assert(this_base); + + DEBUG("amounts.commodities", "Annotating commodity for amount " + << *this << std::endl << details); + + if (commodity_t * ann_comm = + this_base->pool().find_or_create(*this_base, details)) + set_commodity(*ann_comm); +#ifdef ASSERTS_ON + else + assert(false); +#endif + + DEBUG("amounts.commodities", "Annotated amount is " << *this); +} + +bool amount_t::has_annotation() const +{ + if (! quantity) + throw_(amount_error, + _("Cannot determine if an uninitialized amount's commodity is annotated")); + + assert(! has_commodity() || ! commodity().has_annotation() || + as_annotated_commodity(commodity()).details); + return has_commodity() && commodity().has_annotation(); +} + +annotation_t& amount_t::annotation() +{ + if (! quantity) + throw_(amount_error, + _("Cannot return commodity annotation details of an uninitialized amount")); + + if (! commodity().has_annotation()) + throw_(amount_error, + _("Request for annotation details from an unannotated amount")); + + annotated_commodity_t& ann_comm(as_annotated_commodity(commodity())); + return ann_comm.details; +} + +amount_t amount_t::strip_annotations(const keep_details_t& what_to_keep) const +{ + if (! quantity) + throw_(amount_error, + _("Cannot strip commodity annotations from an uninitialized amount")); + + if (! what_to_keep.keep_all(commodity())) { + amount_t t(*this); + t.set_commodity(commodity().strip_annotations(what_to_keep)); + return t; + } + return *this; +} + + +namespace { + void parse_quantity(std::istream& in, string& value) + { + char buf[256]; + char c = peek_next_nonws(in); + READ_INTO(in, buf, 255, c, + std::isdigit(c) || c == '-' || c == '.' || c == ','); + + string::size_type len = std::strlen(buf); + while (len > 0 && ! std::isdigit(buf[len - 1])) { + buf[--len] = '\0'; + in.unget(); + } + + value = buf; + } +} + +bool amount_t::parse(std::istream& in, const parse_flags_t& flags) +{ + // The possible syntax for an amount is: + // + // [-]NUM[ ]SYM [@ AMOUNT] + // SYM[ ][-]NUM [@ AMOUNT] + + string symbol; + string quant; + annotation_t details; + bool negative = false; + + commodity_t::flags_t comm_flags = COMMODITY_STYLE_DEFAULTS; + + char c = peek_next_nonws(in); + if (c == '-') { + negative = true; + in.get(c); + c = peek_next_nonws(in); + } + + char n; + if (std::isdigit(c)) { + parse_quantity(in, quant); + + if (! in.eof() && ((n = static_cast<char>(in.peek())) != '\n')) { + if (std::isspace(n)) + comm_flags |= COMMODITY_STYLE_SEPARATED; + + commodity_t::parse_symbol(in, symbol); + + if (! symbol.empty()) + comm_flags |= COMMODITY_STYLE_SUFFIXED; + + if (! in.eof() && ((n = static_cast<char>(in.peek())) != '\n')) + details.parse(in); + } + } else { + commodity_t::parse_symbol(in, symbol); + + if (! in.eof() && ((n = static_cast<char>(in.peek())) != '\n')) { + if (std::isspace(static_cast<char>(in.peek()))) + comm_flags |= COMMODITY_STYLE_SEPARATED; + + parse_quantity(in, quant); + + if (! quant.empty() && ! in.eof() && + ((n = static_cast<char>(in.peek())) != '\n')) + details.parse(in); + } + } + + if (quant.empty()) { + if (flags.has_flags(PARSE_SOFT_FAIL)) + return false; + else + throw_(amount_error, _("No quantity specified for amount")); + } + + // Allocate memory for the amount's quantity value. We have to + // monitor the allocation in an auto_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; + + if (quantity) { + if (quantity->refc > 1) + _release(); + else + new_quantity.reset(quantity); + quantity = NULL; + } + + if (! new_quantity.get()) + new_quantity.reset(new bigint_t); + + // No one is holding a reference to this now. + new_quantity->refc--; + + // Create the commodity if has not already been seen, and update the + // precision if something greater was used for the quantity. + + bool newly_created = false; + + if (symbol.empty()) { + commodity_ = NULL; + } else { + commodity_ = commodity_pool_t::current_pool->find(symbol); + if (! commodity_) { + commodity_ = commodity_pool_t::current_pool->create(symbol); + newly_created = true; + } + 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 + // punctuation. + + precision_t decimal_offset = 0; + string::size_type string_index = quant.length(); + string::size_type last_comma = string::npos; + string::size_type last_period = string::npos; + + bool no_more_commas = false; + bool no_more_periods = false; + bool european_style = (commodity_t::european_by_default || + commodity().has_flags(COMMODITY_STYLE_EUROPEAN)); + + new_quantity->prec = 0; + + BOOST_REVERSE_FOREACH (const char& ch, quant) { + string_index--; + + if (ch == '.') { + if (no_more_periods) + throw_(amount_error, _("Too many periods in amount")); + + if (european_style) { + if (decimal_offset % 3 != 0) + throw_(amount_error, _("Incorrect use of european-style period")); + comm_flags |= COMMODITY_STYLE_THOUSANDS; + no_more_commas = true; + } else { + if (last_comma != string::npos) { + european_style = true; + if (decimal_offset % 3 != 0) + throw_(amount_error, _("Incorrect use of european-style period")); + } else { + no_more_periods = true; + new_quantity->prec = decimal_offset; + decimal_offset = 0; + } + } + + if (last_period == string::npos) + last_period = string_index; + } + else if (ch == ',') { + if (no_more_commas) + throw_(amount_error, _("Too many commas in amount")); + + if (european_style) { + if (last_period != string::npos) { + throw_(amount_error, _("Incorrect use of european-style comma")); + } else { + no_more_commas = true; + new_quantity->prec = decimal_offset; + decimal_offset = 0; + } + } else { + if (decimal_offset % 3 != 0) { + if (last_comma != string::npos || + last_period != string::npos) { + throw_(amount_error, _("Incorrect use of American-style comma")); + } else { + european_style = true; + no_more_commas = true; + new_quantity->prec = decimal_offset; + decimal_offset = 0; + } + } else { + comm_flags |= COMMODITY_STYLE_THOUSANDS; + no_more_periods = true; + } + } + + if (last_comma == string::npos) + last_comma = string_index; + } + else { + decimal_offset++; + } + } + + if (european_style) + comm_flags |= COMMODITY_STYLE_EUROPEAN; + + if (flags.has_flags(PARSE_NO_MIGRATE)) { + // Can't call set_keep_precision here, because it assumes that `quantity' + // is non-NULL. + new_quantity->add_flags(BIGINT_KEEP_PREC); + } + else if (commodity_) { + commodity().add_flags(comm_flags); + + if (new_quantity->prec > commodity().precision()) + commodity().set_precision(new_quantity->prec); + } + + // Now we have the final number. Remove commas and periods, if necessary. + + if (last_comma != string::npos || last_period != string::npos) { + string::size_type len = quant.length(); + scoped_array<char> buf(new char[len + 1]); + const char * p = quant.c_str(); + char * t = buf.get(); + + while (*p) { + if (*p == ',' || *p == '.') + p++; + *t++ = *p++; + } + *t = '\0'; + + mpq_set_str(MP(new_quantity.get()), buf.get(), 10); + mpz_ui_pow_ui(temp, 10, new_quantity->prec); + mpq_set_z(tempq, temp); + mpq_div(MP(new_quantity.get()), MP(new_quantity.get()), tempq); + + IF_DEBUG("amount.parse") { + char * buf = mpq_get_str(NULL, 10, MP(new_quantity.get())); + DEBUG("amount.parse", "Rational parsed = " << buf); + std::free(buf); + } + } else { + mpq_set_str(MP(new_quantity.get()), quant.c_str(), 10); + } + + if (negative) + mpq_neg(MP(new_quantity.get()), MP(new_quantity.get())); + + new_quantity->refc++; + quantity = new_quantity.release(); + + if (! flags.has_flags(PARSE_NO_REDUCE)) + in_place_reduce(); // will not throw an exception + + VERIFY(valid()); + + return true; +} + +void amount_t::parse_conversion(const string& larger_str, + const string& smaller_str) +{ + amount_t larger, smaller; + + larger.parse(larger_str, PARSE_NO_REDUCE); + smaller.parse(smaller_str, PARSE_NO_REDUCE); + + larger *= smaller.number(); + + if (larger.commodity()) { + larger.commodity().set_smaller(smaller); + larger.commodity().add_flags(smaller.commodity().flags() | + COMMODITY_NOMARKET); + } + if (smaller.commodity()) + smaller.commodity().set_larger(larger); +} + +void amount_t::print(std::ostream& _out) const +{ + VERIFY(valid()); + + if (! quantity) { + _out << "<null>"; + return; + } + + std::ostringstream out; + + commodity_t& comm(commodity()); + + if (! comm.has_flags(COMMODITY_STYLE_SUFFIXED)) { + comm.print(out); + if (comm.has_flags(COMMODITY_STYLE_SEPARATED)) + out << " "; + } + + stream_out_mpq(out, MP(quantity), display_precision(), + comm ? commodity().precision() : 0, comm); + + if (comm.has_flags(COMMODITY_STYLE_SUFFIXED)) { + if (comm.has_flags(COMMODITY_STYLE_SEPARATED)) + out << " "; + comm.print(out); + } + + // If there are any annotations associated with this commodity, output them + // now. + comm.write_annotations(out); + + // Things are output to a string first, so that if anyone has specified a + // width or fill for _out, it will be applied to the entire amount string, + // and not just the first part. + _out << out.str(); +} + +bool amount_t::valid() const +{ + if (quantity) { + if (! quantity->valid()) { + DEBUG("ledger.validate", "amount_t: ! quantity->valid()"); + return false; + } + + if (quantity->refc == 0) { + DEBUG("ledger.validate", "amount_t: quantity->refc == 0"); + return false; + } + } + else if (commodity_) { + DEBUG("ledger.validate", "amount_t: commodity_ != NULL"); + return false; + } + return true; +} + +void to_xml(std::ostream& out, const amount_t& amt, bool commodity_details) +{ + push_xml x(out, "amount"); + + if (amt.has_commodity()) + to_xml(out, amt.commodity(), commodity_details); + + { + push_xml y(out, "quantity"); + out << y.guard(amt.quantity_string()); + } +} + +#if defined(HAVE_BOOST_SERIALIZATION) + +template<class Archive> +void amount_t::serialize(Archive& ar, const unsigned int /* version */) +{ + ar & is_initialized; + ar & quantity; + ar & commodity_; +} + +#endif // HAVE_BOOST_SERIALIZATION + +} // namespace ledger + +#if defined(HAVE_BOOST_SERIALIZATION) +namespace boost { +namespace serialization { + +template <class Archive> +void serialize(Archive& ar, MP_INT& mpz, const unsigned int /* version */) +{ + ar & mpz._mp_alloc; + ar & mpz._mp_size; + ar & mpz._mp_d; +} + +template <class Archive> +void serialize(Archive& ar, MP_RAT& mpq, const unsigned int /* version */) +{ + ar & mpq._mp_num; + ar & mpq._mp_den; +} + +template <class Archive> +void serialize(Archive& ar, long unsigned int& integer, + const unsigned int /* version */) +{ + ar & make_binary_object(&integer, sizeof(long unsigned int)); +} + +} // namespace serialization +} // namespace boost + +BOOST_CLASS_EXPORT(ledger::annotated_commodity_t) + +template void boost::serialization::serialize(boost::archive::binary_iarchive&, + MP_INT&, const unsigned int); +template void boost::serialization::serialize(boost::archive::binary_oarchive&, + MP_INT&, const unsigned int); +template void boost::serialization::serialize(boost::archive::binary_iarchive&, + MP_RAT&, const unsigned int); +template void boost::serialization::serialize(boost::archive::binary_oarchive&, + MP_RAT&, const unsigned int); +template void boost::serialization::serialize(boost::archive::binary_iarchive&, + long unsigned int&, + const unsigned int); +template void boost::serialization::serialize(boost::archive::binary_oarchive&, + long unsigned int&, + const unsigned int); + +template void ledger::amount_t::serialize(boost::archive::binary_iarchive&, + const unsigned int); +template void ledger::amount_t::serialize(boost::archive::binary_oarchive&, + const unsigned int); + +#endif // HAVE_BOOST_SERIALIZATION diff --git a/src/amount.h b/src/amount.h new file mode 100644 index 00000000..a37efdb8 --- /dev/null +++ b/src/amount.h @@ -0,0 +1,761 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup math Mathematical objects + */ + +/** + * @file amount.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Basic type for handling commoditized math: amount_t + * + * An amount is the most basic numerical type in Ledger, and relies on + * commodity.h to represent commoditized amounts, which allows Ledger to + * handle mathematical expressions involving disparate commodities. + * + * Amounts can be of virtually infinite size and precision. When + * division or multiplication is performed, the precision is + * automatically expanded to include as many extra digits as necessary + * to avoid losing information. + */ +#ifndef _AMOUNT_H +#define _AMOUNT_H + +#include "utils.h" +#include "times.h" +#include "flags.h" + +namespace ledger { + +class commodity_t; +class annotation_t; +class keep_details_t; +class commodity_pool_t; + +DECLARE_EXCEPTION(amount_error, std::runtime_error); + +enum parse_flags_enum_t { + PARSE_DEFAULT = 0x00, + PARSE_PARTIAL = 0x01, + PARSE_SINGLE = 0x02, + PARSE_NO_MIGRATE = 0x04, + PARSE_NO_REDUCE = 0x08, + PARSE_NO_ASSIGN = 0x10, + PARSE_NO_DATES = 0x20, + PARSE_OP_CONTEXT = 0x40, + PARSE_SOFT_FAIL = 0x80 +}; + +typedef basic_flags_t<parse_flags_enum_t, uint_least8_t> parse_flags_t; + +/** + * @brief Encapsulate infinite-precision commoditized amounts + * + * Used to represent commoditized infinite-precision numbers, and + * uncommoditized, plain numbers. In the commoditized case, commodities + * keep track of how they are used, and are always displayed back to the + * user after the same fashion. For uncommoditized numbers, no display + * truncation is ever done. In both cases, internal precision is always + * kept to an excessive degree. + */ +class amount_t + : public ordered_field_operators<amount_t, + ordered_field_operators<amount_t, double, + ordered_field_operators<amount_t, unsigned long, + ordered_field_operators<amount_t, long> > > > +{ +public: + /** Ready the amount subsystem for use. + @note Normally called by session_t::initialize(). */ + static void initialize(); + /** Shutdown the amount subsystem and free all resources. + @note Normally called by session_t::shutdown(). */ + static void shutdown(); + + static bool is_initialized; + + /** The amount's decimal precision. */ + typedef uint_least16_t precision_t; + + /** Number of places of precision by which values are extended to + avoid losing precision during division and multiplication. */ + static const std::size_t extend_by_digits = 6U; + + /** If amounts should be streamed using to_fullstring() rather than + to_string(), so that complete precision is always displayed no matter + what the precision of an individual commodity may be. */ + static bool stream_fullstrings; + +protected: + void _copy(const amount_t& amt); + void _dup(); + void _clear(); + void _release(); + + struct bigint_t; + + bigint_t * quantity; + commodity_t * commodity_; + +public: + /** @name Constructors + @{ */ + + /** Creates a value for which is_null() is true, and which has no + value or commodity. If used in a value expression it evaluates to + zero, and its commodity equals \c commodity_t::null_commodity. */ + amount_t() : quantity(NULL), commodity_(NULL) { + TRACE_CTOR(amount_t, ""); + } + + /** Convert a double to an amount. As much precision as possible is + decoded from the binary floating point number. */ + amount_t(const double val); + + /** Convert an unsigned long to an amount. It's precision is zero. */ + amount_t(const unsigned long val); + + /** Convert a long to an amount. It's precision is zero, and the sign + is preserved. */ + amount_t(const long val); + + /** Parse a string as an (optionally commoditized) amount. If no + commodity is present, the resulting commodity is \c + commodity_t::null_commodity. The number may be of infinite + precision. */ + explicit amount_t(const string& val) : quantity(NULL) { + TRACE_CTOR(amount_t, "const string&"); + parse(val); + } + /** 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); + } + + /*@}*/ + + /** Create an amount whose display precision is never truncated, even + if the amount uses a commodity (which normally causes "round on + streaming" to occur). This function is mostly used by debugging + code and unit tests. This is the proper way to specify \c + $100.005, where display of the extra digit precision is required. + If a regular constructor were used, the amount would stream as \c + $100.01, even though its internal value equals \c $100.005. */ + static amount_t exact(const string& value); + + /** Release the reference count held for the underlying \c + amount_t::bigint_t object. */ + ~amount_t() { + TRACE_DTOR(amount_t); + if (quantity) + _release(); + } + + /** @name Assignment and copy + @{*/ + + /** Copy an amount object. Copies are very efficient, using a + copy-on-write model. Until the copy is changed, it refers to the + same memory used by the original via reference counting. The \c + amount_t::bigint_t class in amount.cc maintains the reference. */ + amount_t(const amount_t& amt) : quantity(NULL) { + TRACE_CTOR(amount_t, "copy"); + if (amt.quantity) + _copy(amt); + else + commodity_ = NULL; + } + /** 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); + } + /** Assign an amount object. This is like copying if the amount was + null beforehand, otherwise the previous value's reference is must + be freed. */ + amount_t& operator=(const amount_t& amt); + + amount_t& operator=(const double val) { + return *this = amount_t(val); + } + amount_t& operator=(const unsigned long val) { + return *this = amount_t(val); + } + amount_t& operator=(const long val) { + return *this = amount_t(val); + } + + /* Assign a string to an amount. This causes the contents of the + string to be parsed, look for a commoditized or uncommoditized + amount specifier. */ + amount_t& operator=(const string& str) { + return *this = amount_t(str); + } + amount_t& operator=(const char * str) { + assert(str); + return *this = amount_t(str); + } + + /*@}*/ + + /** @name Comparison + @{ */ + + /** Compare two amounts, returning a number less than zero if \p amt + is greater, exactly zero if they are equal, and greater than zero + if \p amt is less. This method is used to implement all of the + other comparison methods.*/ + int compare(const amount_t& amt) const; + + /** Test two amounts for equality. First the commodity pointers are + quickly tested, then the multi-precision values themselves must be + compared. */ + bool operator==(const amount_t& amt) const; + + template <typename T> + bool operator==(const T& val) const { + return compare(val) == 0; + } + template <typename T> + bool operator<(const T& amt) const { + return compare(amt) < 0; + } + template <typename T> + bool operator>(const T& amt) const { + return compare(amt) > 0; + } + + /*@}*/ + + /** @name Binary arithmetic + */ + /*@{*/ + + amount_t& operator+=(const amount_t& amt); + amount_t& operator-=(const amount_t& amt); + amount_t& operator*=(const amount_t& amt); + + /** Divide two amounts while extending the precision to preserve the + accuracy of the result. For example, if \c 10 is divided by \c 3, + the result ends up having a precision of \link + amount_t::extend_by_digits \endlink place to avoid losing internal + resolution. */ + amount_t& operator/=(const amount_t& amt); + + /*@}*/ + + /** @name Unary arithmetic + @{ */ + + /** Return an amount's internal precision. To find the precision it + should be displayed at -- assuming it was not created using + amount_t::exact() -- use the following expression instead: + @code + amount.commodity().precision() + @endcode */ + precision_t precision() const; + bool keep_precision() const; + void set_keep_precision(const bool keep = true) const; + precision_t display_precision() const; + + /** Returns the negated value of an amount. + @see operator-() + */ + amount_t negated() const { + amount_t temp(*this); + temp.in_place_negate(); + return temp; + } + void in_place_negate(); + + amount_t operator-() const { + return negated(); + } + + /** Returns the absolute value of an amount. Equivalent to: + @code + (x < * 0) ? - x : x + @endcode + */ + amount_t abs() const { + if (sign() < 0) + return negated(); + return *this; + } + + amount_t inverted() const; + + /** Yields an amount whose display precision when output is truncated + to the display precision of its commodity. This is normally the + default state of an amount, but if one has become unrounded, this + sets the "keep precision" state back to false. + @see set_keep_precision */ + amount_t rounded() const { + amount_t temp(*this); + temp.in_place_round(); + return temp; + } + void in_place_round(); + + /** Yields an amount which has lost all of its extra precision, beyond what + the display precision of the commodity would have printed. */ + amount_t truncated() const { + amount_t temp(*this); + temp.in_place_truncate(); + return temp; + } + void in_place_truncate() { + *this = amount_t(to_string()); + } + + /** Yields an amount which has lost all of its extra precision, beyond what + the display precision of the commodity would have printed. */ + amount_t floored() const { + amount_t temp(*this); + temp.in_place_floor(); + return temp; + } + void in_place_floor(); + + /** Yields an amount whose display precision is never truncated, even + though its commodity normally displays only rounded values. */ + amount_t unrounded() const { + amount_t temp(*this); + temp.in_place_unround(); + return temp; + } + void in_place_unround(); + + /** reduces a value to its most basic commodity form, for amounts that + utilize "scaling commodities". For example, an amount of \c 1h + after reduction will be \c 3600s. + */ + amount_t reduced() const { + amount_t temp(*this); + temp.in_place_reduce(); + return temp; + } + void in_place_reduce(); + + /** unreduce(), if used with a "scaling commodity", yields the most + compact form greater than one. That is, \c 3599s will unreduce to + \c 59.98m, while \c 3601 unreduces to \c 1h. + */ + amount_t unreduced() const { + amount_t temp(*this); + temp.in_place_unreduce(); + return temp; + } + void in_place_unreduce(); + + /** Returns the historical value for an amount -- the default moment + returns the most recently known price -- based on the price history + for the given commodity (or determined automatically, if none is + provided). For example, if the amount were <tt>10 AAPL</tt>, and + on Apr 10, 2000 each share of \c AAPL was worth \c $10, then + calling value() for that moment in time would yield the amount \c + $100.00. + */ + optional<amount_t> + value(const bool primary_only = true, + const optional<datetime_t>& moment = none, + const optional<commodity_t&>& in_terms_of = none) const; + + amount_t price() const; + + /*@}*/ + + /** @name Truth tests + */ + /*@{*/ + + /** Truth tests. An amount may be truth test in several ways: + + sign() returns an integer less than, greater than, or equal to + zero depending on whether the amount is negative, zero, or + greater than zero. Note that this function tests the actual + value of the amount -- using its internal precision -- and not + the display value. To test its display value, use: + `round().sign()'. + + is_nonzero(), or operator bool, returns true if an amount's + display value is not zero. + + is_zero() returns true if an amount's display value is zero. + Thus, $0.0001 is considered zero if the current display precision + for dollars is two decimal places. + + is_realzero() returns true if an amount's actual value is zero. + Thus, $0.0001 is never considered realzero. + + is_null() returns true if an amount has no value and no + commodity. This only occurs if an uninitialized amount has never + been assigned a value. + */ + int sign() const; + + operator bool() const { + return is_nonzero(); + } + bool is_nonzero() const { + return ! is_zero(); + } + + bool is_zero() const; + bool is_realzero() const { + return sign() == 0; + } + + bool is_null() const { + if (! quantity) { + assert(! commodity_); + return true; + } + return false; + } + + /*@}*/ + + /** @name Conversion + */ + /*@{*/ + + /** Conversion methods. An amount may be converted to the same types + it can be constructed from -- with the exception of unsigned + long. Implicit conversions are not allowed in C++ (though they + are in Python), rather the following conversion methods must be + called explicitly: + + to_double([bool]) returns an amount as a double. If the optional + boolean argument is true (the default), an exception is thrown if + the conversion would lose information. + + to_long([bool]) returns an amount as a long integer. If the + optional boolean argument is true (the default), an exception is + thrown if the conversion would lose information. + + fits_in_long() returns true if to_long() would not lose + precision. + + to_string() returns an amount'ss "display value" as a string -- + after rounding the value according to the commodity's default + precision. It is equivalent to: `round().to_fullstring()'. + + to_fullstring() returns an amount's "internal value" as a string, + without any rounding. + + quantity_string() returns an amount's "display value", but + without any commodity. Note that this is different from + `number().to_string()', because in that case the commodity has + been stripped and the full, internal precision of the amount + would be displayed. + */ + double to_double() const; + long to_long() const; + bool fits_in_long() const; + + operator string() const { + return to_string(); + } + string to_string() const; + string to_fullstring() const; + string quantity_string() const; + + /*@}*/ + + /** @name Commodity methods + */ + /*@{*/ + + /** The following methods relate to an + amount's commodity: + + commodity() returns an amount's commodity. If the amount has no + commodity, the value returned is `current_pool->null_commodity'. + + has_commodity() returns true if the amount has a commodity. + + set_commodity(commodity_t) sets an amount's commodity to the + given value. Note that this merely sets the current amount to + that commodity, it does not "observe" the amount for possible + changes in the maximum display precision of the commodity, the + way that `parse' does. + + clear_commodity() sets an amount's commodity to null, such that + has_commodity() afterwards returns false. + + number() returns a commodity-less version of an amount. This is + useful for accessing just the numeric portion of an amount. + */ + commodity_t& commodity() const; + + bool has_commodity() const; + void set_commodity(commodity_t& comm) { + if (! quantity) + *this = 0L; + commodity_ = &comm; + } + void clear_commodity() { + commodity_ = NULL; + } + + amount_t number() const { + if (! has_commodity()) + return *this; + + amount_t temp(*this); + temp.clear_commodity(); + return temp; + } + + /*@}*/ + + /** @name Commodity annotations + */ + /*@{*/ + + /** An amount's commodity may be annotated with special details, such as the + price it was purchased for, when it was acquired, or an arbitrary note, + identifying perhaps the lot number of an item. + + annotate_commodity(amount_t price, [datetime_t date, string tag]) + sets the annotations for the current amount's commodity. Only + the price argument is required, although it can be passed as + `none' if no price is desired. + + commodity_annotated() returns true if an amount's commodity has + any annotation details associated with it. + + annotation_details() returns all of the details of an annotated + commodity's annotations. The structure returns will evaluate as + boolean false if there are no details. + + strip_annotations() returns an amount whose commodity's annotations have + been stripped. + */ + void annotate(const annotation_t& details); + bool has_annotation() const; + + annotation_t& annotation(); + const annotation_t& annotation() const { + return const_cast<amount_t&>(*this).annotation(); + } + + /** If the lot price is considered whenever working with commoditized + values. + + Let's say a user adds two values of the following form: + @code + 10 AAPL + 10 AAPL {$20} + @endcode + + This expression adds ten shares of Apple stock with another ten + shares that were purchased for \c $20 a share. If \c keep_price + is false, the result of this expression is an amount equal to + <tt>20 AAPL</tt>. If \c keep_price is \c true the expression + yields an exception for adding amounts with different commodities. + In that case, a \link balance_t \endlink object must be used to + store the combined sum. */ + amount_t strip_annotations(const keep_details_t& what_to_keep) const; + + /*@}*/ + + /** @name Parsing + */ + /*@{*/ + + /** The `flags' argument of both parsing may be one or more of the + following: + + PARSE_NO_MIGRATE means to not pay attention to the way an + amount is used. Ordinarily, if an amount were $100.001, for + example, it would cause the default display precision for $ to be + "widened" to three decimal places. If PARSE_NO_MIGRATE is + used, the commodity's default display precision is not changed. + + PARSE_NO_REDUCE means not to call in_place_reduce() on the + resulting amount after it is parsed. + + These parsing methods observe the amounts they parse (unless + PARSE_NO_MIGRATE is true), and set the display details of + the corresponding commodity accordingly. This way, amounts do + not require commodities to be pre-defined in any way, but merely + displays them back to the user in the same fashion as it saw them + used. + + There is also a static convenience method called + `parse_conversion' which can be used to define a relationship + between scaling commodity values. For example, Ledger uses it to + define the relationships among various time values: + + @code + amount_t::parse_conversion("1.0m", "60s"); // a minute is 60 seconds + amount_t::parse_conversion("1.0h", "60m"); // an hour is 60 minutes + @endcode + + The method parse() is used to parse an amount from an input stream + or a string. A global operator>>() is also defined which simply + calls parse on the input stream. The parse() method has two forms: + + parse(istream, flags_t) parses an amount from the given input + stream. + + parse(string, flags_t) parses an amount from the given string. + + parse(string, flags_t) also parses an amount from a string. + */ + bool parse(std::istream& in, + const parse_flags_t& flags = PARSE_DEFAULT); + bool parse(const string& str, + const parse_flags_t& flags = PARSE_DEFAULT) { + std::istringstream stream(str); + bool result = parse(stream, flags); + return result; + } + + static void parse_conversion(const string& larger_str, + const string& smaller_str); + + /*@}*/ + + /** @name Printing + */ + /*@{*/ + + /** An amount may be output to a stream using the `print' method. There is + also a global operator<< defined which simply calls print for an amount + on the given stream. There is one form of the print method, which takes + one required argument and two arguments with default values: + + print(ostream, bool omit_commodity = false, bool full_precision = false) + prints an amounts to the given output stream, using its commodity's + default display characteristics. If `omit_commodity' is true, the + commodity will not be displayed, only the amount (although the + commodity's display precision is still used). If `full_precision' is + true, the full internal precision of the amount is displayed, regardless + of its commodity's display precision. + */ + void print(std::ostream& out) const; + + /*@}*/ + + /** @name Debugging + */ + /*@{*/ + + /** There are two methods defined to help with debugging: + + dump(ostream) dumps an amount to an output stream. There is + little different from print(), it simply surrounds the display + value with a marker, for example "AMOUNT($1.00)". This code is + used by other dumping code elsewhere in Ledger. + + valid() returns true if an amount is valid. This ensures that if + an amount has a commodity, it has a valid value pointer, for + example, even if that pointer simply points to a zero value. + */ + void dump(std::ostream& out) const { + out << "AMOUNT("; + print(out); + out << ")"; + } + + bool valid() const; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */); +#endif // HAVE_BOOST_SERIALIZATION + + /*@}*/ +}; + +inline amount_t amount_t::exact(const string& value) { + amount_t temp; + temp.parse(value, PARSE_NO_MIGRATE); + return temp; +} + +inline string amount_t::to_string() const { + std::ostringstream bufstream; + print(bufstream); + return bufstream.str(); +} + +inline string amount_t::to_fullstring() const { + std::ostringstream bufstream; + unrounded().print(bufstream); + return bufstream.str(); +} + +inline string amount_t::quantity_string() const { + std::ostringstream bufstream; + number().print(bufstream); + return bufstream.str(); +} + +inline std::ostream& operator<<(std::ostream& out, const amount_t& amt) { + if (amount_t::stream_fullstrings) + amt.unrounded().print(out); + else + amt.print(out); + return out; +} +inline std::istream& operator>>(std::istream& in, amount_t& amt) { + amt.parse(in); + return in; +} + +void to_xml(std::ostream& out, const amount_t& amt, + bool commodity_details = false); + +} // namespace ledger + +#endif // _AMOUNT_H diff --git a/src/annotate.cc b/src/annotate.cc new file mode 100644 index 00000000..146a7afd --- /dev/null +++ b/src/annotate.cc @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2003-2009, 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 "amount.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +void annotation_t::parse(std::istream& in) +{ + do { + istream_pos_type pos = in.tellg(); + + char buf[256]; + char c = peek_next_nonws(in); + if (c == '{') { + if (price) + throw_(amount_error, _("Commodity specifies more than one price")); + + in.get(c); + c = peek_next_nonws(in); + if (c == '=') { + in.get(c); + add_flags(ANNOTATION_PRICE_FIXATED); + } + + READ_INTO(in, buf, 255, c, c != '}'); + if (c == '}') + in.get(c); + else + throw_(amount_error, _("Commodity price lacks closing brace")); + + amount_t temp; + temp.parse(buf, PARSE_NO_MIGRATE); + + DEBUG("commodity.annotations", "Parsed annotation price: " << temp); + + // Since this price will maintain its own precision, make sure + // it is at least as large as the base commodity, since the user + // may have only specified {$1} or something similar. + + if (temp.has_commodity() && + temp.precision() > temp.commodity().precision()) + temp = temp.rounded(); // no need to retain individual precision + + price = temp; + } + else if (c == '[') { + if (date) + throw_(amount_error, _("Commodity specifies more than one date")); + + in.get(c); + READ_INTO(in, buf, 255, c, c != ']'); + if (c == ']') + in.get(c); + else + throw_(amount_error, _("Commodity date lacks closing bracket")); + + date = parse_date(buf); + } + else if (c == '(') { + 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")); + + tag = buf; + } + else { + in.clear(); + in.seekg(pos, std::ios::beg); + break; + } + } while (true); + +#if defined(DEBUG_ON) + if (SHOW_DEBUG("amounts.commodities") && *this) { + DEBUG("amounts.commodities", + "Parsed commodity annotations: " << std::endl << *this); + } +#endif +} + +void annotation_t::print(std::ostream& out, bool keep_base) const +{ + if (price) + out << " {" + << (has_flags(ANNOTATION_PRICE_FIXATED) ? "=" : "") + << (keep_base ? *price : price->unreduced()).rounded() + << '}'; + + if (date) + out << " [" << format_date(*date, FMT_WRITTEN) << ']'; + + if (tag) + out << " (" << *tag << ')'; +} + +bool keep_details_t::keep_all(const commodity_t& comm) const +{ + return (! comm.has_annotation() || + (keep_price && keep_date && keep_tag && ! only_actuals)); +} + +bool keep_details_t::keep_any(const commodity_t& comm) const +{ + return comm.has_annotation() && (keep_price || keep_date || keep_tag); +} + +bool annotated_commodity_t::operator==(const commodity_t& comm) const +{ + // If the base commodities don't match, the game's up. + if (base != comm.base) + return false; + + assert(annotated); + if (! comm.annotated) + return false; + + if (details != as_annotated_commodity(comm).details) + return false; + + return true; +} + +commodity_t& +annotated_commodity_t::strip_annotations(const keep_details_t& what_to_keep) +{ + DEBUG("commodity.annotated.strip", + "Reducing commodity " << *this << std::endl + << " keep price " << what_to_keep.keep_price << " " + << " keep date " << what_to_keep.keep_date << " " + << " keep tag " << what_to_keep.keep_tag); + + commodity_t * new_comm; + + bool keep_price = (what_to_keep.keep_price && + (! what_to_keep.only_actuals || + ! details.has_flags(ANNOTATION_PRICE_CALCULATED))); + bool keep_date = (what_to_keep.keep_date && + (! what_to_keep.only_actuals || + ! details.has_flags(ANNOTATION_DATE_CALCULATED))); + bool keep_tag = (what_to_keep.keep_tag && + (! what_to_keep.only_actuals || + ! details.has_flags(ANNOTATION_TAG_CALCULATED))); + + if ((keep_price && details.price) || + (keep_date && details.date) || + (keep_tag && details.tag)) + { + new_comm = pool().find_or_create + (referent(), annotation_t(keep_price ? details.price : none, + keep_date ? details.date : none, + keep_tag ? details.tag : none)); + } else { + new_comm = pool().find_or_create(base_symbol()); + } + + assert(new_comm); + return *new_comm; +} + +void annotated_commodity_t::write_annotations(std::ostream& out) const +{ + details.print(out, pool().keep_base); +} + +} // namespace ledger diff --git a/src/annotate.h b/src/annotate.h new file mode 100644 index 00000000..38ebaeae --- /dev/null +++ b/src/annotate.h @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup math + */ + +/** + * @file annotate.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Types for annotating commodities + * + * Long. + */ +#ifndef _ANNOTATE_H +#define _ANNOTATE_H + +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 + + 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"); + } + annotation_t(const annotation_t& other) + : supports_flags<>(other.flags()), + price(other.price), date(other.date), tag(other.tag) { + TRACE_CTOR(annotation_t, "copy"); + } + ~annotation_t() { + TRACE_DTOR(annotation_t); + } + + operator bool() const { + return price || date || tag; + } + + bool operator==(const annotation_t& rhs) const { + return (price == rhs.price && + date == rhs.date && + tag == rhs.tag); + } + + void parse(std::istream& in); + + void print(std::ostream& out, bool keep_base = false) const; + + bool valid() const { + assert(*this); + return true; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<supports_flags<> >(*this); + ar & price; + ar & date; + ar & tag; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline void to_xml(std::ostream& out, const annotation_t& details) +{ + push_xml x(out, "annotation"); + + if (details.price) + { + push_xml y(out, "price"); + to_xml(out, *details.price); + } + + if (details.date) + { + push_xml y(out, "date"); + to_xml(out, *details.date, false); + } + + if (details.tag) + { + push_xml y(out, "tag"); + out << y.guard(*details.tag); + } +} + +struct keep_details_t +{ + bool keep_price; + bool keep_date; + bool keep_tag; + bool only_actuals; + + explicit keep_details_t(bool _keep_price = false, + bool _keep_date = false, + bool _keep_tag = false, + bool _only_actuals = false) + : keep_price(_keep_price), + keep_date(_keep_date), + keep_tag(_keep_tag), + only_actuals(_only_actuals) + { + TRACE_CTOR(keep_details_t, "bool, bool, bool, bool"); + } + keep_details_t(const keep_details_t& other) + : keep_price(other.keep_price), keep_date(other.keep_date), + keep_tag(other.keep_tag), only_actuals(other.only_actuals) { + TRACE_CTOR(keep_details_t, "copy"); + } + ~keep_details_t() throw() { + TRACE_DTOR(keep_details_t); + } + + bool keep_all() const { + return keep_price && keep_date && keep_tag && ! only_actuals; + } + bool keep_all(const commodity_t& comm) const; + + bool keep_any() const { + return keep_price || keep_date || keep_tag; + } + bool keep_any(const commodity_t& comm) const; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & keep_price; + ar & keep_date; + ar & keep_tag; + ar & only_actuals; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline std::ostream& operator<<(std::ostream& out, + const annotation_t& details) { + details.print(out); + return out; +} + +class annotated_commodity_t + : public commodity_t, + public equality_comparable<annotated_commodity_t, + equality_comparable2<annotated_commodity_t, commodity_t, + noncopyable> > +{ +protected: + friend class commodity_pool_t; + + commodity_t * ptr; + + explicit annotated_commodity_t(commodity_t * _ptr, + const annotation_t& _details) + : commodity_t(_ptr->parent_, _ptr->base), ptr(_ptr), details(_details) { + TRACE_CTOR(annotated_commodity_t, "commodity_t *, annotation_t"); + annotated = true; + } + +public: + annotation_t details; + + virtual ~annotated_commodity_t() { + TRACE_DTOR(annotated_commodity_t); + } + + virtual bool operator==(const commodity_t& comm) const; + virtual bool operator==(const annotated_commodity_t& comm) const { + return *this == static_cast<const commodity_t&>(comm); + } + + virtual commodity_t& referent() { + return *ptr; + } + virtual const commodity_t& referent() const { + return *ptr; + } + + virtual commodity_t& strip_annotations(const keep_details_t& what_to_keep); + virtual void write_annotations(std::ostream& out) const; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + explicit annotated_commodity_t() : ptr(NULL) { + TRACE_CTOR(annotated_commodity_t, ""); + } + + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<commodity_t>(*this); + ar & ptr; + ar & details; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline annotated_commodity_t& +as_annotated_commodity(commodity_t& commodity) { + return downcast<annotated_commodity_t>(commodity); +} +inline const annotated_commodity_t& +as_annotated_commodity(const commodity_t& commodity) { + return downcast<const annotated_commodity_t>(commodity); +} + +} // namespace ledger + +#endif // _ANNOTATE_H diff --git a/src/archive.cc b/src/archive.cc new file mode 100644 index 00000000..7306f8d3 --- /dev/null +++ b/src/archive.cc @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2003-2009, 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> + +#if defined(HAVE_BOOST_SERIALIZATION) + +#include "archive.h" +#include "amount.h" +#include "commodity.h" +#include "pool.h" +#include "scope.h" +#include "account.h" +#include "post.h" +#include "xact.h" + +#define LEDGER_MAGIC 0x4c454447 +#define ARCHIVE_VERSION 0x03000006 + +//BOOST_IS_ABSTRACT(ledger::scope_t) +BOOST_CLASS_EXPORT(ledger::scope_t) +BOOST_CLASS_EXPORT(ledger::child_scope_t) +BOOST_CLASS_EXPORT(ledger::symbol_scope_t) +BOOST_CLASS_EXPORT(ledger::call_scope_t) +BOOST_CLASS_EXPORT(ledger::account_t) +BOOST_CLASS_EXPORT(ledger::item_t) +BOOST_CLASS_EXPORT(ledger::post_t) +BOOST_CLASS_EXPORT(ledger::xact_base_t) +BOOST_CLASS_EXPORT(ledger::xact_t) +BOOST_CLASS_EXPORT(ledger::auto_xact_t) +BOOST_CLASS_EXPORT(ledger::period_xact_t) + +template void ledger::journal_t::serialize(boost::archive::binary_oarchive&, + const unsigned int); +template void ledger::journal_t::serialize(boost::archive::binary_iarchive&, + const unsigned int); +namespace ledger { + +namespace { + bool read_header_bits(std::istream& in) { + uint32_t bytes; + + assert(sizeof(uint32_t) == 4); + in.read(reinterpret_cast<char *>(&bytes), sizeof(uint32_t)); + if (bytes != LEDGER_MAGIC) { + DEBUG("archive.journal", "Magic bytes not present"); + return false; + } + + in.read(reinterpret_cast<char *>(&bytes), sizeof(uint32_t)); + if (bytes != ARCHIVE_VERSION) { + DEBUG("archive.journal", "Archive version mismatch"); + return false; + } + + return true; + } + + void write_header_bits(std::ostream& out) { + uint32_t bytes; + + assert(sizeof(uint32_t) == 4); + bytes = LEDGER_MAGIC; + out.write(reinterpret_cast<char *>(&bytes), sizeof(uint32_t)); + + bytes = ARCHIVE_VERSION; + out.write(reinterpret_cast<char *>(&bytes), sizeof(uint32_t)); + } +} + +bool archive_t::read_header() +{ + uintmax_t size = file_size(file); + if (size < 8) + return false; + + // Open the stream, read the version number and the list of sources + ifstream stream(file, std::ios::binary); + if (! read_header_bits(stream)) + return false; + + boost::archive::binary_iarchive iarchive(stream); + + DEBUG("archive.journal", "Reading header from archive"); + iarchive >> *this; + + DEBUG("archive.journal", + "Version number: " << std::hex << ARCHIVE_VERSION << std::dec); + DEBUG("archive.journal", "Number of sources: " << sources.size()); + +#if defined(DEBUG_ON) + foreach (const journal_t::fileinfo_t& i, sources) + DEBUG("archive.journal", "Loaded source: " << *i.filename); +#endif + + return true; +} + +bool archive_t::should_load(const std::list<path>& data_files) +{ + std::size_t found = 0; + + DEBUG("archive.journal", "Should the archive be loaded?"); + + if (! exists(file)) { + DEBUG("archive.journal", "No, it does not exist"); + return false; + } + + if (! read_header()) { + DEBUG("archive.journal", "No, header failed to read"); + return false; + } + + if (data_files.empty()) { + DEBUG("archive.journal", "No, there were no data files!"); + return false; + } + + if (sources.empty()) { + DEBUG("archive.journal", "No, there were no sources!"); + return false; + } + + if (data_files.size() != sources.size()) { + DEBUG("archive.journal", "No, number of sources doesn't match: " + << data_files.size() << " != " << sources.size()); + return false; + } + + foreach (const path& p, data_files) { + DEBUG("archive.journal", "Scanning for data file: " << p); + + if (! exists(p)) { + DEBUG("archive.journal", "No, an input source no longer exists: " << p); + return false; + } + + foreach (const journal_t::fileinfo_t& i, sources) { + assert(! i.from_stream); + assert(i.filename); + + DEBUG("archive.journal", "Comparing against source file: " << *i.filename); + + if (*i.filename == p) { + if (! exists(*i.filename)) { + DEBUG("archive.journal", + "No, a referent source no longer exists: " << *i.filename); + return false; + } + + if (i.modtime != posix_time::from_time_t(last_write_time(p))) { + DEBUG("archive.journal", "No, a source's modtime has changed: " << p); + return false; + } + + if (i.size != file_size(p)) { + DEBUG("archive.journal", "No, a source's size has changed: " << p); + return false; + } + + found++; + } + } + } + + if (found != data_files.size()) { + DEBUG("archive.journal", "No, not every source's name matched"); + return false; + } + + DEBUG("archive.journal", "Yes, it should be loaded!"); + return true; +} + +bool archive_t::should_save(journal_t& journal) +{ + std::list<path> data_files; + + DEBUG("archive.journal", "Should the archive be saved?"); + + if (journal.was_loaded) { + DEBUG("archive.journal", "No, it's one we loaded before"); + return false; + } + + if (journal.sources.empty()) { + DEBUG("archive.journal", "No, there were no sources!"); + return false; + } + + foreach (const journal_t::fileinfo_t& i, journal.sources) { + if (i.from_stream) { + DEBUG("archive.journal", "No, one source was from a stream"); + return false; + } + + if (! exists(*i.filename)) { + DEBUG("archive.journal", + "No, a source no longer exists: " << *i.filename); + return false; + } + + data_files.push_back(*i.filename); + } + + if (should_load(data_files)) { + DEBUG("archive.journal", "No, because it's still loadable"); + return false; + } + + DEBUG("archive.journal", "Yes, it should be saved!"); + return true; +} + +void archive_t::save(journal_t& journal) +{ + INFO_START(archive, "Saved journal file cache"); + + ofstream stream(file, std::ios::binary); + + write_header_bits(stream); + sources = journal.sources; + +#if defined(DEBUG_ON) + foreach (const journal_t::fileinfo_t& i, sources) + DEBUG("archive.journal", "Saving source: " << *i.filename); +#endif + + boost::archive::binary_oarchive oa(stream); + + DEBUG("archive.journal", "Creating archive with version " + << std::hex << ARCHIVE_VERSION << std::dec); + oa << *this; + + DEBUG("archive.journal", + "Archiving journal with " << sources.size() << " sources"); + oa << journal; + + INFO_FINISH(archive); +} + +bool archive_t::load(journal_t& journal) +{ + INFO_START(archive, "Read cached journal file"); + + ifstream stream(file, std::ios::binary); + if (! read_header_bits(stream)) + return false; + + boost::archive::binary_iarchive iarchive(stream); + + // Skip past the archive header, it was already read in before + archive_t temp; + iarchive >> temp; + + iarchive >> journal; + journal.was_loaded = true; + + INFO_FINISH(archive); + + return true; +} + +} // namespace ledger + +#endif // HAVE_BOOST_SERIALIZATION diff --git a/src/archive.h b/src/archive.h new file mode 100644 index 00000000..03fc970a --- /dev/null +++ b/src/archive.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup report Reporting + */ + +/** + * @file archive.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _ARCHIVE_H +#define _ARCHIVE_H + +#include "journal.h" + +namespace ledger { + +class archive_t +{ + path file; + + std::list<journal_t::fileinfo_t> sources; + +public: + archive_t() { + TRACE_CTOR(archive_t, ""); + } + archive_t(const path& _file) : file(_file) { + TRACE_CTOR(archive_t, "const path&"); + } + archive_t(const archive_t& ar) : file(ar.file) { + TRACE_CTOR(archive_t, "copy"); + } + ~archive_t() { + TRACE_DTOR(archive_t); + } + + bool read_header(); + + bool should_load(const std::list<path>& data_files); + bool should_save(journal_t& journal); + + void save(journal_t& journal); + bool load(journal_t& journal); + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & sources; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +} // namespace ledger + +#endif // _ARCHIVE_H diff --git a/src/balance.cc b/src/balance.cc new file mode 100644 index 00000000..4ff51ffc --- /dev/null +++ b/src/balance.cc @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2003-2009, 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 "balance.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" +#include "unistring.h" // for justify() + +namespace ledger { + +balance_t::balance_t(const double val) +{ + TRACE_CTOR(balance_t, "const double"); + amounts.insert + (amounts_map::value_type(commodity_pool_t::current_pool->null_commodity, val)); +} + +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)); +} + +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)); +} + +balance_t& balance_t::operator+=(const balance_t& bal) +{ + foreach (const amounts_map::value_type& pair, bal.amounts) + *this += pair.second; + return *this; +} + +balance_t& balance_t::operator+=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot add an uninitialized amount to a balance")); + + if (amt.is_realzero()) + return *this; + + amounts_map::iterator i = amounts.find(&amt.commodity()); + if (i != amounts.end()) + i->second += amt; + else + amounts.insert(amounts_map::value_type(&amt.commodity(), amt)); + + return *this; +} + +balance_t& balance_t::operator-=(const balance_t& bal) +{ + foreach (const amounts_map::value_type& pair, bal.amounts) + *this -= pair.second; + return *this; +} + +balance_t& balance_t::operator-=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot subtract an uninitialized amount from a balance")); + + if (amt.is_realzero()) + return *this; + + amounts_map::iterator i = amounts.find(&amt.commodity()); + if (i != amounts.end()) { + i->second -= amt; + if (i->second.is_realzero()) + amounts.erase(i); + } else { + amounts.insert(amounts_map::value_type(&amt.commodity(), amt.negated())); + } + return *this; +} + +balance_t& balance_t::operator*=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot multiply a balance by an uninitialized amount")); + + if (is_realzero()) { + ; + } + else if (amt.is_realzero()) { + *this = amt; + } + else if (! amt.commodity()) { + // Multiplying by an amount with no commodity causes all the + // component amounts to be increased by the same factor. + foreach (amounts_map::value_type& pair, amounts) + pair.second *= amt; + } + else if (amounts.size() == 1) { + // Multiplying by a commoditized amount is only valid if the sole + // commodity in the balance is of the same kind as the amount's + // commodity. + if (*amounts.begin()->first == amt.commodity()) + amounts.begin()->second *= amt; + else + throw_(balance_error, + _("Cannot multiply a balance with annotated commodities by a commoditized amount")); + } + else { + assert(amounts.size() > 1); + throw_(balance_error, + _("Cannot multiply a multi-commodity balance by a commoditized amount")); + } + return *this; +} + +balance_t& balance_t::operator/=(const amount_t& amt) +{ + if (amt.is_null()) + throw_(balance_error, + _("Cannot divide a balance by an uninitialized amount")); + + if (is_realzero()) { + ; + } + else if (amt.is_realzero()) { + throw_(balance_error, _("Divide by zero")); + } + else if (! amt.commodity()) { + // Dividing by an amount with no commodity causes all the + // component amounts to be divided by the same factor. + foreach (amounts_map::value_type& pair, amounts) + pair.second /= amt; + } + else if (amounts.size() == 1) { + // Dividing by a commoditized amount is only valid if the sole + // commodity in the balance is of the same kind as the amount's + // commodity. + if (*amounts.begin()->first == amt.commodity()) + amounts.begin()->second /= amt; + else + throw_(balance_error, + _("Cannot divide a balance with annotated commodities by a commoditized amount")); + } + else { + assert(amounts.size() > 1); + throw_(balance_error, + _("Cannot divide a multi-commodity balance by a commoditized amount")); + } + return *this; +} + +optional<balance_t> +balance_t::value(const bool primary_only, + const optional<datetime_t>& moment, + const optional<commodity_t&>& in_terms_of) const +{ + balance_t temp; + bool resolved = false; + + foreach (const amounts_map::value_type& pair, amounts) { + if (optional<amount_t> val = pair.second.value(primary_only, moment, + in_terms_of)) { + temp += *val; + resolved = true; + } else { + temp += pair.second; + } + } + 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 +{ + if (! commodity) { + if (amounts.size() == 1) { + return amounts.begin()->second; + } + else if (amounts.size() > 1) { + // Try stripping annotations before giving an error. + balance_t temp(strip_annotations(keep_details_t())); + if (temp.amounts.size() == 1) + return temp.commodity_amount(commodity); + + throw_(amount_error, + _("Requested amount of a balance with multiple commodities: %1") + << temp); + } + } + else if (amounts.size() > 0) { + amounts_map::const_iterator i = + amounts.find(const_cast<commodity_t *>(&*commodity)); + if (i != amounts.end()) + return i->second; + } + return none; +} + +balance_t +balance_t::strip_annotations(const keep_details_t& what_to_keep) const +{ + balance_t temp; + + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.strip_annotations(what_to_keep); + + return temp; +} + +void balance_t::print(std::ostream& out, + const int first_width, + const int latter_width, + const bool right_justify, + const bool colorize) const +{ + bool first = true; + int lwidth = latter_width; + + if (lwidth == -1) + lwidth = first_width; + + typedef std::vector<const amount_t *> amounts_array; + amounts_array sorted; + + foreach (const amounts_map::value_type& pair, amounts) + if (pair.second) + sorted.push_back(&pair.second); + + std::stable_sort(sorted.begin(), sorted.end(), compare_amount_commodities()); + + foreach (const amount_t * amount, sorted) { + int width; + if (! first) { + out << std::endl; + width = lwidth; + } else { + first = false; + width = first_width; + } + + std::ostringstream buf; + buf << *amount; + justify(out, buf.str(), width, right_justify, + colorize && amount->sign() < 0); + } + + if (first) { + out.width(first_width); + if (right_justify) + out << std::right; + else + out << std::left; + out << 0; + } +} + +void to_xml(std::ostream& out, const balance_t& bal) +{ + push_xml x(out, "balance"); + + foreach (const balance_t::amounts_map::value_type& pair, bal.amounts) + to_xml(out, pair.second); +} + +} // namespace ledger diff --git a/src/balance.h b/src/balance.h new file mode 100644 index 00000000..826de134 --- /dev/null +++ b/src/balance.h @@ -0,0 +1,602 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup math + */ + +/** + * @file balance.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Basic type for adding multiple commodities together + * + * Unlike the amount_t class, which throws an exception if amounts of + * differing commodities are added or subtracted, the balance_t class + * is designed to allow this, tracking the amounts of each component + * commodity separately. + */ +#ifndef _BALANCE_H +#define _BALANCE_H + +#include "amount.h" + +namespace ledger { + +DECLARE_EXCEPTION(balance_error, std::runtime_error); + +/** + * @class balance_t + * + * @brief A wrapper around amount_t allowing addition of multiple commodities. + * + * The balance_t class is appopriate for keeping a running balance + * where amounts of multiple commodities may be involved. + */ +class balance_t + : public equality_comparable<balance_t, + equality_comparable<balance_t, amount_t, + equality_comparable<balance_t, double, + equality_comparable<balance_t, unsigned long, + equality_comparable<balance_t, long, + additive<balance_t, + additive<balance_t, amount_t, + additive<balance_t, double, + additive<balance_t, unsigned long, + additive<balance_t, long, + multiplicative<balance_t, amount_t, + multiplicative<balance_t, double, + multiplicative<balance_t, unsigned long, + multiplicative<balance_t, long> > > > > > > > > > > > > > +{ +public: + typedef std::map<commodity_t *, amount_t> amounts_map; + + amounts_map amounts; + + /** + * Constructors. balance_t supports similar forms of construction + * to amount_t. + * + * balance_t() creates an empty balance to which amounts or other + * balances may be added or subtracted. + * + * balance_t(amount_t) constructs a balance whose starting value is + * equal to the given amount. + * + * balance_t(double), balance_t(unsigned long) and balance_t(long) + * will construct an amount from their arguments and then construct + * a balance whose starting value is equal to that amount. This + * initial balance will have no commodity. + * + * balance_t(string) and balance_t(const char *) both convert from a + * string representation of an amount to a balance whose initial + * value is that amount. This is the proper way to initialize a + * balance like '$100.00'. + */ + balance_t() { + TRACE_CTOR(balance_t, ""); + } + balance_t(const amount_t& amt) { + 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)); + } + 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)); + } + 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)); + } + + /** + * Destructor. Destroys all of the accumulated amounts in the + * balance. + */ + ~balance_t() { + TRACE_DTOR(balance_t); + } + + /** + * Assignment and copy operators. An balance may be assigned or copied. + */ + balance_t(const balance_t& bal) : amounts(bal.amounts) { + TRACE_CTOR(balance_t, "copy"); + } + + balance_t& operator=(const balance_t& bal) { + if (this != &bal) + amounts = bal.amounts; + return *this; + } + balance_t& operator=(const amount_t& amt) { + if (amt.is_null()) + throw_(balance_error, + _("Cannot assign an uninitialized amount to a balance")); + + amounts.clear(); + if (! amt.is_realzero()) + amounts.insert(amounts_map::value_type(&amt.commodity(), amt)); + + return *this; + } + + balance_t& operator=(const string& str) { + return *this = balance_t(str); + } + balance_t& operator=(const char * str) { + return *this = balance_t(str); + } + + /** + * Comparison operators. Balances are fairly restrictive in terms + * of how they may be compared. They may be compared for equality + * or inequality, but this is all, since the concept of "less than" + * or "greater than" makes no sense when amounts of multiple + * commodities are involved. + * + * Balances may also be compared to amounts, in which case the sum + * of the balance must equal the amount exactly. + * + * If a comparison between balances is desired, the balances must + * first be rendered to value equivalent amounts using the `value' + * method, to determine a market valuation at some specific moment + * in time. + */ + bool operator==(const balance_t& bal) const { + amounts_map::const_iterator i, j; + for (i = amounts.begin(), j = bal.amounts.begin(); + i != amounts.end() && j != bal.amounts.end(); + i++, j++) { + if (! (i->first == j->first && i->second == j->second)) + return false; + } + return i == amounts.end() && j == bal.amounts.end(); + } + bool operator==(const amount_t& amt) const { + if (amt.is_null()) + throw_(balance_error, + _("Cannot compare a balance to an uninitialized amount")); + + if (amt.is_realzero()) + return amounts.empty(); + else + return amounts.size() == 1 && amounts.begin()->second == amt; + } + + template <typename T> + bool operator==(const T& val) const { + return *this == amount_t(val); + } + + /** + * Binary arithmetic operators. Balances support addition and + * subtraction of other balances or amounts, but multiplication and + * division are restricted to uncommoditized amounts only. + */ + balance_t& operator+=(const balance_t& bal); + balance_t& operator+=(const amount_t& amt); + balance_t& operator+=(const double val) { + return *this += amount_t(val); + } + balance_t& operator+=(const unsigned long val) { + return *this += amount_t(val); + } + balance_t& operator+=(const long val) { + return *this += amount_t(val); + } + + balance_t& operator-=(const balance_t& bal); + balance_t& operator-=(const amount_t& amt); + balance_t& operator-=(const double val) { + return *this -= amount_t(val); + } + balance_t& operator-=(const unsigned long val) { + return *this -= amount_t(val); + } + balance_t& operator-=(const long val) { + return *this -= amount_t(val); + } + + balance_t& operator*=(const amount_t& amt); + balance_t& operator*=(const double val) { + return *this *= amount_t(val); + } + balance_t& operator*=(const unsigned long val) { + return *this *= amount_t(val); + } + balance_t& operator*=(const long val) { + return *this *= amount_t(val); + } + + balance_t& operator/=(const amount_t& amt); + balance_t& operator/=(const double val) { + return *this /= amount_t(val); + } + balance_t& operator/=(const unsigned long val) { + return *this /= amount_t(val); + } + balance_t& operator/=(const long val) { + return *this /= amount_t(val); + } + + /** + * Unary arithmetic operators. There are only a few unary methods + * support on balance: + * + * negate(), also unary minus (- x), returns a balance all of whose + * component amounts have been negated. In order words, it inverts + * the sign of all member amounts. + * + * abs() returns a balance where no component amount is negative. + * + * reduce() reduces the values in a balance to their most basic + * commodity forms, for amounts that utilize "scaling commodities". + * For example, a balance of 1h and 1m after reduction will be + * 3660s. + * + * unreduce(), if used with amounts that use "scaling commodities", + * yields the most compact form greater than 1.0 for each component + * amount. That is, a balance of 10m and 1799s will unreduce to + * 39.98m. + * + * value(optional<datetime_t>) returns the total historical value for + * a balance -- the default moment returns a value based on the most + * recently known price -- based on the price history of its + * component commodities. See amount_t::value for an example. + * + * Further, for the sake of efficiency and avoiding temporary + * objects, the following methods support "in-place" variants act on + * the balance itself and return a reference to the result + * (`*this'): + * + * in_place_negate() + * in_place_reduce() + * in_place_unreduce() + */ + balance_t negated() const { + balance_t temp(*this); + temp.in_place_negate(); + return temp; + } + void in_place_negate() { + foreach (amounts_map::value_type& pair, amounts) + pair.second.in_place_negate(); + } + balance_t operator-() const { + return negated(); + } + + balance_t abs() const { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.abs(); + return temp; + } + + balance_t rounded() const { + balance_t temp(*this); + temp.in_place_round(); + return temp; + } + void in_place_round() { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.rounded(); + *this = temp; + } + + balance_t truncated() const { + balance_t temp(*this); + temp.in_place_truncate(); + return temp; + } + void in_place_truncate() { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.truncated(); + *this = temp; + } + + balance_t floored() const { + balance_t temp(*this); + temp.in_place_floor(); + return temp; + } + void in_place_floor() { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.floored(); + *this = temp; + } + + balance_t unrounded() const { + balance_t temp(*this); + temp.in_place_unround(); + return temp; + } + void in_place_unround() { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.unrounded(); + *this = temp; + } + + balance_t reduced() const { + balance_t temp(*this); + temp.in_place_reduce(); + return temp; + } + void in_place_reduce() { + // A temporary must be used here because reduction may cause + // multiple component amounts to collapse to the same commodity. + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.reduced(); + *this = temp; + } + + balance_t unreduced() const { + balance_t temp(*this); + temp.in_place_unreduce(); + return temp; + } + void in_place_unreduce() { + // A temporary must be used here because unreduction may cause + // multiple component amounts to collapse to the same commodity. + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.unreduced(); + *this = temp; + } + + optional<balance_t> + value(const bool primary_only = false, + const optional<datetime_t>& moment = none, + const optional<commodity_t&>& in_terms_of = none) const; + + balance_t price() const; + + /** + * Truth tests. An balance may be truth test in two ways: + * + * is_nonzero(), or operator bool, returns true if a balance's + * display value is not zero. + * + * is_zero() returns true if an balance's display value is zero. + * Thus, a balance containing $0.0001 is considered zero if the + * current display precision for dollars is two decimal places. + * + * is_realzero() returns true if an balance's actual value is zero. + * Thus, a balance containing $0.0001 is never considered realzero. + * + * is_empty() returns true if a balance has no amounts within it. + * This can occur after a balance has been default initialized, or + * if the exact amount it contains is subsequently subtracted from + * it. + */ + operator bool() const { + return is_nonzero(); + } + + bool is_nonzero() const { + if (is_empty()) + return false; + + foreach (const amounts_map::value_type& pair, amounts) + if (pair.second.is_nonzero()) + return true; + return false; + } + + bool is_zero() const { + if (is_empty()) + return true; + + foreach (const amounts_map::value_type& pair, amounts) + if (! pair.second.is_zero()) + return false; + return true; + } + + bool is_realzero() const { + if (is_empty()) + return true; + + foreach (const amounts_map::value_type& pair, amounts) + if (! pair.second.is_realzero()) + return false; + return true; + } + + bool is_empty() const { + return amounts.size() == 0; + } + bool single_amount() const { + return amounts.size() == 1; + } + + /** + * Conversion methods. A balance can be converted to an amount, but + * only if contains a single component amount. + */ + operator string() const { + return to_string(); + } + string to_string() const { + std::ostringstream buf; + print(buf); + return buf.str(); + } + + amount_t to_amount() const { + if (is_empty()) + throw_(balance_error, _("Cannot convert an empty balance to an amount")); + else if (amounts.size() == 1) + return amounts.begin()->second; + else + throw_(balance_error, + _("Cannot convert a balance with multiple commodities to an amount")); + return amount_t(); + } + + /** + * Commodity-related methods. Balances support two + * commodity-related methods: + * + * commodity_count() returns the number of different commodities + * stored in the balance. + * + * commodity_amount(optional<commodity_t>) returns an (optional) + * amount for the given commodity within the balance; if no + * commodity is specified, it returns the (optional) uncommoditized + * component of the balance. If no matching element can be found, + * boost::none is returned. + */ + std::size_t commodity_count() const { + return amounts.size(); + } + + optional<amount_t> + commodity_amount(const optional<const commodity_t&>& commodity = none) const; + + balance_t number() const { + balance_t temp; + foreach (const amounts_map::value_type& pair, amounts) + temp += pair.second.number(); + return temp; + } + + /** + * Annotated commodity methods. The amounts contained by a balance + * may use annotated commodities. The `strip_annotations' method + * will return a balance all of whose component amount have had + * their commodity annotations likewise stripped. See + * amount_t::strip_annotations for more details. + */ + balance_t strip_annotations(const keep_details_t& what_to_keep) const; + + /** + * Printing methods. A balance may be output to a stream using the + * `print' method. There is also a global operator<< defined which + * simply calls print for a balance on the given stream. There is + * one form of the print method, which takes two required arguments + * and one arguments with a default value: + * + * print(ostream, int first_width, int latter_width) prints a + * balance to the given output stream, using each commodity's + * default display characteristics. The first_width parameter + * specifies the width that should be used for printing amounts + * (since they are likely to vary in width). The latter_width, if + * specified, gives the width to be used for each line after the + * first. This is useful when printing in a column which falls at + * the right-hand side of the screen. + * + * In addition to the width constraints, balances will also print + * with commodities in alphabetized order, regardless of the + * relative amounts of those commodities. There is no option to + * change this behavior. + */ + void print(std::ostream& out, + const int first_width = -1, + const int latter_width = -1, + const bool right_justify = false, + const bool colorize = false) const; + + /** + * Debugging methods. There are two methods defined to help with + * debugging: + * + * dump(ostream) dumps a balance to an output stream. There is + * little different from print(), it simply surrounds the display + * value with a marker, for example "BALANCE($1.00, DM 12.00)". + * This code is used by other dumping code elsewhere in Ledger. + * + * valid() returns true if the amounts within the balance are valid. + */ + void dump(std::ostream& out) const { + out << "BALANCE("; + bool first = true; + foreach (const amounts_map::value_type& pair, amounts) { + if (first) + first = false; + else + out << ", "; + pair.second.print(out); + } + out << ")"; + } + + bool valid() const { + foreach (const amounts_map::value_type& pair, amounts) + if (! pair.second.valid()) { + DEBUG("ledger.validate", "balance_t: ! pair.second.valid()"); + return false; + } + return true; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & amounts; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline std::ostream& operator<<(std::ostream& out, const balance_t& bal) { + bal.print(out, 12); + return out; +} + +void to_xml(std::ostream& out, const balance_t& amt); + +} // namespace ledger + +#endif // _BALANCE_H diff --git a/src/chain.cc b/src/chain.cc new file mode 100644 index 00000000..113a71d8 --- /dev/null +++ b/src/chain.cc @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2003-2009, 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 "chain.h" +#include "predicate.h" +#include "filters.h" +#include "report.h" +#include "session.h" + +namespace ledger { + +post_handler_ptr chain_post_handlers(report_t& report, + post_handler_ptr base_handler, + bool for_accounts_report) +{ + post_handler_ptr handler(base_handler); + predicate_t display_predicate; + predicate_t only_predicate; + + assert(report.HANDLED(amount_)); + expr_t& expr(report.HANDLER(amount_).expr); + expr.set_context(&report); + + if (! for_accounts_report) { + // Make sure only forecast postings which match are allowed through + if (report.HANDLED(forecast_while_)) { + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(forecast_while_).str(), + report.what_to_keep()), + report)); + } + + // truncate_xacts cuts off a certain number of _xacts_ from being + // displayed. It does not affect calculation. + if (report.HANDLED(head_) || report.HANDLED(tail_)) + handler.reset + (new truncate_xacts(handler, + report.HANDLED(head_) ? + report.HANDLER(head_).value.to_int() : 0, + report.HANDLED(tail_) ? + report.HANDLER(tail_).value.to_int() : 0)); + + // filter_posts will only pass through posts matching the + // `display_predicate'. + if (report.HANDLED(display_)) { + display_predicate = predicate_t(report.HANDLER(display_).str(), + report.what_to_keep()); + handler.reset(new filter_posts(handler, display_predicate, report)); + } + } + + // changed_value_posts adds virtual posts to the list to account for changes + // in market value of commodities, which otherwise would affect the running + // total unpredictably. + if (report.HANDLED(revalued) && (! for_accounts_report || + report.HANDLED(unrealized))) + handler.reset(new changed_value_posts(handler, report, + for_accounts_report, + report.HANDLED(unrealized))); + + // calc_posts computes the running total. When this appears will determine, + // for example, whether filtered posts are included or excluded from the + // running total. + handler.reset(new calc_posts(handler, expr, (! for_accounts_report || + (report.HANDLED(revalued) && + report.HANDLED(unrealized))))); + + // filter_posts will only pass through posts matching the + // `secondary_predicate'. + if (report.HANDLED(only_)) { + only_predicate = predicate_t(report.HANDLER(only_).str(), + report.what_to_keep()); + handler.reset(new filter_posts(handler, only_predicate, report)); + } + + if (! for_accounts_report) { + // sort_posts will sort all the posts it sees, based on the `sort_order' + // value expression. + if (report.HANDLED(sort_)) { + if (report.HANDLED(sort_xacts_)) + handler.reset(new sort_xacts(handler, report.HANDLER(sort_).str())); + else + handler.reset(new sort_posts(handler, report.HANDLER(sort_).str())); + } + else if (! report.HANDLED(period_) && + ! report.HANDLED(unsorted)) { + handler.reset(new sort_posts(handler, "date")); + } + + // collapse_posts causes xacts with multiple posts to appear as xacts + // with a subtotaled post for each commodity used. + if (report.HANDLED(collapse)) + handler.reset(new collapse_posts(handler, expr, + display_predicate, only_predicate, + report.HANDLED(collapse_if_zero))); + + // subtotal_posts combines all the posts it receives into one subtotal + // xact, which has one post for each commodity in each account. + // + // period_posts is like subtotal_posts, but it subtotals according to time + // periods rather than totalling everything. + // + // dow_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)); + else if (report.HANDLED(subtotal)) + handler.reset(new subtotal_posts(handler, expr)); + } + + if (report.HANDLED(dow)) + handler.reset(new dow_posts(handler, expr)); + else if (report.HANDLED(by_payee)) + handler.reset(new by_payee_posts(handler, expr)); + + // interval_posts groups posts together based on a time period, such as + // weekly or monthly. + if (report.HANDLED(period_)) { + handler.reset(new interval_posts(handler, expr, + report.HANDLER(period_).str(), + report.HANDLED(exact), + report.HANDLED(empty))); + handler.reset(new sort_posts(handler, "date")); + } + + if (report.HANDLED(date_)) + handler.reset(new transfer_details(handler, transfer_details::SET_DATE, + report.session.journal->master, + report.HANDLER(date_).str(), + report)); + if (report.HANDLED(account_)) + handler.reset(new transfer_details(handler, transfer_details::SET_ACCOUNT, + report.session.journal->master, + report.HANDLER(account_).str(), + report)); + if (report.HANDLED(payee_)) + handler.reset(new transfer_details(handler, transfer_details::SET_PAYEE, + report.session.journal->master, + report.HANDLER(payee_).str(), + report)); + + // related_posts will pass along all posts related to the post received. If + // the `related_all' handler is on, then all the xact's posts are passed; + // meaning that if one post of an xact is to be printed, all the post for + // that xact will be printed. + if (report.HANDLED(related)) + handler.reset(new related_posts(handler, report.HANDLED(related_all))); + + // anonymize_posts removes all meaningful information from xact payee's and + // account names, for the sake of creating useful bug reports. + if (report.HANDLED(anon)) + handler.reset(new anonymize_posts(handler)); + + // This filter_posts will only pass through posts matching the `predicate'. + if (report.HANDLED(limit_)) { + DEBUG("report.predicate", + "Report predicate expression = " << report.HANDLER(limit_).str()); + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + + // budget_posts takes a set of posts from a data file and uses them to + // generate "budget posts" which balance against the reported posts. + // + // forecast_posts is a lot like budget_posts, except that it adds xacts + // only for the future, and does not balance them against anything but the + // future balance. + + if (report.budget_flags != BUDGET_NO_BUDGET) { + budget_posts * budget_handler = new budget_posts(handler, + report.budget_flags); + budget_handler->add_period_xacts(report.session.journal->period_xacts); + handler.reset(budget_handler); + + // Apply this before the budget handler, so that only matching posts are + // calculated toward the budget. The use of filter_posts above will + // further clean the results so that no automated posts that don't match + // the filter get reported. + if (report.HANDLED(limit_)) + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + else if (report.HANDLED(forecast_while_)) { + forecast_posts * forecast_handler + = new forecast_posts(handler, + predicate_t(report.HANDLER(forecast_while_).str(), + report.what_to_keep()), + report, + report.HANDLED(forecast_years_) ? + static_cast<std::size_t> + (report.HANDLER(forecast_years_).value.to_long()) : + 5UL); + forecast_handler->add_period_xacts(report.session.journal->period_xacts); + handler.reset(forecast_handler); + + // See above, under budget_posts. + if (report.HANDLED(limit_)) + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + + return handler; +} + +} // namespace ledger diff --git a/src/chain.h b/src/chain.h new file mode 100644 index 00000000..a2b75b3a --- /dev/null +++ b/src/chain.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file chain.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _CHAIN_H +#define _CHAIN_H + +#include "utils.h" + +namespace ledger { + +class post_t; +class account_t; + +template <typename T> +struct item_handler : public noncopyable +{ + shared_ptr<item_handler> handler; + +public: + item_handler() { + TRACE_CTOR(item_handler, ""); + } + item_handler(shared_ptr<item_handler> _handler) : handler(_handler) { + TRACE_CTOR(item_handler, "shared_ptr<item_handler>"); + } + virtual ~item_handler() { + TRACE_DTOR(item_handler); + } + + virtual void flush() { + if (handler.get()) + handler->flush(); + } + virtual void operator()(T& item) { + if (handler.get()) { + check_for_signal(); + (*handler.get())(item); + } + } +}; + +typedef shared_ptr<item_handler<post_t> > post_handler_ptr; +typedef shared_ptr<item_handler<account_t> > acct_handler_ptr; + +class report_t; +post_handler_ptr +chain_post_handlers(report_t& report, + post_handler_ptr base_handler, + bool for_accounts_report = false); + +} // namespace ledger + +#endif // _CHAIN_H diff --git a/src/commodity.cc b/src/commodity.cc new file mode 100644 index 00000000..79ed77fe --- /dev/null +++ b/src/commodity.cc @@ -0,0 +1,705 @@ +/* + * Copyright (c) 2003-2009, 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 "amount.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +bool commodity_t::european_by_default = false; + +void commodity_t::history_t::add_price(commodity_t& source, + 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); + } else { + DEBUG("commodity.prices.add", + "marking commodity " << source.symbol() << " as primary"); + source.add_flags(COMMODITY_PRIMARY); + } +} + +bool commodity_t::history_t::remove_price(const datetime_t& date) +{ + DEBUG("commodity.prices.add", "remove_price: " << date); + + history_map::size_type n = prices.erase(date); + if (n > 0) + return true; + return false; +} + +void commodity_t::varied_history_t:: + add_price(commodity_t& source, + const datetime_t& date, + const amount_t& price, + const bool reflexive) +{ + 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); + + hist->add_price(source, date, price, reflexive); +} + +bool commodity_t::varied_history_t::remove_price(const datetime_t& date, + commodity_t& comm) +{ + DEBUG("commodity.prices.add", "varied_remove_price: " << date << ", " << comm); + + if (optional<history_t&> hist = history(comm)) + return hist->remove_price(date); + return false; +} + +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 +{ + price_point_t point; + bool found = false; + +#define DEBUG_INDENT(cat, indent) \ + do { \ + if (SHOW_DEBUG(cat)) \ + for (int i = 0; i < indent; i++) \ + ledger::_log_buffer << " "; \ + } while (false) + +#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); + } +#endif + + if (prices.size() == 0) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " there are no prices in this history"); +#endif + return none; + } + + if (! moment) { + history_map::const_reverse_iterator r = prices.rbegin(); + point.when = (*r).first; + point.price = (*r).second; + found = true; +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " using most recent price"); +#endif + } else { + history_map::const_iterator i = prices.lower_bound(*moment); + if (i == prices.end()) { + history_map::const_reverse_iterator r = prices.rbegin(); + point.when = (*r).first; + point.price = (*r).second; + found = true; +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " using last price"); +#endif + } 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; + } +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " using found price"); +#endif + } + } + + if (! found) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " could not find a price"); +#endif + return none; + } + else if (moment && point.when > *moment) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " price is too young "); +#endif + return none; + } + else if (oldest && point.when < *oldest) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", " price is too old "); +#endif + return none; + } + else { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", + " returning price: " << point.when << ", " << point.price); +#endif + return point; + } +} + +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 +{ + optional<price_point_t> point; + optional<datetime_t> limit = oldest; + + assert(! commodity || source != *commodity); + +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", "varied_find_price for: " << source); + + DEBUG_INDENT("commodity.prices.find", indent); + 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 (history_by_commodity_map::value_type hist, histories) { + commodity_t& comm(*hist.first); + if (comm == source) + continue; + +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", + " searching for price via commodity '" << comm << "'"); +#endif + + 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; + + if (commodity && comm != *commodity) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", " looking for translation price"); +#endif + + xlat = comm.find_price(commodity, moment, limit +#if defined(DEBUG_ON) + , indent + 2 +#endif + ); + if (xlat) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", " found translated price " + << xlat->price << " from " << xlat->when); +#endif + + point->price = xlat->price * point->price; + if (xlat->when < point->when) { + point->when = xlat->when; +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", + " adjusting date of result back to " << point->when); +#endif + } + } else { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", " saw no translated price there"); +#endif + continue; + } + } + + assert(! commodity || point->price.commodity() == *commodity); +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", + " saw a price there: " << point->price << " from " << point->when); +#endif + if (! limit || point->when > *limit) { + limit = point->when; + best = *point; + found = true; + } + } else { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent + 1); + DEBUG("commodity.prices.find", " saw no price there"); +#endif + } + } + + if (found) { +#if defined(DEBUG_ON) + DEBUG_INDENT("commodity.prices.find", indent); + DEBUG("commodity.prices.find", + " found price " << best.price << " from " << best.when); + DEBUG("commodity.download", + "found price " << best.price << " from " << best.when); +#endif + 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); + } + + history_by_commodity_map::iterator i = histories.find(comm); + if (i != histories.end()) + return (*i).second; + + return none; +} + +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) +{ + 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); + DEBUG("commodity.download", "slip.moment = " << seconds_diff); + } else { + seconds_diff = (TRUE_CURRENT_TIME() - point->when).total_seconds(); + DEBUG("commodity.download", "slip.now = " << seconds_diff); + } + + DEBUG("commodity.download", "leeway = " << pool().quote_leeway); + if (seconds_diff < pool().quote_leeway) + exceeds_leeway = false; + } + + if (exceeds_leeway) { + DEBUG("commodity.download", + "attempting to download a more current quote..."); + if (optional<price_point_t> quote = + pool().get_commodity_quote(*this, in_terms_of)) { + if (! in_terms_of || + (quote->price.has_commodity() && + quote->price.commodity() == *in_terms_of)) + return quote; + } + } + } + return point; +} + +commodity_t::operator bool() const +{ + return this != pool().null_commodity; +} + +bool commodity_t::symbol_needs_quotes(const string& symbol) +{ + foreach (char ch, symbol) + if (std::isspace(ch) || std::isdigit(ch) || ch == '-' || ch == '.') + return true; + + return false; +} + +namespace { + bool is_reserved_token(const char * buf) + { + switch (buf[0]) { + case 'a': + return std::strcmp(buf, "and") == 0; + case 'd': + return std::strcmp(buf, "div") == 0; + case 'e': + return std::strcmp(buf, "else") == 0; + case 'f': + return std::strcmp(buf, "false") == 0; + case 'i': + return std::strcmp(buf, "if") == 0; + case 'o': + return std::strcmp(buf, "or") == 0; + case 'n': + return std::strcmp(buf, "not") == 0; + case 't': + return std::strcmp(buf, "true") == 0; + } + return false; + } +} + +void commodity_t::parse_symbol(std::istream& in, string& symbol) +{ + // Invalid commodity characters: + // SPACE, TAB, NEWLINE, RETURN + // 0-9 . , ; - + * / ^ ? : & | ! = + // < > { } [ ] ( ) @ + + static int invalid_chars[256] = { + /* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ + /* 00 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, + /* 10 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 20 */ 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, + /* 30 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 40 */ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 50 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, + /* 60 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 70 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, + /* 80 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* 90 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* a0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* b0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* c0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* d0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* e0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* f0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + istream_pos_type pos = in.tellg(); + + char buf[256]; + char c = peek_next_nonws(in); + if (c == '"') { + in.get(c); + READ_INTO(in, buf, 255, c, c != '"'); + if (c == '"') + in.get(c); + else + throw_(amount_error, _("Quoted commodity symbol lacks closing quote")); + } else { + char * _p = buf; + c = static_cast<char>(in.peek()); + while (_p - buf < 255 && in.good() && ! in.eof() && c != '\n') { + std::size_t bytes = 0; + std::ptrdiff_t size = _p - buf; + unsigned char d = c; + + // Check for the start of a UTF-8 multi-byte encoded string + if (d >= 192 && d <= 223 && size < 254) + bytes = 2; + else if (d >= 224 && d <= 239 && size < 253) + bytes = 3; + else if (d >= 240 && d <= 247 && size < 252) + bytes = 4; + else if (d >= 248 && d <= 251 && size < 251) + bytes = 5; + else if (d >= 252 && d <= 253 && size < 250) + bytes = 6; + else if (d >= 254) // UTF-8 encoding error + break; + + if (bytes > 0) { // we're looking at a UTF-8 encoding + for (std::size_t i = 0; i < bytes; i++) { + in.get(c); + if (in.bad() || in.eof()) + throw_(amount_error, _("Invalid UTF-8 encoding for commodity name")); + *_p++ = c; + } + } + else if (invalid_chars[static_cast<unsigned char>(c)]) { + break; + } + else { + in.get(c); + if (in.eof()) + break; + if (c == '\\') { + in.get(c); + if (in.eof()) + throw_(amount_error, _("Backslash at end of commodity name")); + } + *_p++ = c; + } + + c = static_cast<char>(in.peek()); + } + *_p = '\0'; + + if (is_reserved_token(buf)) + buf[0] = '\0'; + } + symbol = buf; + + if (symbol.length() == 0) { + in.clear(); + in.seekg(pos, std::ios::beg); + } +} + +void commodity_t::parse_symbol(char *& p, string& symbol) +{ + if (*p == '"') { + char * q = std::strchr(p + 1, '"'); + if (! q) + throw_(amount_error, _("Quoted commodity symbol lacks closing quote")); + symbol = string(p + 1, 0, q - p - 1); + p = q + 2; + } else { + char * q = next_element(p); + symbol = p; + if (q) + p = q; + else + p += symbol.length(); + } + if (symbol.empty()) + throw_(amount_error, _("Failed to parse commodity")); +} + +bool commodity_t::valid() const +{ + if (symbol().empty() && this != pool().null_commodity) { + DEBUG("ledger.validate", + "commodity_t: symbol().empty() && this != null_commodity"); + return false; + } + + if (annotated && ! base) { + DEBUG("ledger.validate", "commodity_t: annotated && ! base"); + return false; + } + + if (precision() > 16) { + DEBUG("ledger.validate", "commodity_t: precision() > 16"); + return false; + } + + return true; +} + +bool compare_amount_commodities::operator()(const amount_t * left, + const amount_t * right) const +{ + commodity_t& leftcomm(left->commodity()); + commodity_t& rightcomm(right->commodity()); + + DEBUG("commodity.compare", " left symbol (" << leftcomm << ")"); + DEBUG("commodity.compare", "right symbol (" << rightcomm << ")"); + + int cmp = leftcomm.base_symbol().compare(rightcomm.base_symbol()); + if (cmp != 0) + return cmp < 0; + + if (! leftcomm.has_annotation()) { + return rightcomm.has_annotation(); + } + else if (! rightcomm.has_annotation()) { + return ! leftcomm.has_annotation(); + } + else { + annotated_commodity_t& aleftcomm(static_cast<annotated_commodity_t&>(leftcomm)); + annotated_commodity_t& arightcomm(static_cast<annotated_commodity_t&>(rightcomm)); + + if (! aleftcomm.details.price && arightcomm.details.price) + return true; + if (aleftcomm.details.price && ! arightcomm.details.price) + return false; + + if (aleftcomm.details.price && arightcomm.details.price) { + amount_t leftprice(*aleftcomm.details.price); + amount_t rightprice(*arightcomm.details.price); + + if (leftprice.commodity() == rightprice.commodity()) { + return (leftprice - rightprice).sign() < 0; + } else { + // Since we have two different amounts, there's really no way + // to establish a true sorting order; we'll just do it based + // on the numerical values. + leftprice.clear_commodity(); + rightprice.clear_commodity(); + return (leftprice - rightprice).sign() < 0; + } + } + + if (! aleftcomm.details.date && arightcomm.details.date) + return true; + if (aleftcomm.details.date && ! arightcomm.details.date) + return false; + + if (aleftcomm.details.date && arightcomm.details.date) { + gregorian::date_duration diff = + *aleftcomm.details.date - *arightcomm.details.date; + return diff.is_negative(); + } + + if (! aleftcomm.details.tag && arightcomm.details.tag) + return true; + if (aleftcomm.details.tag && ! arightcomm.details.tag) + return false; + + if (aleftcomm.details.tag && arightcomm.details.tag) + return *aleftcomm.details.tag < *arightcomm.details.tag; + + assert(false); + return true; + } +} + +void to_xml(std::ostream& out, const commodity_t& comm, + bool commodity_details) +{ + push_xml x(out, "commodity", true); + + out << " flags=\""; + if (! (comm.has_flags(COMMODITY_STYLE_SUFFIXED))) out << 'P'; + if (comm.has_flags(COMMODITY_STYLE_SEPARATED)) out << 'S'; + if (comm.has_flags(COMMODITY_STYLE_THOUSANDS)) out << 'T'; + if (comm.has_flags(COMMODITY_STYLE_EUROPEAN)) out << 'E'; + out << '"'; + + x.close_attrs(); + + { + push_xml y(out, "symbol"); + out << y.guard(comm.symbol()); + } + + 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); + } + } + } + } + } +} + +} // namespace ledger diff --git a/src/commodity.h b/src/commodity.h new file mode 100644 index 00000000..3370f3f2 --- /dev/null +++ b/src/commodity.h @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup math + */ + +/** + * @file commodity.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Types for handling commodities + * + * This file contains one of the most basic types in Ledger: + * commodity_t, and its annotated cousin, annotated_commodity_t. + */ +#ifndef _COMMODITY_H +#define _COMMODITY_H + +namespace ledger { + +class keep_details_t; + +DECLARE_EXCEPTION(commodity_error, std::runtime_error); + +struct price_point_t +{ + datetime_t when; + amount_t price; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & when; + ar & price; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +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; + + class base_t : public noncopyable, public supports_flags<uint_least16_t> + { + public: +#define COMMODITY_STYLE_DEFAULTS 0x000 +#define COMMODITY_STYLE_SUFFIXED 0x001 +#define COMMODITY_STYLE_SEPARATED 0x002 +#define COMMODITY_STYLE_EUROPEAN 0x004 +#define COMMODITY_STYLE_THOUSANDS 0x008 +#define COMMODITY_NOMARKET 0x010 +#define COMMODITY_BUILTIN 0x020 +#define COMMODITY_WALKED 0x040 +#define COMMODITY_KNOWN 0x080 +#define COMMODITY_PRIMARY 0x100 + + 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; + + mutable bool searched; + + public: + explicit base_t(const string& _symbol) + : supports_flags<uint_least16_t> + (commodity_t::european_by_default ? + static_cast<uint_least16_t>(COMMODITY_STYLE_EUROPEAN) : + static_cast<uint_least16_t>(COMMODITY_STYLE_DEFAULTS)), + symbol(_symbol), precision(0), searched(false) { + TRACE_CTOR(base_t, "const string&"); + } + virtual ~base_t() { + TRACE_DTOR(base_t); + } + +#if defined(HAVE_BOOST_SERIALIZATION) + private: + base_t() { + TRACE_CTOR(base_t, ""); + } + + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<supports_flags<uint_least16_t> >(*this); + ar & symbol; + ar & precision; + ar & name; + ar & note; + ar & varied_history; + ar & smaller; + ar & larger; + } +#endif // HAVE_BOOST_SERIALIZATION + }; + + shared_ptr<base_t> base; + + commodity_pool_t * parent_; + optional<string> qualified_symbol; + optional<string> mapping_key_; + bool annotated; + + explicit commodity_t(commodity_pool_t * _parent, + const shared_ptr<base_t>& _base) + : delegates_flags<uint_least16_t>(*_base.get()), base(_base), + parent_(_parent), annotated(false) { + TRACE_CTOR(commodity_t, "commodity_pool_t *, shared_ptr<base_t>"); + } + +public: + static bool european_by_default; + + virtual ~commodity_t() { + TRACE_DTOR(commodity_t); + } + + operator bool() const; + + virtual bool operator==(const commodity_t& comm) const { + if (comm.annotated) + return comm == *this; + return base.get() == comm.base.get(); + } + + static bool symbol_needs_quotes(const string& symbol); + + virtual commodity_t& referent() { + return *this; + } + virtual const commodity_t& referent() const { + return *this; + } + + bool has_annotation() const { + return annotated; + } + + virtual commodity_t& strip_annotations(const keep_details_t&) { + return *this; + } + virtual void write_annotations(std::ostream&) const {} + + commodity_pool_t& pool() const { + return *parent_; + } + + string base_symbol() const { + return base->symbol; + } + string symbol() const { + return qualified_symbol ? *qualified_symbol : base_symbol(); + } + + string mapping_key() const { + if (mapping_key_) + return *mapping_key_; + else + return base_symbol(); + } + + optional<string> name() const { + return base->name; + } + void set_name(const optional<string>& arg = none) { + base->name = arg; + } + + optional<string> note() const { + return base->note; + } + void set_note(const optional<string>& arg = none) { + base->note = arg; + } + + amount_t::precision_t precision() const { + return base->precision; + } + void set_precision(amount_t::precision_t arg) { + base->precision = arg; + } + + optional<amount_t> smaller() const { + return base->smaller; + } + void set_smaller(const optional<amount_t>& arg = none) { + base->smaller = arg; + } + + optional<amount_t> larger() const { + return base->larger; + } + void set_larger(const optional<amount_t>& arg = none) { + base->larger = arg; + } + + optional<varied_history_t&> varied_history() { + if (base->varied_history) + return *base->varied_history; + return none; + } + optional<const varied_history_t&> varied_history() const { + if (base->varied_history) + return *base->varied_history; + return none; + } + + optional<history_t&> history(const optional<commodity_t&>& commodity); + + // These methods provide a transparent pass-through to the underlying + // base->varied_history object. + + 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); + } + bool remove_price(const datetime_t& date, commodity_t& commodity) { + if (base->varied_history) + base->varied_history->remove_price(date, commodity); + return false; + } + + optional<price_point_t> + find_price(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 { + if (base->varied_history && ! has_flags(COMMODITY_WALKED)) { + const_cast<commodity_t&>(*this).add_flags(COMMODITY_WALKED); + optional<price_point_t> point = + base->varied_history->find_price(*this, commodity, moment, oldest +#if defined(DEBUG_ON) + , indent +#endif + ); + const_cast<commodity_t&>(*this).drop_flags(COMMODITY_WALKED); + return point; + } + return none; + } + + 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); + + // Methods related to parsing, reading, writing, etc., the commodity + // itself. + + static void parse_symbol(std::istream& in, string& symbol); + static void parse_symbol(char *& p, string& symbol); + static string parse_symbol(std::istream& in) { + string temp; + parse_symbol(in, temp); + return temp; + } + + void print(std::ostream& out) const { + out << symbol(); + } + + bool valid() const; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + supports_flags<uint_least16_t> temp_flags; + +protected: + explicit commodity_t() + : delegates_flags<uint_least16_t>(temp_flags), parent_(NULL), + annotated(false) { + TRACE_CTOR(commodity_t, ""); + } + +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<delegates_flags<uint_least16_t> >(*this); + 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); + return out; +} + +struct compare_amount_commodities { + bool operator()(const amount_t * left, const amount_t * right) const; +}; + +void to_xml(std::ostream& out, const commodity_t& comm, + bool commodity_details = false); + +} // namespace ledger + +#endif // _COMMODITY_H diff --git a/src/compare.cc b/src/compare.cc new file mode 100644 index 00000000..36884a46 --- /dev/null +++ b/src/compare.cc @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2003-2009, 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 "compare.h" +#include "op.h" +#include "post.h" +#include "account.h" + +namespace ledger { + +void push_sort_value(std::list<sort_value_t>& sort_values, + expr_t::ptr_op_t node, scope_t& scope) +{ + if (node->kind == expr_t::op_t::O_CONS) { + push_sort_value(sort_values, node->left(), scope); + push_sort_value(sort_values, node->right(), scope); + } else { + bool inverted = false; + + if (node->kind == expr_t::op_t::O_NEG) { + inverted = true; + node = node->left(); + } + + sort_values.push_back(sort_value_t()); + sort_values.back().inverted = inverted; + sort_values.back().value = expr_t(node).calc(scope).simplified(); + + if (sort_values.back().value.is_null()) + throw_(calc_error, + _("Could not determine sorting value based an expression")); + } +} + +template <> +bool compare_items<post_t>::operator()(post_t * left, post_t * right) +{ + assert(left); + assert(right); + + post_t::xdata_t& lxdata(left->xdata()); + if (! lxdata.has_flags(POST_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *left); + find_sort_values(lxdata.sort_values, bound_scope); + lxdata.add_flags(POST_EXT_SORT_CALC); + } + + post_t::xdata_t& rxdata(right->xdata()); + if (! rxdata.has_flags(POST_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *right); + find_sort_values(rxdata.sort_values, bound_scope); + rxdata.add_flags(POST_EXT_SORT_CALC); + } + + return sort_value_is_less_than(lxdata.sort_values, rxdata.sort_values); +} + +template <> +bool compare_items<account_t>::operator()(account_t * left, account_t * right) +{ + assert(left); + assert(right); + + account_t::xdata_t& lxdata(left->xdata()); + if (! lxdata.has_flags(ACCOUNT_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *left); + find_sort_values(lxdata.sort_values, bound_scope); + lxdata.add_flags(ACCOUNT_EXT_SORT_CALC); + } + + account_t::xdata_t& rxdata(right->xdata()); + if (! rxdata.has_flags(ACCOUNT_EXT_SORT_CALC)) { + bind_scope_t bound_scope(*sort_order.get_context(), *right); + find_sort_values(rxdata.sort_values, bound_scope); + rxdata.add_flags(ACCOUNT_EXT_SORT_CALC); + } + + DEBUG("value.sort", "Comparing accounts " << left->fullname() + << " <> " << right->fullname()); + + return sort_value_is_less_than(lxdata.sort_values, rxdata.sort_values); +} + +} // namespace ledger diff --git a/src/compare.h b/src/compare.h new file mode 100644 index 00000000..740ba275 --- /dev/null +++ b/src/compare.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup data + */ + +/** + * @file compare.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _COMPARE_H +#define _COMPARE_H + +#include "expr.h" + +namespace ledger { + +class post_t; +class account_t; + +void push_sort_value(std::list<sort_value_t>& sort_values, + expr_t::ptr_op_t node, scope_t& scope); + +template <typename T> +class compare_items +{ + expr_t sort_order; + + compare_items(); + +public: + compare_items(const compare_items& other) : sort_order(other.sort_order) { + TRACE_CTOR(compare_items, "copy"); + } + compare_items(const expr_t& _sort_order) : sort_order(_sort_order) { + TRACE_CTOR(compare_items, "const value_expr&"); + } + ~compare_items() throw() { + TRACE_DTOR(compare_items); + } + + void find_sort_values(std::list<sort_value_t>& sort_values, scope_t& scope) { + push_sort_value(sort_values, sort_order.get_op(), scope); + } + + bool operator()(T * left, T * right); +}; + +sort_value_t calc_sort_value(const expr_t::ptr_op_t op); + +template <typename T> +bool compare_items<T>::operator()(T * left, T * right) +{ + assert(left); assert(right); + return sort_value_is_less_than(find_sort_values(left), + find_sort_values(right)); +} + +template <> +bool compare_items<post_t>::operator()(post_t * left, post_t * right); +template <> +bool compare_items<account_t>::operator()(account_t * left, + account_t * right); + +} // namespace ledger + +#endif // _COMPARE_H diff --git a/src/draft.cc b/src/draft.cc new file mode 100644 index 00000000..8478a31d --- /dev/null +++ b/src/draft.cc @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2003-2009, 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 "draft.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "journal.h" +#include "session.h" +#include "report.h" +#include "output.h" + +namespace ledger { + +void draft_t::xact_template_t::dump(std::ostream& out) const +{ + if (date) + out << _("Date: ") << *date << std::endl; + else + out << _("Date: <today>") << std::endl; + + if (code) + out << _("Code: ") << *code << std::endl; + if (note) + out << _("Note: ") << *note << std::endl; + + if (payee_mask.empty()) + out << _("Payee mask: INVALID (template expression will cause an error)") + << std::endl; + else + out << _("Payee mask: ") << payee_mask << std::endl; + + if (posts.empty()) { + out << std::endl + << _("<Posting copied from last related transaction>") + << std::endl; + } else { + bool has_only_from = true; + bool has_only_to = true; + + foreach (const post_template_t& post, posts) { + if (post.from) + has_only_to = false; + else + has_only_from = false; + } + + foreach (const post_template_t& post, posts) { + straccstream accum; + out << std::endl + << ACCUM(accum << _("[Posting \"%1\"]") + << (post.from ? _("from") : _("to"))) + << std::endl; + + if (post.account_mask) + out << _(" Account mask: ") << *post.account_mask << std::endl; + else if (post.from) + out << _(" Account mask: <use last of last related accounts>") << std::endl; + else + out << _(" Account mask: <use first of last related accounts>") << std::endl; + + if (post.amount) + out << _(" Amount: ") << *post.amount << std::endl; + + if (post.cost) + out << _(" Cost: ") << *post.cost_operator + << " " << *post.cost << std::endl; + } + } +} + +void draft_t::parse_args(const value_t& args) +{ + regex date_mask(_("([0-9]+(?:[-/.][0-9]+)?(?:[-/.][0-9]+))?")); + smatch what; + bool check_for_date = true; + + tmpl = xact_template_t(); + + optional<date_time::weekdays> weekday; + xact_template_t::post_template_t * post = NULL; + + value_t::sequence_t::const_iterator begin = args.begin(); + value_t::sequence_t::const_iterator end = args.end(); + + for (; begin != end; begin++) { + if (check_for_date && + regex_match((*begin).to_string(), what, date_mask)) { + tmpl->date = parse_date(what[0]); + check_for_date = false; + } + else if (check_for_date && + bool(weekday = string_to_day_of_week(what[0]))) { + short dow = static_cast<short>(*weekday); + date_t date = CURRENT_DATE() - date_duration(1); + while (date.day_of_week() != dow) + date -= date_duration(1); + tmpl->date = date; + check_for_date = false; + } + else { + string arg = (*begin).to_string(); + + if (arg == "at") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->payee_mask = (*++begin).to_string(); + } + else if (arg == "to" || arg == "from") { + if (! post || post->account_mask) { + tmpl->posts.push_back(xact_template_t::post_template_t()); + post = &tmpl->posts.back(); + } + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + post->account_mask = mask_t((*++begin).to_string()); + post->from = arg == "from"; + } + else if (arg == "on") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->date = parse_date((*++begin).to_string()); + check_for_date = false; + } + else if (arg == "code") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->code = (*++begin).to_string(); + } + else if (arg == "note") { + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + tmpl->note = (*++begin).to_string(); + } + else if (arg == "rest") { + ; // just ignore this argument + } + else if (arg == "@" || arg == "@@") { + amount_t cost; + post->cost_operator = arg; + if (begin == end) + throw std::runtime_error(_("Invalid xact command arguments")); + arg = (*++begin).to_string(); + if (! cost.parse(arg, PARSE_SOFT_FAIL | PARSE_NO_MIGRATE)) + throw std::runtime_error(_("Invalid xact command arguments")); + post->cost = cost; + } + else { + // Without a preposition, it is either: + // + // A payee, if we have not seen one + // An account or an amount, if we have + // An account if an amount has just been seen + // An amount if an account has just been seen + + if (tmpl->payee_mask.empty()) { + tmpl->payee_mask = arg; + } + else { + amount_t amt; + optional<mask_t> account; + + if (! amt.parse(arg, PARSE_SOFT_FAIL | PARSE_NO_MIGRATE)) + account = mask_t(arg); + + if (! post || + (account && post->account_mask) || + (! account && post->amount)) { + tmpl->posts.push_back(xact_template_t::post_template_t()); + post = &tmpl->posts.back(); + } + + if (account) { + post->from = false; + post->account_mask = account; + } else { + post->amount = amt; + } + } + } + } + } + + if (! tmpl->posts.empty()) { + bool has_only_from = true; + bool has_only_to = true; + + // A single account at the end of the line is the "from" account + if (tmpl->posts.size() > 1 && + tmpl->posts.back().account_mask && ! tmpl->posts.back().amount) + tmpl->posts.back().from = true; + + foreach (xact_template_t::post_template_t& post, tmpl->posts) { + if (post.from) + has_only_to = false; + else + has_only_from = false; + } + + if (has_only_from) { + tmpl->posts.push_front(xact_template_t::post_template_t()); + } + else if (has_only_to) { + tmpl->posts.push_back(xact_template_t::post_template_t()); + tmpl->posts.back().from = true; + } + } +} + +xact_t * draft_t::insert(journal_t& journal) +{ + if (tmpl->payee_mask.empty()) + throw std::runtime_error(_("xact' command requires at least a payee")); + + xact_t * matching = NULL; + + std::auto_ptr<xact_t> added(new xact_t); + + for (xacts_list::reverse_iterator j = journal.xacts.rbegin(); + j != journal.xacts.rend(); + j++) { + if (tmpl->payee_mask.match((*j)->payee)) { + matching = *j; + DEBUG("derive.xact", + "Found payee match: transaction on line " << (*j)->pos->beg_line); + break; + } + } + + if (! tmpl->date) { + added->_date = CURRENT_DATE(); + DEBUG("derive.xact", "Setting date to current date"); + } else { + added->_date = tmpl->date; + DEBUG("derive.xact", "Setting date to template date: " << *tmpl->date); + } + + added->set_state(item_t::UNCLEARED); + + if (matching) { + added->payee = matching->payee; + added->code = matching->code; + added->note = matching->note; + +#if defined(DEBUG_ON) + DEBUG("derive.xact", "Setting payee from match: " << added->payee); + if (added->code) + DEBUG("derive.xact", "Setting code from match: " << *added->code); + if (added->note) + DEBUG("derive.xact", "Setting note from match: " << *added->note); +#endif + } else { + added->payee = tmpl->payee_mask.str(); + DEBUG("derive.xact", "Setting payee from template: " << added->payee); + } + + if (tmpl->code) { + added->code = tmpl->code; + DEBUG("derive.xact", "Now setting code from template: " << *added->code); + } + if (tmpl->note) { + added->note = tmpl->note; + DEBUG("derive.xact", "Now setting note from template: " << *added->note); + } + + if (tmpl->posts.empty()) { + if (matching) { + DEBUG("derive.xact", "Template had no postings, copying from match"); + + foreach (post_t * post, matching->posts) { + added->add_post(new post_t(*post)); + added->posts.back()->set_state(item_t::UNCLEARED); + } + } else { + throw_(std::runtime_error, + _("No accounts, and no past transaction matching '%1'") + << tmpl->payee_mask); + } + } else { + DEBUG("derive.xact", "Template had postings"); + + bool any_post_has_amount = false; + foreach (xact_template_t::post_template_t& post, tmpl->posts) { + if (post.amount) { + DEBUG("derive.xact", " and at least one has an amount specified"); + any_post_has_amount = true; + break; + } + } + + foreach (xact_template_t::post_template_t& post, tmpl->posts) { + std::auto_ptr<post_t> new_post; + + commodity_t * found_commodity = NULL; + + if (matching) { + if (post.account_mask) { + DEBUG("derive.xact", + "Looking for matching posting based on account mask"); + + foreach (post_t * x, matching->posts) { + if (post.account_mask->match(x->account->fullname())) { + new_post.reset(new post_t(*x)); + DEBUG("derive.xact", + "Founding posting from line " << x->pos->beg_line); + break; + } + } + } else { + if (post.from) { + for (posts_list::reverse_iterator j = matching->posts.rbegin(); + j != matching->posts.rend(); + j++) { + if ((*j)->must_balance()) { + new_post.reset(new post_t(**j)); + DEBUG("derive.xact", + "Copied last real posting from matching"); + break; + } + } + } else { + for (posts_list::iterator j = matching->posts.begin(); + j != matching->posts.end(); + j++) { + if ((*j)->must_balance()) { + new_post.reset(new post_t(**j)); + DEBUG("derive.xact", + "Copied first real posting from matching"); + break; + } + } + } + } + } + + if (! new_post.get()) { + new_post.reset(new post_t); + DEBUG("derive.xact", "New posting was NULL, creating a blank one"); + } + + if (! new_post->account) { + DEBUG("derive.xact", "New posting still needs an account"); + + if (post.account_mask) { + DEBUG("derive.xact", "The template has an account mask"); + + account_t * acct = NULL; + if (! acct) { + acct = journal.find_account_re(post.account_mask->str()); +#if defined(DEBUG_ON) + if (acct) + DEBUG("derive.xact", "Found account as a regular expression"); +#endif + } + if (! acct) { + acct = journal.find_account(post.account_mask->str()); +#if defined(DEBUG_ON) + if (acct) + DEBUG("derive.xact", "Found (or created) account by name"); +#endif + } + + // Find out the default commodity to use by looking at the last + // commodity used in that account + for (xacts_list::reverse_iterator j = journal.xacts.rbegin(); + j != journal.xacts.rend(); + j++) { + foreach (post_t * x, (*j)->posts) { + if (x->account == acct && ! x->amount.is_null()) { + new_post.reset(new post_t(*x)); + DEBUG("derive.xact", + "Found account in journal postings, setting new posting"); + break; + } + } + } + + new_post->account = acct; + DEBUG("derive.xact", + "Set new posting's account to: " << acct->fullname()); + } else { + if (post.from) { + new_post->account = journal.find_account(_("Liabilities:Unknown")); + DEBUG("derive.xact", + "Set new posting's account to: Liabilities:Unknown"); + } else { + new_post->account = journal.find_account(_("Expenses:Unknown")); + DEBUG("derive.xact", + "Set new posting's account to: Expenses:Unknown"); + } + } + } + assert(new_post->account); + + if (new_post.get() && ! new_post->amount.is_null()) { + found_commodity = &new_post->amount.commodity(); + + if (any_post_has_amount) { + new_post->amount = amount_t(); + DEBUG("derive.xact", "New posting has an amount, but we cleared it"); + } else { + any_post_has_amount = true; + DEBUG("derive.xact", "New posting has an amount, and we're using it"); + } + } + + if (post.amount) { + new_post->amount = *post.amount; + DEBUG("derive.xact", "Copied over posting amount"); + + if (post.from) { + new_post->amount.in_place_negate(); + DEBUG("derive.xact", "Negated new posting amount"); + } + } + + if (post.cost) { + if (post.cost->sign() < 0) + throw parse_error(_("A posting's cost may not be negative")); + + post.cost->in_place_unround(); + + if (*post.cost_operator == "@") { + // For the sole case where the cost might be uncommoditized, + // guarantee that the commodity of the cost after multiplication + // is the same as it was before. + commodity_t& cost_commodity(post.cost->commodity()); + *post.cost *= new_post->amount; + post.cost->set_commodity(cost_commodity); + } + + new_post->cost = *post.cost; + DEBUG("derive.xact", "Copied over posting cost"); + } + + if (found_commodity && + ! new_post->amount.is_null() && + ! new_post->amount.has_commodity()) { + new_post->amount.set_commodity(*found_commodity); + DEBUG("derive.xact", "Set posting amount commodity to: " + << new_post->amount.commodity()); + + new_post->amount = new_post->amount.rounded(); + DEBUG("derive.xact", + "Rounded posting amount to: " << new_post->amount); + } + + added->add_post(new_post.release()); + added->posts.back()->account->add_post(added->posts.back()); + added->posts.back()->set_state(item_t::UNCLEARED); + + DEBUG("derive.xact", "Added new posting to derived entry"); + } + } + + if (! journal.add_xact(added.get())) + throw_(std::runtime_error, + _("Failed to finalize derived transaction (check commodities)")); + + return added.release(); +} + +value_t template_command(call_scope_t& args) +{ + report_t& report(find_scope<report_t>(args)); + std::ostream& out(report.output_stream); + + out << _("--- Input arguments ---") << std::endl; + args.value().dump(out); + out << std::endl << std::endl; + + draft_t draft(args.value()); + + out << _("--- Transaction template ---") << std::endl; + draft.dump(out); + + return true; +} + +value_t xact_command(call_scope_t& args) +{ + report_t& report(find_scope<report_t>(args)); + draft_t draft(args.value()); + + xact_t * new_xact = draft.insert(*report.session.journal.get()); + + // Only consider actual postings for the "xact" command + report.HANDLER(limit_).on(string("#xact"), "actual"); + + report.xact_report(post_handler_ptr + (new format_posts(report, + report.HANDLER(print_format_).str())), + *new_xact); + return true; +} + +} // namespace ledger diff --git a/src/draft.h b/src/draft.h new file mode 100644 index 00000000..93e98ff5 --- /dev/null +++ b/src/draft.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup expr + */ + +/** + * @file draft.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _DRAFT_H +#define _DRAFT_H + +#include "exprbase.h" +#include "value.h" + +namespace ledger { + +class journal_t; +class xact_t; + +class draft_t : public expr_base_t<value_t> +{ + typedef expr_base_t<value_t> base_type; + + struct xact_template_t + { + optional<date_t> date; + optional<string> code; + optional<string> note; + mask_t payee_mask; + + struct post_template_t { + bool from; + optional<mask_t> account_mask; + optional<amount_t> amount; + optional<string> cost_operator; + optional<amount_t> cost; + + post_template_t() : from(false) {} + }; + + std::list<post_template_t> posts; + + xact_template_t() {} + + void dump(std::ostream& out) const; + }; + + optional<xact_template_t> tmpl; + +public: + draft_t(const value_t& args) : base_type() { + TRACE_CTOR(draft_t, "value_t"); + if (! args.empty()) + parse_args(args); + } + virtual ~draft_t() { + TRACE_DTOR(draft_t); + } + + void parse_args(const value_t& args); + + virtual result_type real_calc(scope_t&) { + assert(false); + return true; + } + + xact_t * insert(journal_t& journal); + + virtual void dump(std::ostream& out) const { + if (tmpl) + tmpl->dump(out); + } +}; + +value_t xact_command(call_scope_t& args); +value_t template_command(call_scope_t& args); + +} // namespace ledger + +#endif // _DRAFT_H diff --git a/src/emacs.cc b/src/emacs.cc new file mode 100644 index 00000000..dc1a18ae --- /dev/null +++ b/src/emacs.cc @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2003-2009, 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 "emacs.h" +#include "xact.h" +#include "post.h" +#include "account.h" + +namespace ledger { + +void format_emacs_posts::write_xact(xact_t& xact) +{ + out << "\"" << xact.pos->pathname << "\" " + << xact.pos->beg_line << " "; + + tm when = gregorian::to_tm(xact.date()); + std::time_t date = std::mktime(&when); + + out << "(" << (date / 65536) << " " << (date % 65536) << " 0) "; + + if (! xact.code) + out << "nil "; + else + out << "\"" << *xact.code << "\" "; + + if (xact.payee.empty()) + out << "nil"; + else + out << "\"" << xact.payee << "\""; + + out << "\n"; +} + +void format_emacs_posts::operator()(post_t& post) +{ + if (! post.has_xdata() || + ! post.xdata().has_flags(POST_EXT_DISPLAYED)) { + if (! last_xact) { + out << "(("; + write_xact(*post.xact); + } + else if (post.xact != last_xact) { + out << ")\n ("; + write_xact(*post.xact); + } + else { + out << "\n"; + } + + out << " (" << post.pos->beg_line << " "; + out << "\"" << post.reported_account()->fullname() << "\" \"" + << post.amount << "\""; + + switch (post.state()) { + case item_t::CLEARED: + out << " t"; + break; + case item_t::PENDING: + out << " pending"; + break; + default: + out << " nil"; + break; + } + + if (post.cost) + out << " \"" << *post.cost << "\""; + if (post.note) + out << " \"" << *post.note << "\""; + out << ")"; + + last_xact = post.xact; + + post.xdata().add_flags(POST_EXT_DISPLAYED); + } +} + +} // namespace ledger diff --git a/src/emacs.h b/src/emacs.h new file mode 100644 index 00000000..4b2a452a --- /dev/null +++ b/src/emacs.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file emacs.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _EMACS_H +#define _EMACS_H + +#include "chain.h" + +namespace ledger { + +class xact_t; + +class format_emacs_posts : public item_handler<post_t> +{ + format_emacs_posts(); + +protected: + std::ostream& out; + xact_t * last_xact; + +public: + format_emacs_posts(std::ostream& _out) + : out(_out), last_xact(NULL) { + TRACE_CTOR(format_emacs_posts, "std::ostream&"); + } + ~format_emacs_posts() { + TRACE_DTOR(format_emacs_posts); + } + + virtual void write_xact(xact_t& xact); + virtual void flush() { + if (last_xact) + out << "))\n"; + out.flush(); + } + virtual void operator()(post_t& post); +}; + +} // namespace ledger + +#endif // _REPORT_H diff --git a/src/error.cc b/src/error.cc new file mode 100644 index 00000000..d5abe4de --- /dev/null +++ b/src/error.cc @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2003-2009, 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 "utils.h" + +namespace ledger { + +straccstream _ctxt_accum; +std::ostringstream _ctxt_buffer; +straccstream _desc_accum; +std::ostringstream _desc_buffer; + +string error_context() +{ + string context = _ctxt_buffer.str(); + _ctxt_buffer.str(""); + return context; +} + +string file_context(const path& file, const std::size_t line) +{ + std::ostringstream buf; + buf << "\"" << file << "\", line " << line << ": "; + return buf.str(); +} + +string line_context(const string& line, + const string::size_type pos, + const string::size_type end_pos) +{ + std::ostringstream buf; + buf << " " << line << "\n"; + + if (pos != 0) { + buf << " "; + if (end_pos == 0) { + for (string::size_type i = 0; i < pos; i += 1) + buf << " "; + buf << "^"; + } else { + for (string::size_type i = 0; i < end_pos; i += 1) { + if (i >= pos) + buf << "^"; + else + buf << " "; + } + } + } + return buf.str(); +} + +string source_context(const path& file, + const istream_pos_type pos, + const istream_pos_type end_pos, + const string& prefix) +{ + const std::streamoff len = end_pos - pos; + if (! len || file == path("/dev/stdin")) + return _("<no source context>"); + + assert(len > 0); + assert(len < 2048); + + std::ostringstream out; + + ifstream in(file); + in.seekg(pos, std::ios::beg); + + scoped_array<char> buf(new char[static_cast<std::size_t>(len) + 1]); + in.read(buf.get(), static_cast<std::streamsize>(len)); + assert(in.gcount() == static_cast<std::streamsize>(len)); + buf[static_cast<std::size_t>(len)] = '\0'; + + bool first = true; + for (char * p = std::strtok(buf.get(), "\n"); + p; + p = std::strtok(NULL, "\n")) { + if (first) + first = false; + else + out << '\n'; + out << prefix << p; + } + + return out.str(); +} + +} // namespace ledger diff --git a/src/error.h b/src/error.h new file mode 100644 index 00000000..5369faf1 --- /dev/null +++ b/src/error.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file error.h + * @author John Wiegley + * + * @ingroup util + */ +#ifndef _ERROR_H +#define _ERROR_H + +#include "accum.h" + +namespace ledger { + +extern straccstream _desc_accum; +extern std::ostringstream _desc_buffer; + +template <typename T> +inline void throw_func(const string& message) { + _desc_buffer.str(""); + throw T(message); +} + +#define throw_(cls, msg) \ + ((_desc_buffer << ACCUM(_desc_accum << msg)), \ + _desc_accum.clear(), \ + throw_func<cls>(_desc_buffer.str())) + +inline void warning_func(const string& message) { + std::cerr << "Warning: " << message << std::endl; + _desc_buffer.str(""); +} + +#define warning_(msg) \ + ((_desc_buffer << ACCUM(_desc_accum << msg)), \ + _desc_accum.clear(), \ + warning_func(_desc_buffer.str())) + +extern straccstream _ctxt_accum; +extern std::ostringstream _ctxt_buffer; + +#define add_error_context(msg) \ + ((long(_ctxt_buffer.tellp()) == 0) ? \ + ((_ctxt_buffer << ACCUM(_ctxt_accum << msg)), \ + _ctxt_accum.clear()) : \ + ((_ctxt_buffer << std::endl << ACCUM(_ctxt_accum << msg)), \ + _ctxt_accum.clear())) + +string error_context(); + +string file_context(const path& file, std::size_t line); +string line_context(const string& line, + const string::size_type pos = 0, + const string::size_type end_pos = 0); + +string source_context(const path& file, + const istream_pos_type pos, + const istream_pos_type end_pos, + const string& prefix = ""); + +#define DECLARE_EXCEPTION(name, kind) \ + class name : public kind { \ + public: \ + explicit name(const string& why) throw() : kind(why) {} \ + virtual ~name() throw() {} \ + } + +} // namespace ledger + +#endif // _ERROR_H diff --git a/src/expr.cc b/src/expr.cc new file mode 100644 index 00000000..c59f8a57 --- /dev/null +++ b/src/expr.cc @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2003-2009, 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 "expr.h" +#include "parser.h" + +namespace ledger { + +void expr_t::parse(std::istream& in, const parse_flags_t& flags, + const optional<string>& original_string) +{ + base_type::parse(in, flags, original_string); + + parser_t parser; + ptr = parser.parse(in, flags, original_string); +} + +void expr_t::compile(scope_t& scope) +{ + if (! compiled && ptr) { + ptr = ptr->compile(scope); + base_type::compile(scope); + } +} + +value_t expr_t::real_calc(scope_t& scope) +{ + if (ptr) { + ptr_op_t locus; + try { + return ptr->calc(scope, &locus); + } + catch (const std::exception& err) { + if (locus) { + add_error_context(_("While evaluating value expression:")); + add_error_context(op_context(ptr, locus)); + + if (SHOW_INFO()) { + add_error_context(_("The value expression tree was:")); + std::ostringstream buf; + ptr->dump(buf, 0); + + std::istringstream in(buf.str()); + std::ostringstream out; + char linebuf[1024]; + bool first = true; + while (in.good() && ! in.eof()) { + in.getline(linebuf, 1023); + std::streamsize len = in.gcount(); + if (len > 0) { + if (first) + first = false; + else + out << '\n'; + out << " " << linebuf; + } + } + add_error_context(out.str()); + } + } + throw; + } + } + return NULL_VALUE; +} + +bool expr_t::is_constant() const +{ + assert(compiled); + return ptr && ptr->is_value(); +} + +bool expr_t::is_function() const +{ + assert(compiled); + return ptr && ptr->is_function(); +} + +value_t& expr_t::constant_value() +{ + assert(is_constant()); + return ptr->as_value_lval(); +} + +const value_t& expr_t::constant_value() const +{ + assert(is_constant()); + return ptr->as_value(); +} + +expr_t::func_t& expr_t::get_function() +{ + assert(is_function()); + return ptr->as_function_lval(); +} + +string expr_t::context_to_str() const +{ + return ptr ? op_context(ptr) : _("<empty expression>"); +} + +void expr_t::print(std::ostream& out) const +{ + if (ptr) + ptr->print(out); +} + +void expr_t::dump(std::ostream& out) const +{ + if (ptr) ptr->dump(out, 0); +} + +} // namespace ledger diff --git a/src/expr.h b/src/expr.h new file mode 100644 index 00000000..638855b1 --- /dev/null +++ b/src/expr.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup expr + */ + +/** + * @file expr.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _EXPR_H +#define _EXPR_H + +#include "exprbase.h" +#include "value.h" + +namespace ledger { + +class expr_t : public expr_base_t<value_t> +{ + struct token_t; + class parser_t; + + typedef expr_base_t<value_t> base_type; + +public: + class op_t; + typedef intrusive_ptr<op_t> ptr_op_t; + typedef intrusive_ptr<const op_t> const_ptr_op_t; + +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(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); + } + + virtual ~expr_t() { + TRACE_DTOR(expr_t); + } + + expr_t& operator=(const expr_t& _expr) { + if (this != &_expr) { + base_type::operator=(_expr); + ptr = _expr.ptr; + } + return *this; + } + + virtual operator bool() const throw() { + return ptr.get() != NULL; + } + + ptr_op_t get_op() throw() { + return ptr; + } + + void parse(const string& str, const parse_flags_t& flags = PARSE_DEFAULT) { + std::istringstream stream(str); + return parse(stream, flags, str); + } + + virtual void parse(std::istream& in, + const parse_flags_t& flags = PARSE_DEFAULT, + const optional<string>& original_string = none); + virtual void compile(scope_t& scope); + virtual value_t real_calc(scope_t& scope); + + bool is_constant() const; + value_t& constant_value(); + const value_t& constant_value() const; + bool is_function() const; + func_t& get_function(); + + virtual string context_to_str() const; + virtual void print(std::ostream& out) const; + virtual void dump(std::ostream& out) const; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<base_type>(*this); + ar & ptr; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +} // namespace ledger + +#endif // _EXPR_H diff --git a/src/exprbase.h b/src/exprbase.h new file mode 100644 index 00000000..0b3466b0 --- /dev/null +++ b/src/exprbase.h @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup expr + */ + +/** + * @file exprbase.h + * @author John Wiegley + * + * @ingroup expr + * + * This class provides basic behavior for all the domain specific expression + * languages used in Leger: + * + * | Typename | Description | result_type | Derives | + * |-------------+----------------------------+-----------------+-------------| + * | expr_t | Value expressions | value_t | | + * | predicate_t | Special form of expr_t | bool | expr_t | + * | query_t | Report queries | bool | predicate_t | + * | period_t | Time periods and durations | date_interval_t | | + * | draft_t | Partially filled xacts | xact_t * | | + * | format_t | Format strings | string | | + */ +#ifndef _EXPRBASE_H +#define _EXPRBASE_H + +#include "utils.h" +#include "amount.h" + +namespace ledger { + +DECLARE_EXCEPTION(parse_error, std::runtime_error); +DECLARE_EXCEPTION(compile_error, std::runtime_error); +DECLARE_EXCEPTION(calc_error, std::runtime_error); +DECLARE_EXCEPTION(usage_error, std::runtime_error); + +class scope_t; +class call_scope_t; + +template <typename ResultType> +class expr_base_t +{ +public: + typedef ResultType result_type; + + typedef function<result_type (call_scope_t&)> func_t; + +protected: + scope_t * context; + string str; + bool compiled; + + virtual result_type real_calc(scope_t& scope) = 0; + +public: + expr_base_t(const expr_base_t& other) + : context(other.context), str(other.str), compiled(false) { + TRACE_CTOR(expr_base_t, "copy"); + } + expr_base_t(scope_t * _context = NULL) + : context(_context), compiled(false) + { + TRACE_CTOR(expr_base_t, "scope_t *"); + } + virtual ~expr_base_t() { + TRACE_DTOR(expr_base_t); + } + + expr_base_t& operator=(const expr_base_t& _expr) { + if (this != &_expr) { + str = _expr.str; + context = _expr.context; + compiled = _expr.compiled; + } + return *this; + } + expr_base_t& operator=(const string& _expr) { + parse(_expr); + return *this; + } + + virtual operator bool() const throw() { + return ! str.empty(); + } + + virtual string text() { + return str; + } + void set_text(const string& txt) { + str = txt; + compiled = false; + } + + void parse(const string& str, const parse_flags_t& flags = PARSE_DEFAULT) { + std::istringstream stream(str); + return parse(stream, flags, str); + } + virtual void parse(std::istream&, + const parse_flags_t& = PARSE_DEFAULT, + const optional<string>& original_string = none) { + set_text(original_string ? *original_string : "<stream>"); + } + + void mark_uncompiled() { + compiled = false; + } + + void recompile(scope_t& scope) { + compiled = false; + compile(scope); + } + + virtual void compile(scope_t& scope) { + if (! compiled) { + // Derived classes need to do something here. + context = &scope; + compiled = true; + } + } + + result_type operator()(scope_t& scope) { + return calc(scope); + } + + result_type calc(scope_t& scope) + { + if (! compiled) { + if (SHOW_DEBUG("expr.compile")) { + DEBUG("expr.compile", "Before compilation:"); + dump(*_log_stream); + } + + compile(scope); + + if (SHOW_DEBUG("expr.compile")) { + DEBUG("expr.compile", "After compilation:"); + dump(*_log_stream); + } + } + + return real_calc(scope); + } + + result_type calc() { + assert(context); + return calc(*context); + } + + scope_t * get_context() { + return context; + } + void set_context(scope_t * scope) { + context = scope; + } + + virtual string context_to_str() const { + return empty_string; + } + + string print_to_str() const { + std::ostringstream out; + print(out); + return out.str(); + } + string dump_to_str() const { + std::ostringstream out; + dump(out); + return out.str(); + } + string preview_to_str(scope_t& scope) const { + std::ostringstream out; + preview(out); + return out.str(); + } + + virtual void print(std::ostream&) const {} + virtual void dump(std::ostream&) const {} + + result_type preview(std::ostream& out, scope_t& scope) const { + out << _("--- Input expression ---") << std::endl; + out << text() << std::endl; + + out << std::endl << _("--- Text as parsed ---") << std::endl; + print(out); + out << std::endl; + + out << std::endl << _("--- Expression tree ---") << std::endl; + dump(out); + + out << std::endl << _("--- Compiled tree ---") << std::endl; + compile(scope); + dump(out); + + out << std::endl << _("--- Result value ---") << std::endl; + return calc(); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & context; + ar & str; + if (Archive::is_loading::value) + compiled = false; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +template <typename ResultType> +std::ostream& operator<<(std::ostream& out, + const expr_base_t<ResultType>& expr) { + expr.print(out); + return out; +} + +} // namespace ledger + +#endif // _EXPRBASE_H diff --git a/src/filters.cc b/src/filters.cc new file mode 100644 index 00000000..0084fac7 --- /dev/null +++ b/src/filters.cc @@ -0,0 +1,1055 @@ +/* + * Copyright (c) 2003-2009, 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 "filters.h" +#include "iterators.h" +#include "journal.h" +#include "report.h" +#include "compare.h" + +namespace ledger { + +pass_down_posts::pass_down_posts(post_handler_ptr handler, + posts_iterator& iter) + : item_handler<post_t>(handler) +{ + TRACE_CTOR(pass_down_posts, "post_handler_ptr, posts_iterator"); + + for (post_t * post = iter(); post; post = iter()) { + try { + item_handler<post_t>::operator()(*post); + } + catch (const std::exception& err) { + add_error_context(item_context(*post, _("While handling posting"))); + throw; + } + } + + item_handler<post_t>::flush(); +} + +void truncate_xacts::flush() +{ + if (! posts.size()) + return; + + xact_t * xact = (*posts.begin())->xact; + + int l = 0; + foreach (post_t * post, posts) + if (xact != post->xact) { + l++; + xact = post->xact; + } + l++; + + xact = (*posts.begin())->xact; + + int i = 0; + foreach (post_t * post, posts) { + if (xact != post->xact) { + xact = post->xact; + i++; + } + + bool print = false; + if (head_count) { + if (head_count > 0 && i < head_count) + print = true; + else if (head_count < 0 && i >= - head_count) + print = true; + } + + if (! print && tail_count) { + if (tail_count > 0 && l - i <= tail_count) + print = true; + else if (tail_count < 0 && l - i > - tail_count) + print = true; + } + + if (print) + item_handler<post_t>::operator()(*post); + } + posts.clear(); + + item_handler<post_t>::flush(); +} + +void truncate_xacts::operator()(post_t& post) +{ + if (last_xact != post.xact) { + if (last_xact) + xacts_seen++; + last_xact = post.xact; + } + + if (tail_count == 0 && head_count > 0 && + static_cast<int>(xacts_seen) >= head_count) + return; + + posts.push_back(&post); +} + +void sort_posts::post_accumulated_posts() +{ + std::stable_sort(posts.begin(), posts.end(), + compare_items<post_t>(sort_order)); + + foreach (post_t * post, posts) { + post->xdata().drop_flags(POST_EXT_SORT_CALC); + item_handler<post_t>::operator()(*post); + } + + posts.clear(); +} + +namespace { + void split_string(const string& str, const char ch, + std::list<string>& strings) + { + const char * b = str.c_str(); + for (const char * p = b; *p; p++) { + if (*p == ch) { + strings.push_back(string(b, p - b)); + b = p + 1; + } + } + strings.push_back(string(b)); + } + + account_t * create_temp_account_from_path(std::list<string>& account_names, + temporaries_t& temps, + account_t * master) + { + account_t * new_account = NULL; + foreach (const string& name, account_names) { + if (new_account) { + new_account = new_account->find_account(name); + } else { + new_account = master->find_account(name, false); + if (! new_account) + new_account = &temps.create_account(name, master); + } + } + + assert(new_account != NULL); + return new_account; + } +} + +void anonymize_posts::operator()(post_t& post) +{ + SHA1 sha; + uint_least32_t message_digest[5]; + bool copy_xact_details = false; + + if (last_xact != post.xact) { + temps.copy_xact(*post.xact); + last_xact = post.xact; + copy_xact_details = true; + } + xact_t& xact = temps.last_xact(); + + if (copy_xact_details) { + xact.copy_details(*post.xact); + + sha.Reset(); + sha << post.xact->payee.c_str(); + sha.Result(message_digest); + + xact.payee = to_hex(message_digest); + xact.note = none; + } + + std::list<string> account_names; + + for (account_t * acct = post.account; + acct; + acct = acct->parent) { + sha.Reset(); + sha << acct->name.c_str(); + sha.Result(message_digest); + + account_names.push_front(to_hex(message_digest)); + } + + account_t * new_account = + create_temp_account_from_path(account_names, temps, xact.journal->master); + post_t& temp = temps.copy_post(post, xact, new_account); + temp.note = none; + + (*handler)(temp); +} + +void calc_posts::operator()(post_t& post) +{ + post_t::xdata_t& xdata(post.xdata()); + + if (last_post) { + assert(last_post->has_xdata()); + if (calc_running_total) + xdata.total = last_post->xdata().total; + xdata.count = last_post->xdata().count + 1; + } else { + xdata.count = 1; + } + + post.add_to_value(xdata.visited_value, amount_expr); + xdata.add_flags(POST_EXT_VISITED); + + account_t * acct = post.reported_account(); + acct->xdata().add_flags(ACCOUNT_EXT_VISITED); + + if (calc_running_total) + add_or_set_value(xdata.total, xdata.visited_value); + + item_handler<post_t>::operator()(post); + + last_post = &post; +} + +namespace { + typedef function<void (post_t&)> post_functor_t; + + void handle_value(const value_t& value, + account_t * account, + xact_t * xact, + temporaries_t& temps, + post_handler_ptr handler, + const date_t& date = date_t(), + const value_t& total = value_t(), + const bool direct_amount = false, + const bool mark_visited = false, + const optional<post_functor_t>& functor = none) + { + post_t& post = temps.create_post(*xact, account); + post.add_flags(ITEM_GENERATED); + + // If the account for this post is all virtual, then report the post as + // such. This allows subtotal reports to show "(Account)" for accounts + // that contain only virtual posts. + if (account && account->has_xdata() && + account->xdata().has_flags(ACCOUNT_EXT_AUTO_VIRTUALIZE)) { + if (! account->xdata().has_flags(ACCOUNT_EXT_HAS_NON_VIRTUALS)) { + post.add_flags(POST_VIRTUAL); + if (! account->xdata().has_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS)) + post.add_flags(POST_MUST_BALANCE); + } + } + + post_t::xdata_t& xdata(post.xdata()); + + if (is_valid(date)) + xdata.date = date; + + value_t temp(value); + + switch (value.type()) { + case value_t::BOOLEAN: + case value_t::INTEGER: + temp.in_place_cast(value_t::AMOUNT); + // fall through... + + case value_t::AMOUNT: + post.amount = temp.as_amount(); + break; + + case value_t::BALANCE: + case value_t::SEQUENCE: + xdata.compound_value = temp; + xdata.add_flags(POST_EXT_COMPOUND); + break; + + case value_t::DATETIME: + case value_t::DATE: + default: + assert(false); + break; + } + + if (! total.is_null()) + xdata.total = total; + + if (direct_amount) + xdata.add_flags(POST_EXT_DIRECT_AMT); + + if (functor) + (*functor)(post); + + DEBUG("filter.changed_value.rounding", "post.amount = " << post.amount); + + (*handler)(post); + + if (mark_visited) { + post.xdata().add_flags(POST_EXT_VISITED); + post.account->xdata().add_flags(ACCOUNT_EXT_VISITED); + } + } +} + +void collapse_posts::report_subtotal() +{ + if (! count) + return; + + std::size_t displayed_count = 0; + foreach (post_t * post, component_posts) { + if (only_predicate(*post) && display_predicate(*post)) + displayed_count++; + } + + if (displayed_count == 1) { + item_handler<post_t>::operator()(*last_post); + } + else if (only_collapse_if_zero && ! subtotal.is_zero()) { + foreach (post_t * post, component_posts) + item_handler<post_t>::operator()(*post); + } + else { + date_t earliest_date; + + foreach (post_t * post, component_posts) { + date_t reported = post->date(); + if (! is_valid(earliest_date) || + reported < earliest_date) + earliest_date = reported; + } + + xact_t& xact = temps.create_xact(); + xact.payee = last_xact->payee; + xact._date = (is_valid(earliest_date) ? + earliest_date : last_xact->_date); + DEBUG("filter.collapse", "Pseudo-xact date = " << *xact._date); + + handle_value(subtotal, &totals_account, &xact, temps, handler); + } + + component_posts.clear(); + + last_xact = NULL; + last_post = NULL; + subtotal = 0L; + count = 0; +} + +void collapse_posts::operator()(post_t& post) +{ + // If we've reached a new xact, report on the subtotal + // accumulated thus far. + + if (last_xact != post.xact && count > 0) + report_subtotal(); + + post.add_to_value(subtotal, amount_expr); + + component_posts.push_back(&post); + + last_xact = post.xact; + last_post = &post; + count++; +} + +void related_posts::flush() +{ + if (posts.size() > 0) { + foreach (post_t * post, posts) { + if (post->xact) { + foreach (post_t * r_post, post->xact->posts) { + post_t::xdata_t& xdata(r_post->xdata()); + if (! xdata.has_flags(POST_EXT_HANDLED) && + (! xdata.has_flags(POST_EXT_RECEIVED) ? + ! r_post->has_flags(ITEM_GENERATED | POST_VIRTUAL) : + also_matching)) { + xdata.add_flags(POST_EXT_HANDLED); + item_handler<post_t>::operator()(*r_post); + } + } + } else { + // This code should only be reachable from the "output" + // command, since that is the only command which attempts to + // output auto or period xacts. + post_t::xdata_t& xdata(post->xdata()); + if (! xdata.has_flags(POST_EXT_HANDLED) && + ! post->has_flags(ITEM_GENERATED)) { + xdata.add_flags(POST_EXT_HANDLED); + item_handler<post_t>::operator()(*post); + } + } + } + } + + item_handler<post_t>::flush(); +} + +changed_value_posts::changed_value_posts(post_handler_ptr handler, + report_t& _report, + bool _for_accounts_report, + bool _show_unrealized) + : item_handler<post_t>(handler), report(_report), + for_accounts_report(_for_accounts_report), + show_unrealized(_show_unrealized), last_post(NULL), + revalued_account(temps.create_account(_("<Revalued>"))), + rounding_account(temps.create_account(_("<Rounding>"))) +{ + TRACE_CTOR(changed_value_posts, "post_handler_ptr, report_t&, bool"); + + display_amount_expr = report.HANDLER(display_amount_).expr; + total_expr = (report.HANDLED(revalued_total_) ? + report.HANDLER(revalued_total_).expr : + report.HANDLER(display_total_).expr); + display_total_expr = report.HANDLER(display_total_).expr; + changed_values_only = report.HANDLED(revalued_only); + + string gains_equity_account_name; + if (report.HANDLED(unrealized_gains_)) + gains_equity_account_name = report.HANDLER(unrealized_gains_).str(); + else + gains_equity_account_name = _("Equity:Unrealized Gains"); + gains_equity_account = + report.session.journal->master->find_account(gains_equity_account_name); + gains_equity_account->add_flags(ACCOUNT_GENERATED); + + string losses_equity_account_name; + if (report.HANDLED(unrealized_losses_)) + losses_equity_account_name = report.HANDLER(unrealized_losses_).str(); + else + losses_equity_account_name = _("Equity:Unrealized Losses"); + losses_equity_account = + report.session.journal->master->find_account(losses_equity_account_name); + losses_equity_account->add_flags(ACCOUNT_GENERATED); +} + +void changed_value_posts::flush() +{ + if (last_post && last_post->date() <= report.terminus.date()) { + output_revaluation(*last_post, report.terminus.date()); + last_post = NULL; + } + item_handler<post_t>::flush(); +} + +void changed_value_posts::output_revaluation(post_t& post, const date_t& date) +{ + if (is_valid(date)) + post.xdata().date = date; + + value_t repriced_total; + try { + bind_scope_t bound_scope(report, post); + repriced_total = total_expr.calc(bound_scope); + } + catch (...) { + post.xdata().date = date_t(); + throw; + } + post.xdata().date = date_t(); + + DEBUG("filter.changed_value", + "output_revaluation(last_balance) = " << last_total); + DEBUG("filter.changed_value", + "output_revaluation(repriced_total) = " << repriced_total); + + if (! last_total.is_null()) { + if (value_t diff = repriced_total - last_total) { + DEBUG("filter.changed_value", "output_revaluation(strip(diff)) = " + << diff.strip_annotations(report.what_to_keep())); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Commodities revalued"); + xact._date = is_valid(date) ? date : post.date(); + + if (! for_accounts_report) { + handle_value + (/* value= */ diff, + /* account= */ &revalued_account, + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ *xact._date, + /* total= */ repriced_total, + /* direct_amount= */ false, + /* mark_visited= */ false, + /* functor= */ (optional<post_functor_t> + (bind(&changed_value_posts::output_rounding, + this, _1)))); + } + else if (show_unrealized) { + handle_value + (/* value= */ - diff, + /* account= */ (diff < 0L ? + losses_equity_account : + gains_equity_account), + /* xact= */ &xact, + /* temps= */ temps, + /* handler= */ handler, + /* date= */ *xact._date, + /* total= */ value_t(), + /* direct_amount= */ false, + /* mark_visited= */ true); + } + } + } +} + +void changed_value_posts::output_rounding(post_t& post) +{ + bind_scope_t bound_scope(report, post); + value_t new_display_total(display_total_expr.calc(bound_scope)); + + DEBUG("filter.changed_value.rounding", + "rounding.new_display_total = " << new_display_total); + + if (! last_display_total.is_null()) { + if (value_t repriced_amount = display_amount_expr.calc(bound_scope)) { + DEBUG("filter.changed_value.rounding", + "rounding.repriced_amount = " << repriced_amount); + + value_t precise_display_total(new_display_total.truncated() - + repriced_amount.truncated()); + + DEBUG("filter.changed_value.rounding", + "rounding.precise_display_total = " << precise_display_total); + DEBUG("filter.changed_value.rounding", + "rounding.last_display_total = " << last_display_total); + + if (value_t diff = precise_display_total - last_display_total) { + DEBUG("filter.changed_value.rounding", + "rounding.diff = " << diff); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Commodity rounding"); + xact._date = post.date(); + + handle_value(diff, &rounding_account, &xact, temps, handler, + *xact._date, precise_display_total, true); + } + } + } + last_display_total = new_display_total; +} + +void changed_value_posts::operator()(post_t& post) +{ + if (last_post) + output_revaluation(*last_post, post.date()); + + if (changed_values_only) + post.xdata().add_flags(POST_EXT_DISPLAYED); + + if (! for_accounts_report) + output_rounding(post); + + item_handler<post_t>::operator()(post); + + bind_scope_t bound_scope(report, post); + last_total = total_expr.calc(bound_scope); + last_post = &post; +} + +void subtotal_posts::report_subtotal(const char * spec_fmt, + const optional<date_interval_t>& interval) +{ + if (component_posts.empty()) + return; + + optional<date_t> range_start = interval ? interval->start : none; + optional<date_t> range_finish = interval ? interval->inclusive_end() : none; + + if (! range_start || ! range_finish) { + foreach (post_t * post, component_posts) { + date_t date = post->date(); + if (! range_start || date < *range_start) + range_start = date; + if (! range_finish || date > *range_finish) + range_finish = date; + } + } + component_posts.clear(); + + std::ostringstream out_date; + if (spec_fmt) { + out_date << format_date(*range_finish, FMT_CUSTOM, spec_fmt); + } + else if (date_format) { + out_date << "- " << format_date(*range_finish, FMT_CUSTOM, + date_format->c_str()); + } + else { + out_date << "- " << format_date(*range_finish); + } + + xact_t& xact = temps.create_xact(); + xact.payee = out_date.str(); + xact._date = *range_start; + + foreach (values_map::value_type& pair, values) + handle_value(pair.second.value, pair.second.account, &xact, temps, + handler); + + values.clear(); +} + +void subtotal_posts::operator()(post_t& post) +{ + component_posts.push_back(&post); + + account_t * acct = post.reported_account(); + assert(acct); + + 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))); + assert(result.second); + } else { + post.add_to_value((*i).second.value, amount_expr); + } + + // If the account for this post is all virtual, mark it as + // such, so that `handle_value' can show "(Account)" for accounts + // that contain only virtual posts. + + post.reported_account()->xdata().add_flags(ACCOUNT_EXT_AUTO_VIRTUALIZE); + + if (! post.has_flags(POST_VIRTUAL)) + post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_NON_VIRTUALS); + else if (! post.has_flags(POST_MUST_BALANCE)) + post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS); +} + +void interval_posts::report_subtotal(const date_interval_t& interval) +{ + if (last_post && interval) { + if (exact_periods) + subtotal_posts::report_subtotal(); + else + subtotal_posts::report_subtotal(NULL, interval); + } + + last_post = NULL; +} + +void interval_posts::operator()(post_t& post) +{ + date_t date = post.date(); + + if (! interval.find_period(post.date())) + return; + + if (interval.duration) { + if (last_interval && interval != last_interval) { + report_subtotal(last_interval); + + 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(); + + post_t& null_post = temps.create_post(null_xact, &empty_account); + null_post.add_flags(POST_CALCULATED); + null_post.amount = 0L; + + last_post = &null_post; + subtotal_posts::operator()(null_post); + + report_subtotal(last_interval); + } + assert(interval == last_interval); + } else { + last_interval = interval; + } + } else { + last_interval = interval; + } + subtotal_posts::operator()(post); + } else { + item_handler<post_t>::operator()(post); + } + + last_post = &post; +} + +void posts_as_equity::report_subtotal() +{ + date_t finish; + foreach (post_t * post, component_posts) { + date_t date = post->date(); + if (! is_valid(finish) || date > finish) + finish = date; + } + component_posts.clear(); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Opening Balances"); + xact._date = finish; + + value_t total = 0L; + foreach (values_map::value_type& pair, values) { + if (pair.second.value.is_balance()) { + foreach (balance_t::amounts_map::value_type amount_pair, + pair.second.value.as_balance().amounts) + handle_value(amount_pair.second, pair.second.account, &xact, temps, + handler); + } else { + handle_value(pair.second.value, pair.second.account, &xact, temps, + handler); + } + total += pair.second.value; + } + values.clear(); + + if (total.is_balance()) { + foreach (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); + } +} + +void by_payee_posts::flush() +{ + foreach (payee_subtotals_map::value_type& pair, payee_subtotals) + pair.second->report_subtotal(pair.first.c_str()); + + item_handler<post_t>::flush(); + + payee_subtotals.clear(); +} + +void by_payee_posts::operator()(post_t& post) +{ + payee_subtotals_map::iterator i = payee_subtotals.find(post.xact->payee); + if (i == payee_subtotals.end()) { + payee_subtotals_pair + temp(post.xact->payee, + shared_ptr<subtotal_posts>(new subtotal_posts(handler, amount_expr))); + std::pair<payee_subtotals_map::iterator, bool> result + = payee_subtotals.insert(temp); + + assert(result.second); + if (! result.second) + return; + i = result.first; + } + + (*(*i).second)(post); +} + +void transfer_details::operator()(post_t& post) +{ + xact_t& xact = temps.copy_xact(*post.xact); + xact._date = post.date(); + + post_t& temp = temps.copy_post(post, xact); + temp.set_state(post.state()); + + bind_scope_t bound_scope(scope, temp); + value_t substitute(expr.calc(bound_scope)); + + switch (which_element) { + case SET_DATE: + temp.xdata().date = substitute.to_date(); + break; + + case SET_ACCOUNT: { + std::list<string> account_names; + temp.account->remove_post(&temp); + split_string(substitute.to_string(), ':', account_names); + temp.account = create_temp_account_from_path(account_names, temps, + xact.journal->master); + temp.account->add_post(&temp); + break; + } + + case SET_PAYEE: + xact.payee = substitute.to_string(); + break; + + default: + assert(false); + break; + } + + item_handler<post_t>::operator()(temp); +} + +void dow_posts::flush() +{ + for (int i = 0; i < 7; i++) { + foreach (post_t * post, days_of_the_week[i]) + subtotal_posts::operator()(*post); + subtotal_posts::report_subtotal("%As"); + days_of_the_week[i].clear(); + } + + subtotal_posts::flush(); +} + +void generate_posts::add_period_xacts(period_xacts_list& period_xacts) +{ + foreach (period_xact_t * xact, period_xacts) + foreach (post_t * post, xact->posts) + add_post(xact->period, *post); +} + +void generate_posts::add_post(const date_interval_t& period, post_t& post) +{ + pending_posts.push_back(pending_posts_pair(period, &post)); +} + +void budget_posts::report_budget_items(const date_t& date) +{ + if (pending_posts.size() == 0) + return; + + bool reported; + do { + reported = false; + foreach (pending_posts_list::value_type& pair, pending_posts) { + optional<date_t> begin = pair.first.start; + if (! begin) { + if (! pair.first.find_period(date)) + throw_(std::runtime_error, _("Something odd has happened")); + begin = pair.first.start; + } + assert(begin); + + if (*begin <= date && + (! pair.first.finish || *begin < *pair.first.finish)) { + post_t& post = *pair.second; + + DEBUG("budget.generate", "Reporting budget for " + << post.reported_account()->fullname()); + + xact_t& xact = temps.create_xact(); + xact.payee = _("Budget transaction"); + xact._date = begin; + + post_t& temp = temps.copy_post(post, xact); + temp.amount.in_place_negate(); + + if (flags & BUDGET_WRAP_VALUES) { + value_t seq; + seq.push_back(0L); + seq.push_back(temp.amount); + + temp.xdata().compound_value = seq; + temp.xdata().add_flags(POST_EXT_COMPOUND); + } + + ++pair.first; + begin = *pair.first.start; + + item_handler<post_t>::operator()(temp); + + reported = true; + } + } + } while (reported); +} + +void budget_posts::operator()(post_t& post) +{ + bool post_in_budget = false; + + foreach (pending_posts_list::value_type& pair, pending_posts) { + for (account_t * acct = post.reported_account(); + acct; + acct = acct->parent) { + if (acct == (*pair.second).reported_account()) { + post_in_budget = true; + // Report the post as if it had occurred in the parent account. + if (post.reported_account() != acct) + post.set_reported_account(acct); + goto handle; + } + } + } + + handle: + if (post_in_budget && flags & BUDGET_BUDGETED) { + report_budget_items(post.date()); + item_handler<post_t>::operator()(post); + } + else if (! post_in_budget && flags & BUDGET_UNBUDGETED) { + item_handler<post_t>::operator()(post); + } +} + +void forecast_posts::add_post(const date_interval_t& period, post_t& post) +{ + generate_posts::add_post(period, post); + + // Advance the period's interval until it is at or beyond the current date. + date_interval_t& i = pending_posts.back().first; + if (! i.start) { + if (! i.find_period(CURRENT_DATE())) + throw_(std::runtime_error, _("Something odd has happened")); + ++i; + } else { + while (*i.start < CURRENT_DATE()) + ++i; + } +} + +void forecast_posts::flush() +{ + posts_list passed; + date_t last = CURRENT_DATE(); + + // If there are period transactions to apply in a continuing series until + // the forecast condition is met, generate those transactions now. Note + // that no matter what, we abandon forecasting beyond the next 5 years. + // + // It works like this: + // + // Earlier, in forecast_posts::add_period_xacts, we cut up all the periodic + // transactions into their components postings, so that we have N "periodic + // postings". For example, if the user had this: + // + // ~ daily + // Expenses:Food $10 + // Expenses:Auto:Gas $20 + // ~ monthly + // Expenses:Food $100 + // Expenses:Auto:Gas $200 + // + // We now have 4 periodic postings in `pending_posts'. + // + // Each periodic postings gets its own copy of its parent transaction's + // period, which is modified as we go. This is found in the second member + // of the pending_posts_list for each posting. + // + // The algorithm below works by iterating through the N periodic postings + // over and over, until each of them mets the termination critera for the + // forecast and is removed from the set. + + while (pending_posts.size() > 0) { + // At each step through the loop, we find the first periodic posting whose + // period contains the earliest starting date. + pending_posts_list::iterator least = pending_posts.begin(); + for (pending_posts_list::iterator i = ++pending_posts.begin(); + i != pending_posts.end(); + i++) { + if (*(*i).first.start < *(*least).first.start) + least = i; + } + + date_t& begin = *(*least).first.start; + if ((*least).first.finish) + assert(begin < *(*least).first.finish); + + // If the next date in the series for this periodic posting is more than 5 + // years beyond the last valid post we generated, drop it from further + // consideration. + date_t next = *(*least).first.next; + assert(next > begin); + + if (static_cast<std::size_t>((next - last).days()) > + static_cast<std::size_t>(365U) * forecast_years) { + DEBUG("filters.forecast", + "Forecast transaction exceeds " << forecast_years + << " years beyond today"); + pending_posts.erase(least); + continue; + } + + begin = next; + ++(*least).first; + + // `post' refers to the posting defined in the period transaction. We + // make a copy of it within a temporary transaction with the payee + // "Forecast transaction". + post_t& post = *(*least).second; + xact_t& xact = temps.create_xact(); + xact.payee = _("Forecast transaction"); + xact._date = begin; + post_t& temp = temps.copy_post(post, xact); + + // Submit the generated posting + DEBUG("filters.forecast", + "Forecast transaction: " << temp.date() + << " " << temp.account->fullname() + << " " << temp.amount); + item_handler<post_t>::operator()(temp); + + // If the generated posting matches the user's report query, check whether + // it also fails to match the continuation condition for the forecast. If + // it does, drop this periodic posting from consideration. + if (temp.has_xdata() && temp.xdata().has_flags(POST_EXT_MATCHES)) { + DEBUG("filters.forecast", " matches report query"); + bind_scope_t bound_scope(context, temp); + if (! pred(bound_scope)) { + DEBUG("filters.forecast", " fails to match continuation criteria"); + pending_posts.erase(least); + continue; + } + } + } + + item_handler<post_t>::flush(); +} + +pass_down_accounts::pass_down_accounts(acct_handler_ptr handler, + accounts_iterator& iter, + const optional<predicate_t>& _pred, + const optional<scope_t&>& _context) + : item_handler<account_t>(handler), pred(_pred), context(_context) +{ + TRACE_CTOR(pass_down_accounts, "acct_handler_ptr, accounts_iterator, ..."); + + for (account_t * account = iter(); account; account = iter()) { + if (! pred) { + item_handler<account_t>::operator()(*account); + } else { + bind_scope_t bound_scope(*context, *account); + if ((*pred)(bound_scope)) + item_handler<account_t>::operator()(*account); + } + } + + item_handler<account_t>::flush(); +} + +} // namespace ledger diff --git a/src/filters.h b/src/filters.h new file mode 100644 index 00000000..92148dbe --- /dev/null +++ b/src/filters.h @@ -0,0 +1,721 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file filters.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _FILTERS_H +#define _FILTERS_H + +#include "chain.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "temps.h" + +namespace ledger { + +////////////////////////////////////////////////////////////////////// +// +// Posting filters +// + +class ignore_posts : public item_handler<post_t> +{ +public: + virtual void operator()(post_t&) {} +}; + +class collect_posts : public item_handler<post_t> +{ +public: + std::vector<post_t *> posts; + + collect_posts() : item_handler<post_t>() { + TRACE_CTOR(collect_posts, ""); + } + virtual ~collect_posts() { + TRACE_DTOR(collect_posts); + } + + std::size_t length() const { + return posts.size(); + } + + std::vector<post_t *>::iterator begin() { + return posts.begin(); + } + std::vector<post_t *>::iterator end() { + return posts.end(); + } + + virtual void flush() {} + virtual void operator()(post_t& post) { + posts.push_back(&post); + } +}; + +class posts_iterator; + +class pass_down_posts : public item_handler<post_t> +{ + pass_down_posts(); + +public: + pass_down_posts(post_handler_ptr handler, posts_iterator& iter); + + virtual ~pass_down_posts() { + TRACE_DTOR(pass_down_posts); + } +}; + +class push_to_posts_list : public item_handler<post_t> +{ + push_to_posts_list(); + +public: + posts_list& posts; + + push_to_posts_list(posts_list& _posts) : posts(_posts) { + TRACE_CTOR(push_to_posts_list, "posts_list&"); + } + virtual ~push_to_posts_list() { + TRACE_DTOR(push_to_posts_list); + } + + virtual void operator()(post_t& post) { + posts.push_back(&post); + } +}; + +class truncate_xacts : public item_handler<post_t> +{ + int head_count; + int tail_count; + + posts_list posts; + std::size_t xacts_seen; + xact_t * last_xact; + + truncate_xacts(); + +public: + truncate_xacts(post_handler_ptr handler, + int _head_count, int _tail_count) + : item_handler<post_t>(handler), + head_count(_head_count), tail_count(_tail_count), + xacts_seen(0), last_xact(NULL) { + TRACE_CTOR(truncate_xacts, "post_handler_ptr, int, int"); + } + virtual ~truncate_xacts() { + TRACE_DTOR(truncate_xacts); + } + + virtual void flush(); + virtual void operator()(post_t& post); +}; + +class sort_posts : public item_handler<post_t> +{ + typedef std::deque<post_t *> posts_deque; + + posts_deque posts; + const expr_t sort_order; + + sort_posts(); + +public: + sort_posts(post_handler_ptr handler, + const expr_t& _sort_order) + : item_handler<post_t>(handler), + sort_order(_sort_order) { + TRACE_CTOR(sort_posts, + "post_handler_ptr, const value_expr&"); + } + sort_posts(post_handler_ptr handler, + const string& _sort_order) + : item_handler<post_t>(handler), + sort_order(_sort_order) { + TRACE_CTOR(sort_posts, + "post_handler_ptr, const string&"); + } + virtual ~sort_posts() { + TRACE_DTOR(sort_posts); + } + + virtual void post_accumulated_posts(); + + virtual void flush() { + post_accumulated_posts(); + item_handler<post_t>::flush(); + } + + virtual void operator()(post_t& post) { + posts.push_back(&post); + } +}; + +class sort_xacts : public item_handler<post_t> +{ + sort_posts sorter; + xact_t * last_xact; + + sort_xacts(); + +public: + sort_xacts(post_handler_ptr handler, + const expr_t& _sort_order) + : sorter(handler, _sort_order) { + TRACE_CTOR(sort_xacts, + "post_handler_ptr, const value_expr&"); + } + sort_xacts(post_handler_ptr handler, + const string& _sort_order) + : sorter(handler, _sort_order) { + TRACE_CTOR(sort_xacts, + "post_handler_ptr, const string&"); + } + virtual ~sort_xacts() { + TRACE_DTOR(sort_xacts); + } + + virtual void flush() { + sorter.flush(); + item_handler<post_t>::flush(); + } + + virtual void operator()(post_t& post) { + if (last_xact && post.xact != last_xact) + sorter.post_accumulated_posts(); + + sorter(post); + + last_xact = post.xact; + } +}; + +class filter_posts : public item_handler<post_t> +{ + predicate_t pred; + scope_t& context; + + filter_posts(); + +public: + filter_posts(post_handler_ptr handler, + const predicate_t& predicate, + scope_t& _context) + : item_handler<post_t>(handler), pred(predicate), context(_context) { + TRACE_CTOR(filter_posts, "post_handler_ptr, predicate_t, scope_t&"); + } + virtual ~filter_posts() { + TRACE_DTOR(filter_posts); + } + + virtual void operator()(post_t& post) { + bind_scope_t bound_scope(context, post); + if (pred(bound_scope)) { + post.xdata().add_flags(POST_EXT_MATCHES); + (*handler)(post); + } + } +}; + +class anonymize_posts : public item_handler<post_t> +{ + temporaries_t temps; + xact_t * last_xact; + + anonymize_posts(); + +public: + anonymize_posts(post_handler_ptr handler) + : item_handler<post_t>(handler), last_xact(NULL) { + TRACE_CTOR(anonymize_posts, "post_handler_ptr"); + } + virtual ~anonymize_posts() { + TRACE_DTOR(anonymize_posts); + } + + virtual void operator()(post_t& post); +}; + +class calc_posts : public item_handler<post_t> +{ + post_t * last_post; + expr_t& amount_expr; + bool calc_running_total; + + calc_posts(); + +public: + calc_posts(post_handler_ptr handler, + expr_t& _amount_expr, + bool _calc_running_total = false) + : item_handler<post_t>(handler), last_post(NULL), + amount_expr(_amount_expr), calc_running_total(_calc_running_total) { + TRACE_CTOR(calc_posts, "post_handler_ptr, expr_t&, bool"); + } + virtual ~calc_posts() { + TRACE_DTOR(calc_posts); + } + + virtual void operator()(post_t& post); +}; + +class collapse_posts : public item_handler<post_t> +{ + expr_t& amount_expr; + predicate_t display_predicate; + predicate_t only_predicate; + value_t subtotal; + std::size_t count; + xact_t * last_xact; + post_t * last_post; + temporaries_t temps; + account_t& totals_account; + bool only_collapse_if_zero; + std::list<post_t *> component_posts; + + collapse_posts(); + +public: + collapse_posts(post_handler_ptr handler, + expr_t& _amount_expr, + predicate_t _display_predicate, + predicate_t _only_predicate, + bool _only_collapse_if_zero = false) + : item_handler<post_t>(handler), amount_expr(_amount_expr), + display_predicate(_display_predicate), + only_predicate(_only_predicate), count(0), + last_xact(NULL), last_post(NULL), + totals_account(temps.create_account(_("<Total>"))), + only_collapse_if_zero(_only_collapse_if_zero) { + TRACE_CTOR(collapse_posts, "post_handler_ptr"); + } + virtual ~collapse_posts() { + TRACE_DTOR(collapse_posts); + } + + virtual void flush() { + report_subtotal(); + item_handler<post_t>::flush(); + } + + void report_subtotal(); + + virtual void operator()(post_t& post); +}; + +class related_posts : public item_handler<post_t> +{ + posts_list posts; + bool also_matching; + + related_posts(); + +public: + related_posts(post_handler_ptr handler, + const bool _also_matching = false) + : item_handler<post_t>(handler), + also_matching(_also_matching) { + TRACE_CTOR(related_posts, + "post_handler_ptr, const bool"); + } + virtual ~related_posts() throw() { + TRACE_DTOR(related_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post) { + post.xdata().add_flags(POST_EXT_RECEIVED); + posts.push_back(&post); + } +}; + +class changed_value_posts : public item_handler<post_t> +{ + // This filter requires that calc_posts be used at some point + // later in the chain. + + expr_t display_amount_expr; + expr_t total_expr; + expr_t display_total_expr; + report_t& report; + bool changed_values_only; + bool for_accounts_report; + bool show_unrealized; + post_t * last_post; + value_t last_total; + value_t last_display_total; + temporaries_t temps; + account_t& revalued_account; + account_t& rounding_account; + account_t * gains_equity_account; + account_t * losses_equity_account; + + changed_value_posts(); + +public: + changed_value_posts(post_handler_ptr handler, + report_t& _report, + bool _for_accounts_report, + bool _show_unrealized); + + virtual ~changed_value_posts() { + TRACE_DTOR(changed_value_posts); + } + + virtual void flush(); + + void output_revaluation(post_t& post, const date_t& current); + void output_rounding(post_t& post); + + virtual void operator()(post_t& post); +}; + +class subtotal_posts : public item_handler<post_t> +{ + subtotal_posts(); + +protected: + class acct_value_t + { + acct_value_t(); + + public: + account_t * account; + value_t value; + + acct_value_t(account_t * a) : account(a) { + TRACE_CTOR(acct_value_t, "account_t *"); + } + 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(const acct_value_t& av) + : account(av.account), value(av.value) { + TRACE_CTOR(acct_value_t, "copy"); + } + ~acct_value_t() throw() { + TRACE_DTOR(acct_value_t); + } + }; + + typedef std::map<string, acct_value_t> values_map; + 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; + +public: + subtotal_posts(post_handler_ptr handler, expr_t& _amount_expr, + const optional<string>& _date_format = none) + : item_handler<post_t>(handler), amount_expr(_amount_expr), + date_format(_date_format) { + TRACE_CTOR(subtotal_posts, + "post_handler_ptr, expr_t&, const optional<string>&"); + } + virtual ~subtotal_posts() { + TRACE_DTOR(subtotal_posts); + } + + void report_subtotal(const char * spec_fmt = NULL, + const optional<date_interval_t>& interval = none); + + virtual void flush() { + if (values.size() > 0) + report_subtotal(); + item_handler<post_t>::flush(); + } + virtual void operator()(post_t& post); +}; + +class interval_posts : public subtotal_posts +{ + date_interval_t interval; + date_interval_t last_interval; + post_t * last_post; + account_t& empty_account; + bool exact_periods; + bool generate_empty_posts; + + interval_posts(); + +public: + + interval_posts(post_handler_ptr _handler, + expr_t& amount_expr, + const date_interval_t& _interval, + bool _exact_periods = false, + bool _generate_empty_posts = false) + : subtotal_posts(_handler, amount_expr), interval(_interval), + last_post(NULL), empty_account(temps.create_account(_("<None>"))), + exact_periods(_exact_periods), + generate_empty_posts(_generate_empty_posts) { + TRACE_CTOR(interval_posts, + "post_handler_ptr, expr_t&, date_interval_t, bool, bool"); + } + virtual ~interval_posts() throw() { + TRACE_DTOR(interval_posts); + } + + void report_subtotal(const date_interval_t& interval); + + virtual void flush() { + if (last_post && interval.duration) { + if (interval.is_valid()) + report_subtotal(interval); + subtotal_posts::flush(); + } + } + virtual void operator()(post_t& post); +}; + +class posts_as_equity : public subtotal_posts +{ + post_t * last_post; + account_t& equity_account; + account_t * balance_account; + + posts_as_equity(); + +public: + posts_as_equity(post_handler_ptr _handler, expr_t& amount_expr) + : subtotal_posts(_handler, amount_expr), + equity_account(temps.create_account(_("Equity"))) { + TRACE_CTOR(posts_as_equity, "post_handler_ptr, expr_t&"); + balance_account = equity_account.find_account(_("Opening Balances")); + } + virtual ~posts_as_equity() throw() { + TRACE_DTOR(posts_as_equity); + } + + void report_subtotal(); + + virtual void flush() { + report_subtotal(); + subtotal_posts::flush(); + } +}; + +class by_payee_posts : public item_handler<post_t> +{ + typedef std::map<string, shared_ptr<subtotal_posts> > payee_subtotals_map; + typedef std::pair<string, shared_ptr<subtotal_posts> > payee_subtotals_pair; + + expr_t& amount_expr; + payee_subtotals_map payee_subtotals; + + by_payee_posts(); + + public: + by_payee_posts(post_handler_ptr handler, expr_t& _amount_expr) + : item_handler<post_t>(handler), amount_expr(_amount_expr) { + TRACE_CTOR(by_payee_posts, "post_handler_ptr, expr_t&"); + } + virtual ~by_payee_posts() { + TRACE_DTOR(by_payee_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post); +}; + +class transfer_details : public item_handler<post_t> +{ + account_t * master; + expr_t expr; + scope_t& scope; + temporaries_t temps; + + transfer_details(); + +public: + enum element_t { + SET_DATE, + SET_ACCOUNT, + SET_PAYEE + } which_element; + + transfer_details(post_handler_ptr handler, + element_t _which_element, + account_t * _master, + const expr_t& _expr, + scope_t& _scope) + : item_handler<post_t>(handler), master(_master), + expr(_expr), scope(_scope), which_element(_which_element) { + TRACE_CTOR(transfer_details, + "post_handler_ptr, element_t, account_t *, expr_t, scope_t&"); + } + virtual ~transfer_details() { + TRACE_DTOR(transfer_details); + } + + virtual void operator()(post_t& post); +}; + +class dow_posts : public subtotal_posts +{ + posts_list days_of_the_week[7]; + + dow_posts(); + +public: + dow_posts(post_handler_ptr handler, expr_t& amount_expr) + : subtotal_posts(handler, amount_expr) { + TRACE_CTOR(dow_posts, "post_handler_ptr, bool"); + } + virtual ~dow_posts() throw() { + TRACE_DTOR(dow_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post) { + days_of_the_week[post.date().day_of_week()].push_back(&post); + } +}; + +class generate_posts : public item_handler<post_t> +{ + generate_posts(); + +protected: + typedef std::pair<date_interval_t, post_t *> pending_posts_pair; + typedef std::list<pending_posts_pair> pending_posts_list; + + pending_posts_list pending_posts; + temporaries_t temps; + +public: + generate_posts(post_handler_ptr handler) + : item_handler<post_t>(handler) { + TRACE_CTOR(generate_posts, "post_handler_ptr"); + } + + virtual ~generate_posts() { + TRACE_DTOR(generate_posts); + } + + void add_period_xacts(period_xacts_list& period_xacts); + + virtual void add_post(const date_interval_t& period, post_t& post); +}; + +class budget_posts : public generate_posts +{ +#define BUDGET_NO_BUDGET 0x00 +#define BUDGET_BUDGETED 0x01 +#define BUDGET_UNBUDGETED 0x02 +#define BUDGET_WRAP_VALUES 0x04 + + uint_least8_t flags; + + budget_posts(); + +public: + budget_posts(post_handler_ptr handler, + uint_least8_t _flags = BUDGET_BUDGETED) + : generate_posts(handler), flags(_flags) { + TRACE_CTOR(budget_posts, "post_handler_ptr, uint_least8_t"); + } + virtual ~budget_posts() throw() { + TRACE_DTOR(budget_posts); + } + + void report_budget_items(const date_t& date); + + virtual void operator()(post_t& post); +}; + +class forecast_posts : public generate_posts +{ + predicate_t pred; + scope_t& context; + const std::size_t forecast_years; + + public: + forecast_posts(post_handler_ptr handler, + const predicate_t& predicate, + scope_t& _context, + const std::size_t _forecast_years) + : generate_posts(handler), pred(predicate), context(_context), + forecast_years(_forecast_years) { + TRACE_CTOR(forecast_posts, + "post_handler_ptr, predicate_t, scope_t&, std::size_t"); + } + virtual ~forecast_posts() throw() { + TRACE_DTOR(forecast_posts); + } + + virtual void add_post(const date_interval_t& period, post_t& post); + virtual void flush(); +}; + +////////////////////////////////////////////////////////////////////// +// +// Account filters +// + +class accounts_iterator; + +class pass_down_accounts : public item_handler<account_t> +{ + pass_down_accounts(); + + optional<predicate_t> pred; + optional<scope_t&> context; + +public: + pass_down_accounts(acct_handler_ptr handler, + accounts_iterator& iter, + const optional<predicate_t>& _pred = none, + const optional<scope_t&>& _context = none); + + virtual ~pass_down_accounts() { + TRACE_DTOR(pass_down_accounts); + } +}; + +} // namespace ledger + +#endif // _FILTERS_H diff --git a/src/flags.h b/src/flags.h new file mode 100644 index 00000000..69e40e4b --- /dev/null +++ b/src/flags.h @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file flags.h + * @author John Wiegley + * + * @ingroup util + */ +#ifndef _FLAGS_H +#define _FLAGS_H + + +template <typename T = boost::uint_least8_t, typename U = T> +class supports_flags +{ +public: + typedef T flags_t; + +protected: + flags_t _flags; + +public: + supports_flags() : _flags(static_cast<T>(0)) { + TRACE_CTOR(supports_flags, ""); + } + supports_flags(const supports_flags& arg) : _flags(arg._flags) { + TRACE_CTOR(supports_flags, "copy"); + } + supports_flags(const flags_t& arg) : _flags(arg) { + TRACE_CTOR(supports_flags, "const flags_t&"); + } + ~supports_flags() throw() { + TRACE_DTOR(supports_flags); + } + + supports_flags& operator=(const supports_flags& other) { + _flags = other._flags; + return *this; + } + + flags_t flags() const { + return _flags; + } + bool has_flags(const flags_t arg) const { + return _flags & arg; + } + + void set_flags(const flags_t arg) { + _flags = arg; + } + void clear_flags() { + _flags = static_cast<T>(0); + } + void add_flags(const flags_t arg) { + _flags = static_cast<T>(static_cast<U>(_flags) | static_cast<U>(arg)); + } + void drop_flags(const flags_t arg) { + _flags = static_cast<T>(static_cast<U>(_flags) & static_cast<U>(~arg)); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) + { + ar & _flags; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +template <typename T = boost::uint_least8_t, typename U = T> +class basic_flags_t : public supports_flags<T, U> +{ +public: + basic_flags_t() { + TRACE_CTOR(basic_flags_t, ""); + } + basic_flags_t(const T& bits) { + TRACE_CTOR(basic_flags_t, "const T&"); + set_flags(bits); + } + basic_flags_t(const U& bits) { + TRACE_CTOR(basic_flags_t, "const U&"); + set_flags(static_cast<T>(bits)); + } + ~basic_flags_t() throw() { + TRACE_DTOR(basic_flags_t); + } + + basic_flags_t(const basic_flags_t& other) + : supports_flags<T, U>(other) { + TRACE_CTOR(basic_flags_t, "copy"); + } + basic_flags_t& operator=(const basic_flags_t& other) { + set_flags(other.flags()); + return *this; + } + basic_flags_t& operator=(const T& bits) { + set_flags(bits); + return *this; + } + + operator T() const { + return supports_flags<T, U>::flags(); + } + operator U() const { + return supports_flags<T, U>::flags(); + } + + basic_flags_t plus_flags(const T& arg) const { + basic_flags_t temp(*this); + temp.add_flags(arg); + return temp; + } + basic_flags_t minus_flags(const T& arg) const { + basic_flags_t temp(*this); + temp.drop_flags(arg); + return temp; + } +}; + +template <typename T = boost::uint_least8_t> +class delegates_flags : public boost::noncopyable +{ +public: + typedef T flags_t; + +protected: + supports_flags<T>& _flags; + +public: + delegates_flags() : _flags() { + TRACE_CTOR(delegates_flags, ""); + } + delegates_flags(supports_flags<T>& arg) : _flags(arg) { + TRACE_CTOR(delegates_flags, "const supports_flags<T>&"); + } + ~delegates_flags() throw() { + TRACE_DTOR(delegates_flags); + } + + flags_t flags() const { + return _flags.flags(); + } + bool has_flags(const flags_t arg) const { + return _flags.has_flags(arg); + } + + void set_flags(const flags_t arg) { + _flags.set_flags(arg); + } + void clear_flags() { + _flags.clear_flags(); + } + void add_flags(const flags_t arg) { + _flags.add_flags(arg); + } + void drop_flags(const flags_t arg) { + _flags.drop_flags(arg); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) + { + ar & _flags; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +#endif // _FLAGS_H diff --git a/src/format.cc b/src/format.cc new file mode 100644 index 00000000..f26a86a1 --- /dev/null +++ b/src/format.cc @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2003-2009, 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 "format.h" +#include "scope.h" +#include "pstream.h" + +namespace ledger { + +format_t::elision_style_t format_t::default_style = TRUNCATE_TRAILING; +bool format_t::default_style_changed = false; + +void format_t::element_t::dump(std::ostream& out) const +{ + out << "Element: "; + + switch (type) { + case STRING: out << " STRING"; break; + case EXPR: out << " EXPR"; break; + } + + out << " flags: 0x" << std::hex << int(flags()); + out << " min: "; + out << std::right; + out.width(2); + out << std::dec << int(min_width); + out << " max: "; + out << std::right; + out.width(2); + out << std::dec << int(max_width); + + switch (type) { + case STRING: + out << " str: '" << boost::get<string>(data) << "'" << std::endl; + break; + case EXPR: + out << " expr: " << boost::get<expr_t>(data) << std::endl; + break; + } +} + +namespace { + expr_t parse_single_expression(const char *& p, bool single_expr = true) + { + string temp(p); + ptristream str(const_cast<char *&>(p)); + expr_t expr; + expr.parse(str, single_expr ? PARSE_SINGLE : PARSE_PARTIAL, temp); + if (str.eof()) { + expr.set_text(p); + p += std::strlen(p); + } else { + assert(str.good()); + istream_pos_type pos = str.tellg(); + expr.set_text(string(p, p + long(pos))); + p += long(pos) - 1; + + // Don't gobble up any whitespace + const char * base = p; + while (p >= base && std::isspace(*p)) + p--; + } + return expr; + } +} + +format_t::element_t * format_t::parse_elements(const string& fmt, + const optional<format_t&>& tmpl) +{ + std::auto_ptr<element_t> result; + + element_t * current = NULL; + + char buf[1024]; + char * q = buf; + + for (const char * p = fmt.c_str(); *p; p++) { + if (*p != '%' && *p != '\\') { + *q++ = *p; + continue; + } + + if (! result.get()) { + result.reset(new element_t); + current = result.get(); + } else { + current->next.reset(new element_t); + current = current->next.get(); + } + + if (q != buf) { + current->type = element_t::STRING; + current->data = string(buf, q); + q = buf; + + current->next.reset(new element_t); + current = current->next.get(); + } + + if (*p == '\\') { + p++; + current->type = element_t::STRING; + switch (*p) { + case 'b': current->data = string("\b"); break; + case 'f': current->data = string("\f"); break; + case 'n': current->data = string("\n"); break; + case 'r': current->data = string("\r"); break; + case 't': current->data = string("\t"); break; + case 'v': current->data = string("\v"); break; + case '\\': current->data = string("\\"); break; + default: current->data = string(1, *p); break; + } + continue; + } + + ++p; + while (*p == '-') { + switch (*p) { + case '-': + current->add_flags(ELEMENT_ALIGN_LEFT); + break; + } + ++p; + } + + std::size_t num = 0; + while (*p && std::isdigit(*p)) { + num *= 10; + num += *p++ - '0'; + } + current->min_width = num; + + if (*p == '.') { + ++p; + num = 0; + while (*p && std::isdigit(*p)) { + num *= 10; + num += *p++ - '0'; + } + current->max_width = num; + if (current->min_width == 0) + current->min_width = current->max_width; + } + + switch (*p) { + case '%': + current->type = element_t::STRING; + current->data = string("%"); + break; + + case '$': { + if (! tmpl) + throw_(format_error, _("Prior field reference, but no template")); + + p++; + if (*p == '0' || (! std::isdigit(*p) && + *p != 'A' && *p != 'B' && *p != 'C' && + *p != 'D' && *p != 'E' && *p != 'F')) + throw_(format_error, _("%$ field reference must be a digit from 1-9")); + + unsigned int index = std::isdigit(*p) ? *p - '0' : (*p - 'A' + 10); + element_t * tmpl_elem = tmpl->elements.get(); + + for (unsigned int i = 1; i < index && tmpl_elem; i++) { + tmpl_elem = tmpl_elem->next.get(); + while (tmpl_elem && tmpl_elem->type != element_t::EXPR) + tmpl_elem = tmpl_elem->next.get(); + } + + if (! tmpl_elem) + throw_(format_error, _("%$ reference to a non-existent prior field")); + + *current = *tmpl_elem; + break; + } + + case '(': + case '{': { + bool format_amount = *p == '{'; + if (format_amount) p++; + + current->type = element_t::EXPR; + current->data = parse_single_expression(p, ! format_amount); + + // Wrap the subexpression in calls to justify and scrub + if (format_amount) { + if (! *p || *(p + 1) != '}') + throw_(format_error, _("Expected closing brace")); + else + p++; + + 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 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 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); + + current->data = expr_t(call3_node); + } else { + current->data = expr_t(call2_node); + } + + boost::get<expr_t>(current->data).set_text(prev_expr); + } + break; + } + + default: + throw_(format_error, _("Unrecognized formatting character: %1") << *p); + } + } + + if (q != buf) { + if (! result.get()) { + result.reset(new element_t); + current = result.get(); + } else { + current->next.reset(new element_t); + current = current->next.get(); + } + current->type = element_t::STRING; + current->data = string(buf, q); + } + + return result.release(); +} + +string format_t::real_calc(scope_t& scope) +{ + std::ostringstream out_str; + + for (element_t * elem = elements.get(); elem; elem = elem->next.get()) { + std::ostringstream out; + string name; + + if (elem->has_flags(ELEMENT_ALIGN_LEFT)) + out << std::left; + else + out << std::right; + + switch (elem->type) { + case element_t::STRING: + if (elem->min_width > 0) + out.width(elem->min_width); + out << boost::get<string>(elem->data); + break; + + case element_t::EXPR: { + expr_t& expr(boost::get<expr_t>(elem->data)); + try { + + expr.compile(scope); + + value_t value; + if (expr.is_function()) { + call_scope_t args(scope); + args.push_back(long(elem->max_width)); + value = expr.get_function()(args); + } else { + value = expr.calc(scope); + } + DEBUG("format.expr", "value = (" << value << ")"); + + if (elem->min_width > 0) + value.print(out, static_cast<int>(elem->min_width), -1, + ! elem->has_flags(ELEMENT_ALIGN_LEFT)); + else + out << value.to_string(); + } + catch (const calc_error&) { + add_error_context(_("While calculating format expression:")); + add_error_context(expr.context_to_str()); + throw; + } + break; + } + + default: + assert(false); + break; + } + + if (elem->max_width > 0 || elem->min_width > 0) { + unistring temp(out.str()); + string result; + + if (elem->max_width > 0 && elem->max_width < temp.length()) { + result = truncate(temp, elem->max_width); + } else { + result = temp.extract(); + if (elem->min_width > temp.length()) + for (std::size_t i = 0; i < elem->min_width - temp.length(); i++) + result += " "; + } + out_str << result; + } else { + out_str << out.str(); + } + } + + return out_str.str(); +} + +string format_t::truncate(const unistring& ustr, + const std::size_t width, + const std::size_t account_abbrev_length) +{ + assert(width < 4095); + + const std::size_t len = ustr.length(); + if (width == 0 || len <= width) + return ustr.extract(); + + std::ostringstream buf; + + elision_style_t style = default_style; + if (account_abbrev_length > 0 && ! default_style_changed) + style = ABBREVIATE; + + switch (style) { + case TRUNCATE_LEADING: + // This method truncates at the beginning. + buf << ".." << ustr.extract(len - (width - 2), width - 2); + break; + + case TRUNCATE_MIDDLE: + // This method truncates in the middle. + buf << ustr.extract(0, (width - 2) / 2) + << ".." + << ustr.extract(len - ((width - 2) / 2 + (width - 2) % 2), + (width - 2) / 2 + (width - 2) % 2); + break; + + case ABBREVIATE: + if (account_abbrev_length > 0) { + std::list<string> parts; + string::size_type beg = 0; + string strcopy(ustr.extract()); + for (string::size_type pos = strcopy.find(':'); + pos != string::npos; + beg = pos + 1, pos = strcopy.find(':', beg)) + parts.push_back(string(strcopy, beg, pos - beg)); + parts.push_back(string(strcopy, beg)); + + std::ostringstream result; + + std::size_t newlen = len; + for (std::list<string>::iterator i = parts.begin(); + i != parts.end(); + i++) { + // Don't contract the last element + std::list<string>::iterator x = i; + if (++x == parts.end()) { + result << *i; + break; + } + + if (newlen > width) { + unistring temp(*i); + if (temp.length() > account_abbrev_length) { + result << temp.extract(0, account_abbrev_length) << ":"; + newlen -= temp.length() - account_abbrev_length; + } else { + result << temp.extract() << ":"; + newlen -= temp.length(); + } + } else { + result << *i << ":"; + } + } + + if (newlen > width) { + // Even abbreviated its too big to show the last account, so + // abbreviate all but the last and truncate at the beginning. + unistring temp(result.str()); + assert(temp.length() > width - 2); + buf << ".." << temp.extract(temp.length() - (width - 2), width - 2); + } else { + buf << result.str(); + } + break; + } + // fall through... + + case TRUNCATE_TRAILING: + // This method truncates at the end (the default). + buf << ustr.extract(0, width - 2) << ".."; + break; + } + + return buf.str(); +} + +} // namespace ledger diff --git a/src/format.h b/src/format.h new file mode 100644 index 00000000..a2bf1015 --- /dev/null +++ b/src/format.h @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup expr + */ + +/** + * @file format.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _FORMAT_H +#define _FORMAT_H + +#include "expr.h" +#include "unistring.h" + +namespace ledger { + +class unistring; + +DECLARE_EXCEPTION(format_error, std::runtime_error); + +class format_t : public expr_base_t<string> +{ + typedef expr_base_t<string> base_type; + + struct element_t : public supports_flags<> + { +#define ELEMENT_ALIGN_LEFT 0x01 + + enum kind_t { STRING, EXPR }; + + kind_t type; + std::size_t min_width; + std::size_t max_width; + variant<string, expr_t> data; + scoped_ptr<struct element_t> next; + + element_t() throw() + : supports_flags<>(), type(STRING), min_width(0), max_width(0) { + TRACE_CTOR(element_t, ""); + } + ~element_t() throw() { + TRACE_DTOR(element_t); + } + element_t(const element_t& elem) : supports_flags<>() { + *this = elem; + } + + element_t& operator=(const element_t& elem) { + if (this != &elem) { + supports_flags<>::operator=(elem); + type = elem.type; + min_width = elem.min_width; + max_width = elem.max_width; + data = elem.data; + } + return *this; + } + + friend inline void mark_red(std::ostream& out, const element_t * elem) { + out.setf(std::ios::left); + out.width(0); + out << "\033[31m"; + + if (elem->has_flags(ELEMENT_ALIGN_LEFT)) + out << std::left; + else + out << std::right; + + if (elem->min_width > 0) + out.width(elem->min_width); + } + + void dump(std::ostream& out) const; + }; + + scoped_ptr<element_t> elements; + +public: + static enum elision_style_t { + TRUNCATE_TRAILING, + TRUNCATE_MIDDLE, + TRUNCATE_LEADING, + ABBREVIATE + } default_style; + + static bool default_style_changed; + +private: + static element_t * parse_elements(const string& fmt, + const optional<format_t&>& tmpl); + +public: + format_t() : base_type() { + TRACE_CTOR(format_t, ""); + } + format_t(const string& _str, scope_t * context = NULL) + : base_type(context) { + TRACE_CTOR(format_t, "const string&"); + if (! _str.empty()) + parse_format(_str); + } + virtual ~format_t() { + TRACE_DTOR(format_t); + } + + void parse_format(const string& _format, + const optional<format_t&>& tmpl = none) { + elements.reset(parse_elements(_format, tmpl)); + set_text(_format); + } + + virtual result_type real_calc(scope_t& scope); + + virtual void dump(std::ostream& out) const { + for (const element_t * elem = elements.get(); + elem; + elem = elem->next.get()) + elem->dump(out); + } + + static string truncate(const unistring& str, + const std::size_t width, + const std::size_t account_abbrev_length = 0); +}; + +} // namespace ledger + +#endif // _FORMAT_H diff --git a/src/generate.cc b/src/generate.cc new file mode 100644 index 00000000..3549adc8 --- /dev/null +++ b/src/generate.cc @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2003-2009, 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 "generate.h" +#include "session.h" + +namespace ledger { + +generate_posts_iterator::generate_posts_iterator + (session_t& _session, + unsigned int _seed, + std::size_t _quantity, + bool _allow_invalid) + : session(_session), seed(_seed), quantity(_quantity), + allow_invalid(_allow_invalid), + + rnd_gen(seed == 0 ? static_cast<unsigned int>(std::time(0)) : seed), + + year_range(1900, 2300), year_gen(rnd_gen, year_range), + mon_range(1, 12), mon_gen(rnd_gen, mon_range), + day_range(1, 28), day_gen(rnd_gen, day_range), + + upchar_range('A', 'Z'), upchar_gen(rnd_gen, upchar_range), + downchar_range('a', 'z'), downchar_gen(rnd_gen, downchar_range), + numchar_range('0', '9'), numchar_gen(rnd_gen, numchar_range), + + truth_range(0, 1), truth_gen(rnd_gen, truth_range), + three_range(1, 3), three_gen(rnd_gen, three_range), + six_range(1, 6), six_gen(rnd_gen, six_range), + two_six_range(2, 6), two_six_gen(rnd_gen, two_six_range), + strlen_range(1, 40), strlen_gen(rnd_gen, strlen_range), + + neg_number_range(-1000000, -1), neg_number_gen(rnd_gen, neg_number_range), + pos_number_range(1, 1000000), 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()); + +} + +void generate_posts_iterator::generate_string(std::ostream& out, int len, + bool only_alpha) +{ + DEBUG("generate.post.string", + "Generating string of length " << len << ", only alpha " << only_alpha); + + int last = -1; + bool first = true; + for (int i = 0; i < len; i++) { + int next = only_alpha ? 3 : three_gen(); + bool output = true; + switch (next) { + case 1: // colon + if (! first && last == 3 && strlen_gen() % 10 == 0 && i + 1 != len) + out << ':'; + else { + i--; + output = false; + } + break; + case 2: // space + if (! first && last == 3 && strlen_gen() % 20 == 0 && i + 1 != len) + out << ' '; + else { + i--; + output = false; + } + break; + case 3: // character + switch (three_gen()) { + case 1: // uppercase + out << char(upchar_gen()); + break; + case 2: // lowercase + out << char(downchar_gen()); + break; + case 3: // number + if (! only_alpha && ! first) + out << char(numchar_gen()); + else { + i--; + output = false; + } + break; + } + break; + } + if (output) { + last = next; + first = false; + } + } +} + +bool generate_posts_iterator::generate_account(std::ostream& out, + bool no_virtual) +{ + bool must_balance = true; + bool is_virtual = false; + + if (! no_virtual) { + switch (three_gen()) { + case 1: + out << '['; + is_virtual = true; + break; + case 2: + out << '('; + must_balance = false; + is_virtual = true; + break; + case 3: + break; + } + } + + generate_string(out, strlen_gen()); + + if (is_virtual) { + if (must_balance) + out << ']'; + else + out << ')'; + } + + return must_balance; +} + +void generate_posts_iterator::generate_commodity(std::ostream& out) +{ + string comm; + do { + std::ostringstream buf; + generate_string(buf, six_gen(), true); + comm = buf.str(); + } + while (comm == "h" || comm == "m" || comm == "s" || + comm == "and" || comm == "div" || comm == "false" || + comm == "or" || comm == "not" || comm == "true" || + comm == "if" || comm == "else"); + + out << comm; +} + +string generate_posts_iterator::generate_amount(std::ostream& out, + value_t not_this_amount, + bool no_negative) +{ + std::ostringstream buf; + + if (truth_gen()) { // commodity goes in front + generate_commodity(buf); + if (truth_gen()) + buf << ' '; + if (no_negative || truth_gen()) + buf << pos_number_gen(); + else + buf << neg_number_gen(); + } else { + if (no_negative || truth_gen()) + buf << pos_number_gen(); + else + buf << neg_number_gen(); + if (truth_gen()) + buf << ' '; + generate_commodity(buf); + } + + // Possibly generate an annotized commodity, but make it rarer + if (! no_negative && three_gen() == 1) { + if (three_gen() == 1) { + buf << " {"; + generate_amount(buf, value_t(), true); + buf << '}'; + } + if (six_gen() == 1) { + buf << " ["; + generate_date(buf); + buf << ']'; + } + if (six_gen() == 1) { + buf << " ("; + generate_string(buf, six_gen()); + buf << ')'; + } + } + + if (! not_this_amount.is_null() && + value_t(buf.str()).as_amount().commodity() == + not_this_amount.as_amount().commodity()) + return ""; + + out << buf.str(); + + return buf.str(); +} + +bool generate_posts_iterator::generate_post(std::ostream& out, bool no_amount) +{ + out << " "; + bool must_balance = generate_account(out, no_amount); + out << " "; + + if (! no_amount) { + value_t amount(generate_amount(out)); + if (truth_gen()) + generate_cost(out, amount); + } + if (truth_gen()) + generate_note(out); + out << '\n'; + + return must_balance; +} + +void generate_posts_iterator::generate_cost(std::ostream& out, value_t amount) +{ + std::ostringstream buf; + + if (truth_gen()) + buf << " @ "; + else + buf << " @@ "; + + if (! generate_amount(buf, amount, true).empty()) + out << buf.str(); +} + +void generate_posts_iterator::generate_date(std::ostream& out) +{ + out.width(4); + out.fill('0'); + out << year_gen(); + + out.width(1); + out << '/'; + + out.width(2); + out.fill('0'); + out << mon_gen(); + + out.width(1); + out << '/'; + + out.width(2); + out.fill('0'); + out << day_gen(); +} + +void generate_posts_iterator::generate_state(std::ostream& out) +{ + switch (three_gen()) { + case 1: + out << "* "; + break; + case 2: + out << "! "; + break; + case 3: + out << ""; + break; + } +} + +void generate_posts_iterator::generate_code(std::ostream& out) +{ + out << '('; + generate_string(out, six_gen()); + out << ") "; +} + +void generate_posts_iterator::generate_payee(std::ostream& out) +{ + generate_string(out, strlen_gen()); +} + +void generate_posts_iterator::generate_note(std::ostream& out) +{ + out << "\n ; "; + generate_string(out, strlen_gen()); +} + +void generate_posts_iterator::generate_xact(std::ostream& out) +{ + out << format_date(next_date, FMT_WRITTEN); + next_date += gregorian::days(six_gen()); + if (truth_gen()) { + out << '='; + out << format_date(next_eff_date, FMT_WRITTEN); + next_eff_date += gregorian::days(six_gen()); + } + out << ' '; + + generate_state(out); + generate_code(out); + generate_payee(out); + if (truth_gen()) + generate_note(out); + out << '\n'; + + int count = three_gen() * 2; + bool has_must_balance = false; + for (int i = 0; i < count; i++) { + if (generate_post(out)) + has_must_balance = true; + } + if (has_must_balance) + generate_post(out, true); + + out << '\n'; +} + +post_t * generate_posts_iterator::operator()() +{ + post_t * post = posts(); + if (post == NULL && quantity > 0) { + std::ostringstream buf; + generate_xact(buf); + + DEBUG("generate.post", "The post we intend to parse:\n" << buf.str()); + + std::istringstream in(buf.str()); + try { + if (session.journal->parse(in, session) != 0) { + VERIFY(session.journal->xacts.back()->valid()); + posts.reset(*session.journal->xacts.back()); + post = posts(); + } + } + catch (std::exception& err) { + add_error_context(_("While parsing generated transaction (seed %1):") + << seed); + add_error_context(buf.str()); + throw; + } + catch (int status) { + add_error_context(_("While parsing generated transaction (seed %1):") + << seed); + add_error_context(buf.str()); + throw; + } + + quantity--; + } + return post; +} + +} // namespace ledger diff --git a/src/generate.h b/src/generate.h new file mode 100644 index 00000000..66513fc8 --- /dev/null +++ b/src/generate.h @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup generate + */ + +/** + * @file generate.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _GENERATE_H +#define _GENERATE_H + +#include "iterators.h" + +namespace ledger { + +class session_t; + +class generate_posts_iterator : public posts_iterator +{ + session_t& session; + unsigned int seed; + std::size_t quantity; + bool allow_invalid; + date_t next_date; + date_t next_eff_date; + + mt19937 rnd_gen; + + typedef variate_generator<mt19937&, uniform_int<> > int_generator_t; + typedef variate_generator<mt19937&, uniform_real<> > real_generator_t; + + uniform_int<> year_range; + int_generator_t year_gen; + uniform_int<> mon_range; + int_generator_t mon_gen; + uniform_int<> day_range; + int_generator_t day_gen; + + uniform_int<> upchar_range; + int_generator_t upchar_gen; + uniform_int<> downchar_range; + int_generator_t downchar_gen; + uniform_int<> numchar_range; + int_generator_t numchar_gen; + + uniform_int<> truth_range; + int_generator_t truth_gen; + uniform_int<> three_range; + int_generator_t three_gen; + uniform_int<> six_range; + int_generator_t six_gen; + uniform_int<> two_six_range; + int_generator_t two_six_gen; + + uniform_int<> strlen_range; + int_generator_t strlen_gen; + + uniform_real<> neg_number_range; + real_generator_t neg_number_gen; + uniform_real<> pos_number_range; + real_generator_t pos_number_gen; + + xact_posts_iterator posts; + +public: + generate_posts_iterator(session_t& _session, + unsigned int _seed = 0, + std::size_t _quantity = 100, + bool _allow_invalid = false); + + virtual ~generate_posts_iterator() throw() { + TRACE_DTOR(generate_posts_iterator); + } + + virtual post_t * operator()(); + +protected: + void generate_string(std::ostream& out, int len, bool only_alpha = false); + bool generate_account(std::ostream& out, bool no_virtual = false); + void generate_commodity(std::ostream& out); + string generate_amount(std::ostream& out, + value_t not_this_amount = NULL_VALUE, + bool no_negative = false); + bool generate_post(std::ostream& out, bool no_amount = false); + void generate_cost(std::ostream& out, value_t amount); + void generate_date(std::ostream& out); + void generate_state(std::ostream& out); + void generate_code(std::ostream& out); + void generate_payee(std::ostream& out); + void generate_note(std::ostream& out); + void generate_xact(std::ostream& out); +}; + +} // namespace ledger + +#endif // _GENERATE_H diff --git a/src/global.cc b/src/global.cc new file mode 100644 index 00000000..e120e5d5 --- /dev/null +++ b/src/global.cc @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2003-2009, 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 "global.h" +#if defined(HAVE_BOOST_PYTHON) +#include "pyinterp.h" +#else +#include "session.h" +#endif +#include "item.h" +#include "journal.h" +#include "pool.h" + +namespace ledger { + +static bool args_only = false; + +global_scope_t::global_scope_t(char ** envp) +{ + TRACE_CTOR(global_scope_t, ""); + +#if defined(HAVE_BOOST_PYTHON) + if (! python_session.get()) { + python_session.reset(new ledger::python_interpreter_t); + session_ptr = python_session; + } +#else + session_ptr.reset(new session_t); +#endif + + set_session_context(session_ptr.get()); + + // Create the report object, which maintains state relating to each + // command invocation. Because we're running from main(), the + // distinction between session and report doesn't really matter, but if + // a GUI were calling into Ledger it would have one session object per + // open document, with a separate report_t object for each report it + // generated. + report_stack.push_front(new report_t(session())); + scope_t::default_scope = &report(); + + // Read the user's options, in the following order: + // + // 1. environment variables (LEDGER_<option>) + // 2. initialization file (~/.ledgerrc) + // 3. command-line (--option or -o) + // + // Before processing command-line options, we must notify the session object + // that such options are beginning, since options like -f cause a complete + // override of files found anywhere else. + if (! args_only) { + session().set_flush_on_next_data_file(true); + read_environment_settings(envp); + session().set_flush_on_next_data_file(true); + read_init(); + } else { + session().HANDLER(price_db_).off(); + } +} + +global_scope_t::~global_scope_t() +{ + TRACE_DTOR(global_scope_t); + + // 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_VERIFY() set_session_context(NULL); + +#if defined(HAVE_BOOST_PYTHON) + python_session.reset(); +#endif +} + +void global_scope_t::read_init() +{ + if (HANDLED(init_file_)) { + path init_file(HANDLER(init_file_).str()); + if (exists(init_file)) { + TRACE_START(init, 1, "Read initialization file"); + + ifstream init(init_file); + + if (session().journal->read(init_file, NULL, &report()) > 0 || + session().journal->auto_xacts.size() > 0 || + session().journal->period_xacts.size() > 0) { + throw_(parse_error, _("Transactions found in initialization file '%1'") + << init_file); + } + + TRACE_FINISH(init, 1); + } + } +} + +char * global_scope_t::prompt_string() +{ + static char prompt[32]; + std::size_t i; + for (i = 0; i < report_stack.size(); i++) + prompt[i] = ']'; + prompt[i++] = ' '; + prompt[i] = '\0'; + return prompt; +} + +void global_scope_t::report_error(const std::exception& err) +{ + std::cout.flush(); // first display anything that was pending + + if (caught_signal == NONE_CAUGHT) { + // Display any pending error context information + string context = error_context(); + if (! context.empty()) + std::cerr << context << std::endl; + + std::cerr << _("Error: ") << err.what() << std::endl; + } else { + caught_signal = NONE_CAUGHT; + } +} + +void global_scope_t::execute_command(strings_list args, bool at_repl) +{ + session().set_flush_on_next_data_file(true); + + // Process the command verb, arguments and options + if (at_repl) { + args = read_command_arguments(report(), args); + if (args.empty()) + return; + } + + strings_list::iterator arg = args.begin(); + string verb = *arg++; + + // Look for a precommand first, which is defined as any defined function + // whose name starts with "ledger_precmd_". The difference between a + // precommand and a regular command is that precommands ignore the journal + // data file completely, nor is the user's init file read. + // + // Here are some examples of pre-commands: + // + // parse STRING ; show how a value expression is parsed + // eval STRING ; simply evaluate a value expression + // format STRING ; show how a format string is parsed + // + // If such a command is found, create the output stream for the result and + // then invoke the command. + + expr_t::func_t command; + bool is_precommand = false; + bind_scope_t bound_scope(*this, report()); + + if (bool(command = look_for_precommand(bound_scope, verb))) + 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 + // report options based on the command verb. + + if (! is_precommand) { + if (! at_repl) + session().read_journal_files(); + + report().normalize_options(verb); + + if (! bool(command = look_for_command(bound_scope, verb))) + throw_(std::logic_error, _("Unrecognized command '%1'") << verb); + } + + // Create the output stream (it might be a file, the console or a PAGER + // subprocess) and invoke the report command. The output stream is closed + // by the caller of this function. + + report().output_stream + .initialize(report().HANDLED(output_) ? + optional<path>(path(report().HANDLER(output_).str())) : + optional<path>(), + report().HANDLED(pager_) ? + optional<path>(path(report().HANDLER(pager_).str())) : + optional<path>()); + + // Now that the output stream is initialized, report the options that will + // participate in this report, if the user specified --options + + if (HANDLED(options)) + report_options(report(), report().output_stream); + + // Create an argument scope containing the report command's arguments, and + // then invoke the command. The bound scope causes lookups to happen + // first in the global scope, and then in the report scope. + + call_scope_t command_args(bound_scope); + for (strings_list::iterator i = arg; i != args.end(); i++) + command_args.push_back(string_value(*i)); + + INFO_START(command, "Finished executing command"); + command(command_args); + INFO_FINISH(command); +} + +int global_scope_t::execute_command_wrapper(strings_list args, bool at_repl) +{ + int status = 1; + + try { + if (at_repl) push_report(); + execute_command(args, at_repl); + if (at_repl) pop_report(); + + // If we've reached this point, everything succeeded fine. Ledger uses + // exceptions to notify of error conditions, so if you're using gdb, + // just type "catch throw" to find the source point of any error. + status = 0; + } + catch (const std::exception& err) { + if (at_repl) pop_report(); + report_error(err); + } + + return status; +} + +void global_scope_t::report_options(report_t& report, std::ostream& out) +{ + out << "===============================================================================" + << std::endl; + out << "[Global scope options]" << std::endl; + + HANDLER(args_only).report(out); + HANDLER(debug_).report(out); + HANDLER(init_file_).report(out); + HANDLER(script_).report(out); + HANDLER(trace_).report(out); + HANDLER(verbose).report(out); + HANDLER(verify).report(out); + + out << std::endl << "[Session scope options]" << std::endl; + report.session.report_options(out); + + out << std::endl << "[Report scope options]" << std::endl; + report.report_options(out); + out << "===============================================================================" + << std::endl; +} + +option_t<global_scope_t> * global_scope_t::lookup_option(const char * p) +{ + switch (*p) { + case 'a': + OPT(args_only); + break; + case 'd': + OPT(debug_); + break; + case 'f': + OPT(full_help); + break; + case 'h': + OPT_(help); + else OPT(help_calc); + else OPT(help_comm); + else OPT(help_disp); + break; + case 'i': + OPT(init_file_); + break; + case 'o': + OPT(options); + break; + case 's': + OPT(script_); + break; + case 't': + OPT(trace_); + break; + case 'v': + OPT_(verbose); + else OPT(verify); + else OPT(version); + break; + } + return NULL; +} + +expr_t::ptr_op_t global_scope_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + switch (kind) { + case symbol_t::FUNCTION: + if (option_t<global_scope_t> * handler = lookup_option(name.c_str())) + return MAKE_OPT_FUNCTOR(global_scope_t, handler); + break; + + case symbol_t::OPTION: + if (option_t<global_scope_t> * handler = lookup_option(name.c_str())) + return MAKE_OPT_HANDLER(global_scope_t, handler); + break; + + case symbol_t::PRECOMMAND: { + const char * p = name.c_str(); + switch (*p) { + case 'p': + if (is_eq(p, "push")) + return MAKE_FUNCTOR(global_scope_t::push_command); + else if (is_eq(p, "pop")) + return MAKE_FUNCTOR(global_scope_t::pop_command); + break; + } + } + default: + break; + } + + // If you're wondering how symbols from report() will be found, it's + // because of the bind_scope_t object in execute_command() below. + return NULL; +} + +void global_scope_t::read_environment_settings(char * envp[]) +{ + TRACE_START(environment, 1, "Processed environment variables"); + + process_environment(const_cast<const char **>(envp), "LEDGER_", report()); + +#if 1 + // These are here for backwards compatability, but are deprecated. + + if (const char * p = std::getenv("LEDGER")) { + if (! std::getenv("LEDGER_FILE")) + process_option("environ", "file", report(), p, "LEDGER"); + } + if (const char * p = std::getenv("LEDGER_INIT")) { + if (! std::getenv("LEDGER_INIT_FILE")) + process_option("environ", "init-file", report(), p, "LEDGER_INIT"); + } + if (const char * p = std::getenv("PRICE_HIST")) { + if (! std::getenv("LEDGER_PRICEDB")) + process_option("environ", "price-db", report(), p, "PRICE_HIST"); + } + if (const char * p = std::getenv("PRICE_EXP")) + process_option("environ", "price-exp", report(), p, "PRICE_EXP"); +#endif + + TRACE_FINISH(environment, 1); +} + +strings_list +global_scope_t::read_command_arguments(scope_t& scope, strings_list args) +{ + TRACE_START(arguments, 1, "Processed command-line arguments"); + + strings_list remaining = process_arguments(args, scope); + + TRACE_FINISH(arguments, 1); + + return remaining; +} + +void global_scope_t::normalize_session_options() +{ + INFO("Initialization file is " << HANDLER(init_file_).str()); + INFO("Price database is " << session().HANDLER(price_db_).str()); + + foreach (const path& pathname, session().HANDLER(file_).data_files) + INFO("Journal file is " << pathname.string()); +} + +expr_t::func_t global_scope_t::look_for_precommand(scope_t& scope, + const string& verb) +{ + if (expr_t::ptr_op_t def = scope.lookup(symbol_t::PRECOMMAND, verb)) + return def->as_function(); + else + return expr_t::func_t(); +} + +expr_t::func_t global_scope_t::look_for_command(scope_t& scope, + const string& verb) +{ + if (expr_t::ptr_op_t def = scope.lookup(symbol_t::COMMAND, verb)) + return def->as_function(); + else + return expr_t::func_t(); +} + +void global_scope_t::visit_man_page() const +{ + int pid = fork(); + if (pid < 0) { + throw std::logic_error(_("Failed to fork child process")); + } + else if (pid == 0) { // child + execlp("man", "man", "1", "ledger", NULL); + + // We should never, ever reach here + perror("execlp: man"); + exit(1); + } + + int status = -1; + wait(&status); + exit(0); // parent +} + +void handle_debug_options(int argc, char * argv[]) +{ + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (std::strcmp(argv[i], "--args-only") == 0) { + args_only = true; + } + else if (std::strcmp(argv[i], "--verify") == 0) { +#if defined(VERIFY_ON) + verify_enabled = true; // global in utils.h +#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 +#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 + 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 + try { + // global in utils.h + _trace_level = boost::lexical_cast<uint8_t>(argv[i + 1]); + } + catch (const boost::bad_lexical_cast& e) { + throw std::logic_error(_("Argument to --trace must be an integer")); + } + i++; +#endif + } + } + } +} + +} // namespace ledger diff --git a/src/global.h b/src/global.h new file mode 100644 index 00000000..ab3afed4 --- /dev/null +++ b/src/global.h @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2003-2009, 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. + */ + +/** + * @file global.h + * @author John Wiegley + * + * @brief Contains the top-level functions used by main.cc + */ +#ifndef _GLOBAL_H +#define _GLOBAL_H + +#include "interactive.h" +#include "option.h" +#include "report.h" + +namespace ledger { + +class session_t; +class report_t; + +class global_scope_t : public noncopyable, public scope_t +{ + shared_ptr<session_t> session_ptr; + ptr_list<report_t> report_stack; + +public: + global_scope_t(char ** envp); + ~global_scope_t(); + + void read_init(); + void read_environment_settings(char * envp[]); + strings_list read_command_arguments(scope_t& scope, strings_list args); + void normalize_session_options(); + expr_t::func_t look_for_precommand(scope_t& scope, const string& verb); + expr_t::func_t look_for_command(scope_t& scope, const string& verb); + + char * prompt_string(); + + session_t& session() { + return *session_ptr.get(); + } + report_t& report() { + return report_stack.front(); + } + + void push_report() { + report_stack.push_front(new report_t(report_stack.front())); + scope_t::default_scope = &report(); + } + 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(); + } + + void report_error(const std::exception& err); + + /** + * @return \c true if a command was actually executed; otherwise, it probably + * just resulted in setting some options. + */ + void execute_command(strings_list args, bool at_repl); + int execute_command_wrapper(strings_list args, bool at_repl); + + value_t push_command(call_scope_t&) { + // Make a copy at position 2, because the topmost report object has an + // open output stream at this point. We want it to get popped off as + // soon as this command terminate so that the stream is closed cleanly. + report_stack.insert(++report_stack.begin(), + new report_t(report_stack.front())); + return true; + } + value_t pop_command(call_scope_t&) { + pop_report(); + return true; + } + + void show_version_info(std::ostream& out) { + out << + "Ledger " << ledger::version << _(", the command-line accounting tool"); + out << + _("\n\nCopyright (c) 2003-2009, 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; + } + + void report_options(report_t& report, std::ostream& out); + + option_t<global_scope_t> * lookup_option(const char * p); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + OPTION(global_scope_t, args_only); + OPTION(global_scope_t, debug_); + + void visit_man_page() const; + + OPTION_(global_scope_t, full_help, DO() { parent->visit_man_page(); }); // -H + OPTION_(global_scope_t, help, DO() { parent->visit_man_page(); }); // -h + OPTION_(global_scope_t, help_calc, DO() { parent->visit_man_page(); }); + OPTION_(global_scope_t, help_comm, DO() { parent->visit_man_page(); }); + OPTION_(global_scope_t, help_disp, DO() { parent->visit_man_page(); }); + + 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()); + else + on(none, path("./.ledgerrc").string()); + }); + + OPTION(global_scope_t, options); + OPTION(global_scope_t, script_); + OPTION(global_scope_t, trace_); + OPTION(global_scope_t, verbose); + OPTION(global_scope_t, verify); + + OPTION_(global_scope_t, version, DO() { // -v + parent->show_version_info(std::cout); + throw int(0); // exit immediately + }); +}; + +void handle_debug_options(int argc, char * argv[]); + +} // namespace ledger + +#endif // _GLOBAL_H diff --git a/src/interactive.cc b/src/interactive.cc new file mode 100644 index 00000000..35382e8f --- /dev/null +++ b/src/interactive.cc @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2003-2009, 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 "interactive.h" + +namespace ledger { + +void interactive_t::verify_arguments() const +{ + value_t::sequence_t::const_iterator i; + + const char * p = spec.c_str(); + const char * label = _("unknown"); + bool wrong_arg = false; + bool dont_skip = false; + bool optional = *p == '&'; + bool exit_loop = *p == '*'; + std::size_t offset = 1; + bool is_seq = args.value().is_sequence(); + const value_t * next_arg = NULL; + string vlabel; + + if (is_seq) { + i = args.begin(); + if (i != args.end()) + next_arg = &(*i); + } + else if (! args.value().is_null()) { + next_arg = &args.value(); + } + + for (; ! wrong_arg && ! exit_loop && *p && next_arg; p++) { + DEBUG("interactive.verify", + "Want " << *p << " got: " << next_arg->label()); + + wrong_arg = false; + switch (*p) { + case 'a': + label = _("an amount"); + wrong_arg = (! next_arg->is_long() && + ! next_arg->is_amount() && + ! next_arg->is_balance()); + break; + case 'b': + label = _("a boolean"); + wrong_arg = false; // booleans are converted + break; + case 'd': + label = _("a date"); + wrong_arg = (! next_arg->is_date() && + ! next_arg->is_datetime()); + break; + case 't': + label = _("a date/time"); + wrong_arg = (! next_arg->is_date() && + ! next_arg->is_datetime()); + break; + case 'i': + case 'l': + label = _("an integer"); + if (next_arg->is_long() || + (next_arg->is_amount() && + ! next_arg->as_amount().has_commodity())) { + wrong_arg = false; + } + else if (next_arg->is_string()) { + wrong_arg = false; + for (const char * q = next_arg->as_string().c_str(); *q; q++) { + if (! std::isdigit(*q) && *q != '-') { + wrong_arg = true; + break; + } + } + } + else { + wrong_arg = true; + } + break; + case 'm': + label = _("a regex"); + wrong_arg = ! next_arg->is_mask(); + break; + case 's': + label = _("a string"); + wrong_arg = ! next_arg->is_string(); + break; + case 'v': + label = _("any value"); + wrong_arg = false; + break; + case '^': + label = _("a scope"); + wrong_arg = ! next_arg->is_scope(); + break; + case 'S': + label = _("a sequence"); + wrong_arg = false; + break; + case '&': + optional = true; + dont_skip = true; + break; + case '*': + optional = true; + exit_loop = true; + dont_skip = true; + break; + } + if (wrong_arg && optional && next_arg->is_null()) + wrong_arg = false; + + if (wrong_arg) + vlabel = next_arg->label(); + + if (! dont_skip) { + if (is_seq) { + if (++i != args.end()) { + next_arg = &(*i); + offset++; + } else { + next_arg = NULL; + } + } else { + next_arg = NULL; + } + } + dont_skip = false; + } + + if (*p == '&' || *p == '*') + optional = true; + + DEBUG("interactive.verify", "Remaining args are optional"); + + if (wrong_arg) { + throw_(std::logic_error, + _("Expected %1 for argument %2, but received %3") + << label << offset << vlabel); + } + else if (*p && ! optional && ! next_arg) { + throw_(std::logic_error, _("Too few arguments to function")); + } + else if (! *p && next_arg) { + throw_(std::logic_error, _("Too many arguments to function")); + } +} + +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(); +} + +} // namespace ledger diff --git a/src/interactive.h b/src/interactive.h new file mode 100644 index 00000000..199b7b71 --- /dev/null +++ b/src/interactive.h @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2003-2009, 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 interactive.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _INTERACTIVE_H +#define _INTERACTIVE_H + +#include "scope.h" + +namespace ledger { + +class interactive_t : public noncopyable +{ + call_scope_t& args; + string spec; + +public: + explicit interactive_t(call_scope_t& _args, const string& _spec = "") + : args(_args), spec(_spec) { + TRACE_CTOR(interactive_t, "call_scope_t&, const string&"); + verify_arguments(); + } + virtual ~interactive_t() { + TRACE_DTOR(interactive_t); + } + + void verify_arguments() const; + + bool has(std::size_t index) const { + if (index < args.size() && ! args[index].is_null()) + return true; + return false; + } + + value_t& value_at(std::size_t index) { + assert(has(index)); + return args[index]; + } + + template <typename T> + T get(std::size_t index); +}; + +template <> +inline bool interactive_t::get<bool>(std::size_t index) { + return value_at(index).to_boolean(); +} +template <> +inline int interactive_t::get<int>(std::size_t index) { + return value_at(index).to_int(); +} +template <> +inline long interactive_t::get<long>(std::size_t index) { + return value_at(index).to_long(); +} +template <> +inline amount_t interactive_t::get<amount_t>(std::size_t index) { + return value_at(index).to_amount(); +} +template <> +inline string interactive_t::get<string>(std::size_t index) { + return value_at(index).to_string(); +} +template <> +inline mask_t interactive_t::get<mask_t>(std::size_t index) { + return value_at(index).to_mask(); +} +template <> +inline date_t interactive_t::get<date_t>(std::size_t index) { + return value_at(index).to_date(); +} +template <> +inline datetime_t interactive_t::get<datetime_t>(std::size_t index) { + return value_at(index).to_datetime(); +} +template <> +inline value_t::sequence_t& +interactive_t::get<value_t::sequence_t&>(std::size_t index) { + return value_at(index).as_sequence_lval(); +} +template <> +inline const value_t::sequence_t& +interactive_t::get<const value_t::sequence_t&>(std::size_t index) { + return value_at(index).as_sequence(); +} + +template <typename T> +class in_context_t : public interactive_t +{ + T& context; + +public: + explicit in_context_t(call_scope_t& args, const string& spec) + : interactive_t(args, spec), context(find_scope<T>(args)) { + TRACE_CTOR(in_context_t, "call_scope_t&, const string&"); + } + virtual ~in_context_t() { + TRACE_DTOR(in_context_t); + } + + T& operator *() { + return context; + } + T * operator->() { + return &context; + } +}; + +string join_args(call_scope_t& args); + +} // namespace ledger + +#endif // _INTERACTIVE_H + diff --git a/src/item.cc b/src/item.cc new file mode 100644 index 00000000..99d1d835 --- /dev/null +++ b/src/item.cc @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2003-2009, 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 "item.h" +#include "interactive.h" + +namespace ledger { + +bool item_t::use_effective_date = false; + +bool item_t::has_tag(const string& tag) const +{ + DEBUG("item.meta", "Checking if item has tag: " << tag); + if (! metadata) { + DEBUG("item.meta", "Item has no metadata at all"); + return false; + } + string_map::const_iterator i = metadata->find(tag); +#if defined(DEBUG_ON) + if (SHOW_DEBUG("item.meta")) { + if (i == metadata->end()) + DEBUG("item.meta", "Item does not have this tag"); + else + DEBUG("item.meta", "Item has the tag!"); + } +#endif + return i != metadata->end(); +} + +bool item_t::has_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask) const +{ + if (metadata) { + foreach (const string_map::value_type& data, *metadata) { + if (tag_mask.match(data.first)) { + if (! value_mask) + return true; + else if (data.second) + return value_mask->match(*data.second); + } + } + } + return false; +} + +optional<string> item_t::get_tag(const string& tag) const +{ + DEBUG("item.meta", "Getting item tag: " << tag); + if (metadata) { + DEBUG("item.meta", "Item has metadata"); + string_map::const_iterator i = metadata->find(tag); + if (i != metadata->end()) { + DEBUG("item.meta", "Found the item!"); + return (*i).second; + } + } + return none; +} + +optional<string> item_t::get_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask) const +{ + if (metadata) { + foreach (const string_map::value_type& data, *metadata) { + if (tag_mask.match(data.first) && + (! value_mask || + (data.second && value_mask->match(*data.second)))) + return data.second; + } + } + return none; +} + +void item_t::set_tag(const string& tag, + const optional<string>& value) +{ + if (! metadata) + metadata = string_map(); + + DEBUG("item.meta", "Setting tag '" << tag << "' to value '" + << (value ? *value : string("<none>")) << "'"); + + std::pair<string_map::iterator, bool> result + = metadata->insert(string_map::value_type(tag, value)); + assert(result.second); +} + +void item_t::parse_tags(const char * p, + optional<date_t::year_type> current_year) +{ + if (const char * b = std::strchr(p, '[')) { + if (*(b + 1) != '\0' && + (std::isdigit(*(b + 1)) || *(b + 1) == '=')) { + if (const char * e = std::strchr(p, ']')) { + char buf[256]; + std::strncpy(buf, b + 1, e - b - 1); + buf[e - b - 1] = '\0'; + + if (char * p = std::strchr(buf, '=')) { + *p++ = '\0'; + _date_eff = parse_date(p, current_year); + } + if (buf[0]) + _date = parse_date(buf, current_year); + } + } + } + + if (! std::strchr(p, ':')) + return; + + scoped_array<char> buf(new char[std::strlen(p) + 1]); + + std::strcpy(buf.get(), p); + + string tag; + for (char * q = std::strtok(buf.get(), " \t"); + q; + q = std::strtok(NULL, " \t")) { + const string::size_type len = std::strlen(q); + if (! tag.empty()) { + if (! has_tag(tag)) + set_tag(tag, string(p + (q - buf.get()))); + break; + } + else if (q[0] == ':' && q[len - 1] == ':') { // a series of tags + for (char * r = std::strtok(q + 1, ":"); + r; + r = std::strtok(NULL, ":")) + set_tag(r); + } + else if (q[len - 1] == ':') { // a metadata setting + tag = string(q, len - 1); + } + } +} + +void item_t::append_note(const char * p, + optional<date_t::year_type> current_year) +{ + if (note) { + *note += '\n'; + *note += p; + } else { + note = p; + } + + parse_tags(p, current_year); +} + +namespace { + value_t get_status(item_t& item) { + return long(item.state()); + } + value_t get_uncleared(item_t& item) { + return item.state() == item_t::UNCLEARED; + } + value_t get_cleared(item_t& item) { + return item.state() == item_t::CLEARED; + } + value_t get_pending(item_t& item) { + return item.state() == item_t::PENDING; + } + + value_t get_actual(item_t& item) { + return ! item.has_flags(ITEM_GENERATED | ITEM_TEMP); + } + + value_t get_date(item_t& item) { + return item.date(); + } + value_t get_effective_date(item_t& item) { + if (optional<date_t> effective = item.effective_date()) + return *effective; + return NULL_VALUE; + } + value_t get_note(item_t& item) { + return string_value(item.note ? *item.note : empty_string); + } + + value_t has_tag(call_scope_t& args) { + item_t& item(find_scope<item_t>(args)); + + if (args.size() == 1) { + if (args[0].is_string()) + return item.has_tag(args[0].as_string()); + else if (args[0].is_mask()) + return item.has_tag(args[0].as_mask()); + else + throw_(std::runtime_error, + _("Expected string or mask for argument 1, but received %1") + << args[0].label()); + } + else if (args.size() == 2) { + if (args[0].is_mask() && args[1].is_mask()) + return item.has_tag(args[0].to_mask(), args[1].to_mask()); + else + throw_(std::runtime_error, + _("Expected masks for arguments 1 and 2, but received %1 and %2") + << args[0].label() << args[1].label()); + } + else if (args.size() == 0) { + throw_(std::runtime_error, _("Too few arguments to function")); + } + else { + throw_(std::runtime_error, _("Too many arguments to function")); + } + return false; + } + + value_t get_tag(call_scope_t& scope) { + in_context_t<item_t> env(scope, "s"); + if (optional<string> value = env->get_tag(env.get<string>(0))) + return string_value(*value); + return string_value(empty_string); + } + + value_t get_pathname(item_t& item) { + if (item.pos) + return string_value(item.pos->pathname.string()); + else + return string_value(empty_string); + } + + value_t get_beg_pos(item_t& item) { + return item.pos ? long(item.pos->beg_pos) : 0L; + } + + value_t get_beg_line(item_t& item) { + return item.pos ? long(item.pos->beg_line) : 0L; + } + + value_t get_end_pos(item_t& item) { + return item.pos ? long(item.pos->end_pos) : 0L; + } + + value_t get_end_line(item_t& item) { + return item.pos ? long(item.pos->end_line) : 0L; + } + + value_t get_depth(item_t&) { + return 0L; + } + + value_t ignore(item_t&) { + return false; + } + + template <value_t (*Func)(item_t&)> + value_t get_wrapper(call_scope_t& scope) { + return (*Func)(find_scope<item_t>(scope)); + } +} + +value_t get_comment(item_t& item) +{ + if (! item.note) { + return string_value(""); + } else { + std::ostringstream buf; + if (item.note->length() > 15) + buf << "\n ;"; + else + buf << " ;"; + + bool need_separator = false; + for (const char * p = item.note->c_str(); *p; p++) { + if (*p == '\n') { + need_separator = true; + } else { + if (need_separator) { + buf << "\n ;"; + need_separator = false; + } + buf << *p; + } + } + return string_value(buf.str()); + } +} + +expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (kind != symbol_t::FUNCTION) + return NULL; + + switch (name[0]) { + case 'a': + if (name == "actual") + return WRAP_FUNCTOR(get_wrapper<&get_actual>); + break; + + case 'b': + if (name == "beg_line") + return WRAP_FUNCTOR(get_wrapper<&get_beg_line>); + else if (name == "beg_pos") + return WRAP_FUNCTOR(get_wrapper<&get_beg_pos>); + break; + + case 'c': + if (name == "cleared") + return WRAP_FUNCTOR(get_wrapper<&get_cleared>); + else if (name == "comment") + return WRAP_FUNCTOR(get_wrapper<&get_comment>); + break; + + case 'd': + if (name[1] == '\0' || name == "date") + return WRAP_FUNCTOR(get_wrapper<&get_date>); + else if (name == "depth") + return WRAP_FUNCTOR(get_wrapper<&get_depth>); + break; + + case 'e': + if (name == "end_line") + return WRAP_FUNCTOR(get_wrapper<&get_end_line>); + 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>); + break; + + case 'f': + if (name == "filename") + return WRAP_FUNCTOR(get_wrapper<&get_pathname>); + break; + + case 'h': + if (name == "has_tag") + return WRAP_FUNCTOR(ledger::has_tag); + else if (name == "has_meta") + return WRAP_FUNCTOR(ledger::has_tag); + break; + + case 'i': + if (name == "is_account") + return WRAP_FUNCTOR(get_wrapper<&ignore>); + break; + + case 'm': + if (name == "meta") + return WRAP_FUNCTOR(ledger::get_tag); + break; + + case 'n': + if (name == "note") + return WRAP_FUNCTOR(get_wrapper<&get_note>); + break; + + case 'p': + if (name == "pending") + return WRAP_FUNCTOR(get_wrapper<&get_pending>); + else if (name == "parent") + return WRAP_FUNCTOR(get_wrapper<&ignore>); + break; + + case 's': + if (name == "status") + return WRAP_FUNCTOR(get_wrapper<&get_status>); + break; + + case 't': + if (name == "tag") + return WRAP_FUNCTOR(ledger::get_tag); + break; + + case 'u': + if (name == "uncleared") + return WRAP_FUNCTOR(get_wrapper<&get_uncleared>); + break; + + case 'L': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_actual>); + break; + + case 'X': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_cleared>); + break; + + case 'Y': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_pending>); + break; + } + + return NULL; +} + +bool item_t::valid() const +{ + if (_state != UNCLEARED && _state != CLEARED && _state != PENDING) { + DEBUG("ledger.validate", "item_t: state is bad"); + return false; + } + + return true; +} + +void print_item(std::ostream& out, const item_t& item, const string& prefix) +{ + out << source_context(item.pos->pathname, item.pos->beg_pos, + item.pos->end_pos, prefix); +} + +string item_context(const item_t& item, const string& desc) +{ + std::streamoff len = item.pos->end_pos - item.pos->beg_pos; + if (! len) + return _("<no item context>"); + + assert(len > 0); + assert(len < 2048); + + std::ostringstream out; + + if (item.pos->pathname == path("/dev/stdin")) { + out << desc << _(" from standard input:"); + return out.str(); + } + + out << desc << _(" from \"") << item.pos->pathname.string() << "\""; + + if (item.pos->beg_line != item.pos->end_line) + out << _(", lines ") << item.pos->beg_line << "-" + << item.pos->end_line << ":\n"; + else + out << _(", line ") << item.pos->beg_line << ":\n"; + + print_item(out, item, "> "); + + return out.str(); +} + +} // namespace ledger diff --git a/src/item.h b/src/item.h new file mode 100644 index 00000000..84385eb7 --- /dev/null +++ b/src/item.h @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup data Data representation + */ + +/** + * @file item.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _ITEM_H +#define _ITEM_H + +#include "scope.h" + +namespace ledger { + +struct position_t +{ + path pathname; + istream_pos_type beg_pos; + std::size_t beg_line; + istream_pos_type end_pos; + std::size_t end_line; + + position_t() : beg_pos(0), beg_line(0), end_pos(0), end_line(0) { + TRACE_CTOR(position_t, ""); + } + position_t(const position_t& pos) { + TRACE_CTOR(position_t, "copy"); + *this = pos; + } + ~position_t() throw() { + TRACE_DTOR(position_t); + } + + position_t& operator=(const position_t& pos) { + if (this != &pos) { + pathname = pos.pathname; + beg_pos = pos.beg_pos; + beg_line = pos.beg_line; + end_pos = pos.end_pos; + end_line = pos.end_line; + } + return *this; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & pathname; + ar & beg_pos; + ar & beg_line; + ar & end_pos; + ar & end_line; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class item_t : public supports_flags<>, 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 + + enum state_t { UNCLEARED = 0, CLEARED, PENDING }; + + typedef std::map<string, optional<string> > string_map; + + state_t _state; + optional<date_t> _date; + optional<date_t> _date_eff; + optional<string> note; + optional<position_t> pos; + optional<string_map> metadata; + + item_t(flags_t _flags = ITEM_NORMAL, const optional<string>& _note = none) + : supports_flags<>(_flags), _state(UNCLEARED), note(_note) + { + TRACE_CTOR(item_t, "flags_t, const string&"); + } + item_t(const item_t& item) : supports_flags<>(), scope_t() + { + TRACE_CTOR(item_t, "copy"); + copy_details(item); + } + virtual ~item_t() { + TRACE_DTOR(item_t); + } + + void copy_details(const item_t& item) + { + set_flags(item.flags()); + set_state(item.state()); + + _date = item._date; + _date_eff = item._date_eff; + note = item.note; + pos = item.pos; + metadata = item.metadata; + } + + virtual bool operator==(const item_t& xact) { + return this == &xact; + } + virtual bool operator!=(const item_t& xact) { + return ! (*this == xact); + } + + virtual bool has_tag(const string& tag) const; + virtual bool has_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask = none) const; + + virtual optional<string> get_tag(const string& tag) const; + virtual optional<string> get_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask = none) const; + + virtual void set_tag(const string& tag, + const optional<string>& value = none); + + virtual void parse_tags(const char * p, + optional<date_t::year_type> current_year = none); + virtual void append_note(const char * p, + optional<date_t::year_type> current_year = none); + + static bool use_effective_date; + + virtual date_t date() const { + assert(_date); + if (use_effective_date) + if (optional<date_t> effective = effective_date()) + return *effective; + return *_date; + } + virtual optional<date_t> effective_date() const { + return _date_eff; + } + + void set_state(state_t new_state) { + _state = new_state; + } + virtual state_t state() const { + return _state; + } + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + bool valid() 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 & boost::serialization::base_object<supports_flags<> >(*this); + ar & boost::serialization::base_object<scope_t>(*this); + ar & _state; + ar & _date; + ar & _date_eff; + ar & note; + ar & pos; + ar & metadata; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +value_t get_comment(item_t& item); +void print_item(std::ostream& out, const item_t& item, + const string& prefix = ""); +string item_context(const item_t& item, const string& desc); + +} // namespace ledger + +#endif // _ITEM_H diff --git a/src/iterators.cc b/src/iterators.cc new file mode 100644 index 00000000..540ba8ae --- /dev/null +++ b/src/iterators.cc @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2003-2009, 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 "iterators.h" +#include "journal.h" +#include "compare.h" + +namespace ledger { + +void xacts_iterator::reset(journal_t& journal) +{ + xacts_i = journal.xacts.begin(); + xacts_end = journal.xacts.end(); + xacts_uninitialized = false; +} + +xact_t * xacts_iterator::operator()() +{ + if (xacts_i != xacts_end) + return *xacts_i++; + else + return NULL; +} + +void journal_posts_iterator::reset(journal_t& journal) +{ + xacts.reset(journal); + + xact_t * xact = xacts(); + if (xact != NULL) + posts.reset(*xact); +} + +post_t * journal_posts_iterator::operator()() +{ + post_t * post = posts(); + if (post == NULL) { + xact_t * xact = xacts(); + if (xact != NULL) { + posts.reset(*xact); + post = posts(); + } + } + return post; +} + +void posts_commodities_iterator::reset(journal_t& journal) +{ + journal_posts.reset(journal); + + std::set<commodity_t *> commodities; + + for (post_t * post = journal_posts(); post; post = journal_posts()) { + commodity_t& comm(post->amount.commodity()); + if (comm.flags() & COMMODITY_NOMARKET) + continue; + commodities.insert(&comm); + } + + 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; + } + } + } + } + } + + xacts.xacts_i = xact_temps.begin(); + xacts.xacts_end = xact_temps.end(); + + xacts.xacts_uninitialized = false; + + xact_t * xact = xacts(); + if (xact != NULL) + posts.reset(*xact); +} + +post_t * posts_commodities_iterator::operator()() +{ + post_t * post = posts(); + if (post == NULL) { + xact_t * xact = xacts(); + if (xact != NULL) { + posts.reset(*xact); + post = posts(); + } + } + return post; +} + +account_t * basic_accounts_iterator::operator()() +{ + while (! accounts_i.empty() && + accounts_i.back() == accounts_end.back()) { + accounts_i.pop_back(); + accounts_end.pop_back(); + } + if (accounts_i.empty()) + return NULL; + + account_t * account = (*(accounts_i.back()++)).second; + assert(account); + + // If this account has children, queue them up to be iterated next. + if (! account->accounts.empty()) + push_back(*account); + + return account; +} + +void sorted_accounts_iterator::push_back(account_t& account) +{ + accounts_list.push_back(accounts_deque_t()); + + if (flatten_all) { + push_all(account, accounts_list.back()); + + std::stable_sort(accounts_list.back().begin(), + accounts_list.back().end(), + compare_items<account_t>(sort_cmp)); + +#if defined(DEBUG_ON) + if (SHOW_DEBUG("accounts.sorted")) { + foreach (account_t * account, accounts_list.back()) + DEBUG("accounts.sorted", + "Account (flat): " << account->fullname()); + } +#endif + } else { + sort_accounts(account, accounts_list.back()); + } + + sorted_accounts_i.push_back(accounts_list.back().begin()); + sorted_accounts_end.push_back(accounts_list.back().end()); +} + +void sorted_accounts_iterator::push_all(account_t& account, + accounts_deque_t& deque) +{ + foreach (accounts_map::value_type& pair, account.accounts) { + deque.push_back(pair.second); + push_all(*pair.second, deque); + } +} + +void sorted_accounts_iterator::sort_accounts(account_t& account, + accounts_deque_t& deque) +{ + foreach (accounts_map::value_type& pair, account.accounts) + deque.push_back(pair.second); + + std::stable_sort(deque.begin(), deque.end(), + compare_items<account_t>(sort_cmp)); + +#if defined(DEBUG_ON) + if (SHOW_DEBUG("accounts.sorted")) { + foreach (account_t * account, deque) + DEBUG("accounts.sorted", "Account: " << account->fullname()); + } +#endif +} + +account_t * sorted_accounts_iterator::operator()() +{ + while (! sorted_accounts_i.empty() && + sorted_accounts_i.back() == sorted_accounts_end.back()) { + sorted_accounts_i.pop_back(); + sorted_accounts_end.pop_back(); + assert(! accounts_list.empty()); + accounts_list.pop_back(); + } + if (sorted_accounts_i.empty()) + return NULL; + + account_t * account = *sorted_accounts_i.back()++; + assert(account); + + // If this account has children, queue them up to be iterated next. + if (! flatten_all && ! account->accounts.empty()) + push_back(*account); + + // Make sure the sorting value gets recalculated for this account + account->xdata().drop_flags(ACCOUNT_EXT_SORT_CALC); + return account; +} + +} // namespace ledger diff --git a/src/iterators.h b/src/iterators.h new file mode 100644 index 00000000..1cbe4c25 --- /dev/null +++ b/src/iterators.h @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2003-2009, 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 iterators.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _ITERATORS_H +#define _ITERATORS_H + +#include "xact.h" +#include "post.h" +#include "account.h" +#include "temps.h" + +namespace ledger { + +class journal_t; + +class posts_iterator : public noncopyable +{ +public: + virtual ~posts_iterator() throw() {} + virtual post_t * operator()() = 0; +}; + +class xact_posts_iterator : public posts_iterator +{ + posts_list::iterator posts_i; + posts_list::iterator posts_end; + + bool posts_uninitialized; + +public: + xact_posts_iterator() : posts_uninitialized(true) { + TRACE_CTOR(xact_posts_iterator, ""); + } + xact_posts_iterator(xact_t& xact) + : posts_uninitialized(true) { + TRACE_CTOR(xact_posts_iterator, "xact_t&"); + reset(xact); + } + virtual ~xact_posts_iterator() throw() { + TRACE_DTOR(xact_posts_iterator); + } + + void reset(xact_t& xact) { + posts_i = xact.posts.begin(); + posts_end = xact.posts.end(); + + posts_uninitialized = false; + } + + virtual post_t * operator()() { + if (posts_uninitialized || posts_i == posts_end) + return NULL; + return *posts_i++; + } +}; + +class xacts_iterator : public noncopyable +{ +public: + xacts_list::iterator xacts_i; + xacts_list::iterator xacts_end; + + bool xacts_uninitialized; + + xacts_iterator() : xacts_uninitialized(true) { + TRACE_CTOR(xacts_iterator, ""); + } + xacts_iterator(journal_t& journal) : xacts_uninitialized(true) { + TRACE_CTOR(xacts_iterator, "journal_t&"); + reset(journal); + } + virtual ~xacts_iterator() throw() { + TRACE_DTOR(xacts_iterator); + } + + void reset(journal_t& journal); + + xact_t * operator()(); +}; + +class journal_posts_iterator : public posts_iterator +{ + xacts_iterator xacts; + xact_posts_iterator posts; + +public: + journal_posts_iterator() { + TRACE_CTOR(journal_posts_iterator, ""); + } + journal_posts_iterator(journal_t& journal) { + TRACE_CTOR(journal_posts_iterator, "journal_t&"); + reset(journal); + } + virtual ~journal_posts_iterator() throw() { + TRACE_DTOR(journal_posts_iterator); + } + + void reset(journal_t& journal); + + virtual post_t * operator()(); +}; + +class posts_commodities_iterator : public posts_iterator +{ +protected: + journal_posts_iterator journal_posts; + xacts_iterator xacts; + xact_posts_iterator posts; + temporaries_t temps; + xacts_list xact_temps; + +public: + posts_commodities_iterator() { + TRACE_CTOR(posts_commodities_iterator, ""); + } + posts_commodities_iterator(journal_t& journal) { + TRACE_CTOR(posts_commodities_iterator, "journal_t&"); + reset(journal); + } + virtual ~posts_commodities_iterator() throw() { + TRACE_DTOR(posts_commodities_iterator); + } + + void reset(journal_t& journal); + + virtual post_t * operator()(); +}; + +class accounts_iterator : public noncopyable +{ +public: + virtual ~accounts_iterator() throw() {} + virtual account_t * operator()() = 0; +}; + +class basic_accounts_iterator : public accounts_iterator +{ + std::list<accounts_map::const_iterator> accounts_i; + std::list<accounts_map::const_iterator> accounts_end; + +public: + basic_accounts_iterator() { + TRACE_CTOR(basic_accounts_iterator, ""); + } + basic_accounts_iterator(account_t& account) { + TRACE_CTOR(basic_accounts_iterator, "account_t&"); + push_back(account); + } + virtual ~basic_accounts_iterator() throw() { + TRACE_DTOR(basic_accounts_iterator); + } + + void push_back(account_t& account) { + accounts_i.push_back(account.accounts.begin()); + accounts_end.push_back(account.accounts.end()); + } + + virtual account_t * operator()(); +}; + +class sorted_accounts_iterator : public accounts_iterator +{ + expr_t sort_cmp; + bool flatten_all; + + typedef std::deque<account_t *> accounts_deque_t; + + std::list<accounts_deque_t> accounts_list; + std::list<accounts_deque_t::const_iterator> sorted_accounts_i; + std::list<accounts_deque_t::const_iterator> sorted_accounts_end; + +public: + sorted_accounts_iterator(account_t& account, + const expr_t& _sort_cmp, bool _flatten_all) + : sort_cmp(_sort_cmp), flatten_all(_flatten_all) { + TRACE_CTOR(sorted_accounts_iterator, "const expr_t&, bool, account_t&"); + push_back(account); + } + virtual ~sorted_accounts_iterator() throw() { + TRACE_DTOR(sorted_accounts_iterator); + } + + void push_back(account_t& account); + void push_all(account_t& account, accounts_deque_t& deque); + void sort_accounts(account_t& account, accounts_deque_t& deque); + + virtual account_t * operator()(); +}; + +} // namespace ledger + +#endif // _ITERATORS_H diff --git a/src/journal.cc b/src/journal.cc new file mode 100644 index 00000000..6ebccd66 --- /dev/null +++ b/src/journal.cc @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2003-2009, 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 "journal.h" +#include "amount.h" +#include "commodity.h" +#include "pool.h" +#include "xact.h" +#include "account.h" + +namespace ledger { + +journal_t::journal_t() +{ + TRACE_CTOR(journal_t, ""); + initialize(); +} + +journal_t::journal_t(const path& pathname) +{ + TRACE_CTOR(journal_t, "path"); + initialize(); + read(pathname); +} + +journal_t::journal_t(const string& str) +{ + TRACE_CTOR(journal_t, "string"); + initialize(); + read(str); +} + +journal_t::~journal_t() +{ + TRACE_DTOR(journal_t); + + // Don't bother unhooking each xact's posts from the + // accounts they refer to, because all accounts are about to + // be deleted. + foreach (xact_t * xact, xacts) + checked_delete(xact); + + foreach (auto_xact_t * xact, auto_xacts) + checked_delete(xact); + + foreach (period_xact_t * xact, period_xacts) + checked_delete(xact); + + checked_delete(master); +} + +void journal_t::initialize() +{ + master = new account_t; + bucket = NULL; + was_loaded = false; +} + +void journal_t::add_account(account_t * acct) +{ + master->add_account(acct); +} + +bool journal_t::remove_account(account_t * acct) +{ + return master->remove_account(acct); +} + +account_t * journal_t::find_account(const string& name, bool auto_create) +{ + return master->find_account(name, auto_create); +} + +account_t * journal_t::find_account_re(const string& regexp) +{ + return master->find_account_re(regexp); +} + +bool journal_t::add_xact(xact_t * xact) +{ + xact->journal = this; + + if (! xact->finalize()) { + xact->journal = NULL; + return false; + } + + extend_xact(xact); + xacts.push_back(xact); + + return true; +} + +void journal_t::extend_xact(xact_base_t * xact) +{ + foreach (auto_xact_t * auto_xact, auto_xacts) + auto_xact->extend_xact(*xact); +} + +bool journal_t::remove_xact(xact_t * xact) +{ + bool found = false; + xacts_list::iterator i; + for (i = xacts.begin(); i != xacts.end(); i++) + if (*i == xact) { + found = true; + break; + } + if (! found) + return false; + + xacts.erase(i); + xact->journal = NULL; + + return true; +} + +std::size_t journal_t::read(std::istream& in, + const path& pathname, + account_t * master_alt, + scope_t * scope) +{ + std::size_t count = 0; + try { + if (! scope) + scope = scope_t::default_scope; + + if (! scope) + throw_(std::runtime_error, + _("No default scope in which to read journal file '%1'") + << pathname); + + value_t strict = expr_t("strict").calc(*scope); + + count = parse(in, *scope, master_alt ? master_alt : master, + &pathname, strict.to_boolean()); + } + catch (...) { + clear_xdata(); + throw; + } + + // xdata may have been set for some accounts and transaction due to the use + // of balance assertions or other calculations performed in valexpr-based + // posting amounts. + clear_xdata(); + + return count; +} + +std::size_t journal_t::read(const path& pathname, + account_t * master, + 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, scope); + if (count > 0) + sources.push_back(fileinfo_t(filename)); + return count; +} + +bool journal_t::has_xdata() +{ + foreach (xact_t * xact, xacts) + if (xact->has_xdata()) + return true; + + foreach (auto_xact_t * xact, auto_xacts) + if (xact->has_xdata()) + return true; + + foreach (period_xact_t * xact, period_xacts) + if (xact->has_xdata()) + return true; + + if (master->has_xdata() || master->children_with_xdata()) + return true; + + return false; +} + +void journal_t::clear_xdata() +{ + foreach (xact_t * xact, xacts) + if (! xact->has_flags(ITEM_TEMP)) + xact->clear_xdata(); + + foreach (auto_xact_t * xact, auto_xacts) + if (! xact->has_flags(ITEM_TEMP)) + xact->clear_xdata(); + + foreach (period_xact_t * xact, period_xacts) + if (! xact->has_flags(ITEM_TEMP)) + xact->clear_xdata(); + + master->clear_xdata(); +} + +bool journal_t::valid() const +{ + if (! master->valid()) { + DEBUG("ledger.validate", "journal_t: master not valid"); + return false; + } + + foreach (const xact_t * xact, xacts) + if (! xact->valid()) { + DEBUG("ledger.validate", "journal_t: xact not valid"); + return false; + } + + return true; +} + +} // namespace ledger diff --git a/src/journal.h b/src/journal.h new file mode 100644 index 00000000..8d59e3b4 --- /dev/null +++ b/src/journal.h @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2003-2009, 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 journal.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _JOURNAL_H +#define _JOURNAL_H + +#include "utils.h" +#include "times.h" + +namespace ledger { + +class xact_base_t; +class xact_t; +class auto_xact_t; +class period_xact_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; + +class journal_t : public noncopyable +{ +public: + struct fileinfo_t + { + optional<path> filename; + uintmax_t size; + datetime_t modtime; + bool from_stream; + + fileinfo_t() : size(0), from_stream(true) { + TRACE_CTOR(journal_t::fileinfo_t, ""); + } + 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)); + } + fileinfo_t(const fileinfo_t& info) + : filename(info.filename), size(info.size), + modtime(info.modtime), from_stream(info.from_stream) + { + TRACE_CTOR(journal_t::fileinfo_t, "copy"); + } + ~fileinfo_t() throw() { + TRACE_DTOR(journal_t::fileinfo_t); + } + +#if defined(HAVE_BOOST_SERIALIZATION) + private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & filename; + ar & size; + ar & modtime; + ar & from_stream; + } +#endif // HAVE_BOOST_SERIALIZATION + }; + + account_t * master; + account_t * bucket; + xacts_list xacts; + auto_xacts_list auto_xacts; + period_xacts_list period_xacts; + std::list<fileinfo_t> sources; + bool was_loaded; + + journal_t(); + journal_t(const path& pathname); + journal_t(const string& str); + ~journal_t(); + + void initialize(); + + std::list<fileinfo_t>::iterator sources_begin() { + return sources.begin(); + } + std::list<fileinfo_t>::iterator sources_end() { + 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); + + bool add_xact(xact_t * xact); + void extend_xact(xact_base_t * xact); + bool remove_xact(xact_t * xact); + + xacts_list::iterator xacts_begin() { + return xacts.begin(); + } + xacts_list::iterator xacts_end() { + return xacts.end(); + } + auto_xacts_list::iterator auto_xacts_begin() { + return auto_xacts.begin(); + } + auto_xacts_list::iterator auto_xacts_end() { + return auto_xacts.end(); + } + period_xacts_list::iterator period_xacts_begin() { + return period_xacts.begin(); + } + period_xacts_list::iterator period_xacts_end() { + 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); + + bool has_xdata(); + void clear_xdata(); + + bool valid() 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 & master; + ar & bucket; + ar & xacts; + ar & auto_xacts; + ar & period_xacts; + ar & sources; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +} // namespace ledger + +#endif // _JOURNAL_H diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 00000000..0aec8886 --- /dev/null +++ b/src/main.cc @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2003-2009, 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 "global.h" // This is where the meat of main() is, which + // was moved there for the sake of clarity here +#include "session.h" + +using namespace ledger; + +#ifdef HAVE_BOOST_PYTHON +namespace ledger { + extern char * argv0; +} +#endif + +int main(int argc, char * argv[], char * envp[]) +{ + int status = 1; + +#ifdef HAVE_BOOST_PYTHON + argv0 = argv[0]; +#endif + + // The very first thing we do is handle some very special command-line + // options, since they affect how the environment is setup: + // + // --verify ; turns on memory tracing + // --verbose ; turns on logging + // --debug CATEGORY ; turns on debug logging + // --trace LEVEL ; turns on trace logging + handle_debug_options(argc, argv); +#if defined(VERIFY_ON) + IF_VERIFY() initialize_memory_tracing(); +#endif + + INFO("Ledger starting"); + + // Initialize global Boost/C++ environment + std::ios::sync_with_stdio(false); + filesystem::path::default_name_check(filesystem::portable_posix_name); + + std::signal(SIGINT, sigint_handler); + std::signal(SIGPIPE, sigpipe_handler); + +#if defined(HAVE_GETTEXT) + ::textdomain("ledger"); +#endif + + std::auto_ptr<global_scope_t> global_scope; + + 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->session().set_flush_on_next_data_file(true); + + // Construct an STL-style argument list from the process command arguments + strings_list args; + for (int i = 1; i < argc; i++) + 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()); + + args = global_scope->read_command_arguments(bound_scope, args); + + if (global_scope->HANDLED(script_)) { + // Ledger is being invoked as a script command interpreter + global_scope->session().read_journal_files(); + + status = 0; + + ifstream in(global_scope->HANDLER(script_).str()); + while (status == 0 && ! in.eof()) { + char line[1024]; + in.getline(line, 1023); + + char * p = skip_ws(line); + if (*p && *p != '#') + status = global_scope->execute_command_wrapper(split_arguments(p), + true); + } + } + else if (! args.empty()) { + // User has invoke a verb at the interactive command-line + status = global_scope->execute_command_wrapper(args, false); + } + else { + // Commence the REPL by displaying the current Ledger version + global_scope->show_version_info(std::cout); + + global_scope->session().read_journal_files(); + + bool exit_loop = false; + +#ifdef HAVE_LIBEDIT + + rl_readline_name = const_cast<char *>("Ledger"); +#if 0 + // jww (2009-02-05): NYI + rl_attempted_completion_function = ledger_completion; +#endif + + while (char * p = readline(global_scope->prompt_string())) { + char * expansion = NULL; + int result; + + result = history_expand(skip_ws(p), &expansion); + + if (result < 0 || result == 2) { + if (expansion) + std::free(expansion); + std::free(p); + throw_(std::logic_error, + _("Failed to expand history reference '%1'") << p); + } + else if (expansion) { + add_history(expansion); + } + +#else // HAVE_LIBEDIT + + while (! std::cin.eof()) { + std::cout << global_scope->prompt_string(); + char line[1024]; + std::cin.getline(line, 1023); + + char * p = skip_ws(line); + +#endif // HAVE_LIBEDIT + + check_for_signal(); + + if (*p && *p != '#') { + if (std::strncmp(p, "quit", 4) == 0) + exit_loop = true; + else + global_scope->execute_command_wrapper(split_arguments(p), true); + } + +#ifdef HAVE_LIBEDIT + if (expansion) + std::free(expansion); + std::free(p); +#endif + + if (exit_loop) + break; + } + + status = 0; // report success + } + } + catch (const std::exception& err) { + if (global_scope.get()) + 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 + } + + // 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_VERIFY() { + global_scope.reset(); + + INFO("Ledger ended (Boost/libstdc++ may still hold memory)"); +#if defined(VERIFY_ON) + shutdown_memory_tracing(); +#endif + } else { + INFO("Ledger ended"); + } + + // Return the final status to the operating system, either 1 for error or 0 + // for a successful completion. + return status; +} + +// main.cc ends here. diff --git a/src/mask.cc b/src/mask.cc new file mode 100644 index 00000000..c1e66ced --- /dev/null +++ b/src/mask.cc @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2003-2009, 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 "mask.h" + +namespace ledger { + +mask_t::mask_t(const string& pat) : expr() +{ + TRACE_CTOR(mask_t, "const string&"); + *this = pat; +} + +mask_t& mask_t::operator=(const string& pat) +{ +#if defined(HAVE_BOOST_REGEX_UNICODE) + expr = boost::make_u32regex(pat.c_str(), boost::regex::perl | boost::regex::icase); +#else + expr.assign(pat.c_str(), boost::regex::perl | boost::regex::icase); +#endif + VERIFY(valid()); + return *this; +} + +} // namespace ledger diff --git a/src/mask.h b/src/mask.h new file mode 100644 index 00000000..4608898f --- /dev/null +++ b/src/mask.h @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file mask.h + * @author John Wiegley + * + * @ingroup util + * + * @brief Regular expression masking. + */ +#ifndef _MASK_H +#define _MASK_H + +#include "utils.h" +#if defined(HAVE_BOOST_REGEX_UNICODE) +#include "unistring.h" +#endif + +namespace ledger { + +class mask_t +{ +public: +#if defined(HAVE_BOOST_REGEX_UNICODE) + boost::u32regex expr; +#else + boost::regex expr; +#endif + + explicit mask_t(const string& pattern); + + mask_t() : expr() { + TRACE_CTOR(mask_t, ""); + } + mask_t(const mask_t& m) : expr(m.expr) { + TRACE_CTOR(mask_t, "copy"); + } + ~mask_t() throw() { + TRACE_DTOR(mask_t); + } + + mask_t& operator=(const string& other); + + bool operator==(const mask_t& other) const { + return expr == other.expr; + } + + bool match(const string& text) const { +#if defined(HAVE_BOOST_REGEX_UNICODE) + DEBUG("mask.match", + "Matching: \"" << text << "\" =~ /" << str() << "/ = " + << (boost::u32regex_search(text, expr) ? "true" : "false")); + return boost::u32regex_search(text, expr); +#else + DEBUG("mask.match", + "Matching: \"" << text << "\" =~ /" << str() << "/ = " + << (boost::regex_search(text, expr) ? "true" : "false")); + return boost::regex_search(text, expr); +#endif + } + + bool empty() const { + return expr.empty(); + } + + string str() const { + if (! empty()) { +#if defined(HAVE_BOOST_REGEX_UNICODE) + assert(sizeof(boost::uint32_t) == sizeof(UChar32)); + unistring ustr; + std::basic_string<UChar32> expr_str = expr.str(); + std::copy(expr_str.begin(), expr_str.end(), + std::back_inserter(ustr.utf32chars)); + return ustr.extract(); +#else + return expr.str(); +#endif + } else { + return empty_string; + } + } + + bool valid() const { + if (expr.status() != 0) { + DEBUG("ledger.validate", "mask_t: expr.status() != 0"); + return false; + } + return true; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + string temp; + if (Archive::is_loading::value) { + ar & temp; + *this = temp; + } else { + temp = str(); + ar & temp; + } + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline std::ostream& operator<<(std::ostream& out, const mask_t& mask) { + out << mask.str(); + return out; +} + +inline void to_xml(std::ostream& out, const mask_t& mask) +{ + push_xml x(out, "mask"); + out << x.guard(mask.str()); +} + +} // namespace ledger + +#endif // _MASK_H diff --git a/src/op.cc b/src/op.cc new file mode 100644 index 00000000..f38fc86b --- /dev/null +++ b/src/op.cc @@ -0,0 +1,732 @@ +/* + * Copyright (c) 2003-2009, 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 "op.h" +#include "scope.h" +#include "commodity.h" +#include "pool.h" + +namespace ledger { + +expr_t::ptr_op_t expr_t::op_t::compile(scope_t& scope, const int depth) +{ + if (is_ident()) { + DEBUG("expr.compile", "lookup: " << as_ident()); + + if (ptr_op_t def = scope.lookup(symbol_t::FUNCTION, as_ident())) { + // 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. + if (SHOW_DEBUG("expr.compile")) { + DEBUG("expr.compile", "Found definition:"); + def->dump(*_log_stream, 0); + } + return copy(def); + } + else if (left()) { + return copy(); + } + return this; + } + + if (kind < TERMINALS) + return this; + + if (kind == O_DEFINE) { + switch (left()->kind) { + case IDENT: + scope.define(symbol_t::FUNCTION, left()->as_ident(), right()); + break; + case O_CALL: + if (left()->left()->is_ident()) + scope.define(symbol_t::FUNCTION, left()->left()->as_ident(), this); + else + throw_(compile_error, _("Invalid function definition")); + break; + default: + throw_(compile_error, _("Invalid function definition")); + } + return wrap_value(value_t()); + } + + 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 (lhs == left() && (! rhs || rhs == right())) + return this; + + ptr_op_t intermediate(copy(lhs, rhs)); + + // Reduce constants immediately if possible + if ((! lhs || lhs->is_value()) && (! rhs || rhs->is_value())) + return wrap_value(intermediate->calc(scope, NULL, depth)); + + return intermediate; +} + +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; + + switch (kind) { + case VALUE: + result = as_value(); + break; + + case IDENT: { + if (! left()) + 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); + result = left()->compile(call_args, depth + 1) + ->calc(call_args, locus, depth + 1); + 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); + result = as_function()(call_args); +#if defined(DEBUG_ON) + skip_debug = true; +#endif + break; + } + + case O_DEFINE: { + call_scope_t& call_args(downcast<call_scope_t>(scope)); + std::size_t args_count = call_args.size(); + std::size_t args_index = 0; + + assert(left()->kind == O_CALL); + + for (ptr_op_t sym = left()->right(); + 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) + scope.define(symbol_t::FUNCTION, varname->as_ident(), + wrap_value(false)); + else + scope.define(symbol_t::FUNCTION, varname->as_ident(), + wrap_value(call_args[args_index++])); + } + + if (args_index < args_count) + throw_(calc_error, + _("Too many arguments in function call (saw %1)") << args_count); + + result = right()->calc(scope, locus, depth + 1); + break; + } + + case O_LOOKUP: + if (value_t obj = left()->calc(scope, locus, depth + 1)) { + if (obj.is_scope()) { + if (obj.as_scope() == NULL) { + throw_(calc_error, _("Left operand of . operator is NULL")); + } else { + scope_t& objscope(*obj.as_scope()); + if (ptr_op_t member = + objscope.lookup(symbol_t::FUNCTION, right()->as_ident())) { + result = member->calc(objscope, NULL, depth + 1); + break; + } + } + } + } + if (right()->kind != IDENT) + throw_(calc_error, + _("Right operand of . operator must be an identifier")); + else + throw_(calc_error, + _("Failed to lookup member '%1'") << right()->as_ident()); + break; + + case O_CALL: { + call_scope_t call_args(scope); + + if (has_right()) + call_args.set_args(right()->calc(scope, locus, depth + 1)); + + ptr_op_t func = left(); + const string& name(func->as_ident()); + + func = func->left(); + if (! func) + throw_(calc_error, _("Calling unknown function '%1'") << name); + + if (func->is_function()) + result = func->as_function()(call_args); + else + result = func->calc(call_args, locus, depth + 1); + break; + } + + case O_MATCH: + result = (right()->calc(scope, locus, depth + 1).as_mask() + .match(left()->calc(scope, locus, depth + 1).to_string())); + break; + + case O_EQ: + result = (left()->calc(scope, locus, depth + 1) == + right()->calc(scope, locus, depth + 1)); + break; + case O_LT: + result = (left()->calc(scope, locus, depth + 1) < + right()->calc(scope, locus, depth + 1)); + break; + case O_LTE: + result = (left()->calc(scope, locus, depth + 1) <= + right()->calc(scope, locus, depth + 1)); + break; + case O_GT: + result = (left()->calc(scope, locus, depth + 1) > + right()->calc(scope, locus, depth + 1)); + break; + case O_GTE: + result = (left()->calc(scope, locus, depth + 1) >= + right()->calc(scope, locus, depth + 1)); + break; + + case O_ADD: + result = (left()->calc(scope, locus, depth + 1) + + right()->calc(scope, locus, depth + 1)); + break; + case O_SUB: + result = (left()->calc(scope, locus, depth + 1) - + right()->calc(scope, locus, depth + 1)); + break; + case O_MUL: + result = (left()->calc(scope, locus, depth + 1) * + right()->calc(scope, locus, depth + 1)); + break; + case O_DIV: + result = (left()->calc(scope, locus, depth + 1) / + right()->calc(scope, locus, depth + 1)); + break; + + case O_NEG: + result = left()->calc(scope, locus, depth + 1).negated(); + break; + + case O_NOT: + result = ! left()->calc(scope, locus, depth + 1); + break; + + case O_AND: + if (left()->calc(scope, locus, depth + 1)) + result = right()->calc(scope, locus, depth + 1); + else + result = false; + break; + + case O_OR: + if (value_t temp = left()->calc(scope, locus, depth + 1)) + result = temp; + else + result = right()->calc(scope, locus, depth + 1); + break; + + 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 + result = right()->right()->calc(scope, locus, depth + 1); + break; + + case O_COLON: + assert(! "We should never calculate an O_COLON operator"); + 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; + } + 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); + } + } + break; + } + + case LAST: + default: + assert(false); + break; + } + +#if defined(DEBUG_ON) + if (! skip_debug && SHOW_DEBUG("expr.calc")) { + for (int i = 0; i < depth; i++) + ledger::_log_buffer << '.'; + ledger::_log_buffer << op_context(this) << " => "; + result.dump(ledger::_log_buffer, true); + DEBUG("expr.calc", ""); + } +#endif + + return result; + + } + catch (const std::exception& err) { + if (locus && ! *locus) + *locus = this; + throw; + } +} + +namespace { + bool print_cons(std::ostream& out, const expr_t::const_ptr_op_t op, + const expr_t::op_t::context_t& context) + { + bool found = false; + + assert(op->left()); + if (op->left()->print(out, context)) + found = true; + + if (op->has_right()) { + out << ", "; + if (op->right()->kind == expr_t::op_t::O_CONS) + found = print_cons(out, op->right(), context); + else if (op->right()->print(out, context)) + found = true; + } + return found; + } + + bool print_seq(std::ostream& out, const expr_t::const_ptr_op_t op, + const expr_t::op_t::context_t& context) + { + bool found = false; + + assert(op->left()); + if (op->left()->print(out, context)) + found = true; + + if (op->has_right()) { + out << "; "; + + if (op->right()->kind == expr_t::op_t::O_CONS) + found = print_cons(out, op->right(), context); + else if (op->right()->print(out, context)) + found = true; + } + + return found; + } +} + +bool expr_t::op_t::print(std::ostream& out, const context_t& context) const +{ + bool found = false; + + if (context.start_pos && this == context.op_to_find) { + *context.start_pos = out.tellp(); + *context.start_pos -= 1; + found = true; + } + + string symbol; + + switch (kind) { + case VALUE: + as_value().dump(out, context.relaxed); + break; + + case IDENT: + out << as_ident(); + break; + + case FUNCTION: + out << "<FUNCTION>"; + break; + + case O_NOT: + out << "!("; + if (left() && left()->print(out, context)) + found = true; + out << ")"; + break; + case O_NEG: + out << "-("; + if (left() && left()->print(out, context)) + found = true; + out << ")"; + break; + + case O_ADD: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " + "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_SUB: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " - "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_MUL: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " * "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_DIV: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " / "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + + case O_EQ: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " == "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_LT: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " < "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_LTE: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " <= "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_GT: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " > "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_GTE: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " >= "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + + case O_AND: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " & "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + case O_OR: + out << "("; + if (left() && left()->print(out, context)) + found = true; + out << " | "; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + break; + + case O_QUERY: + if (left() && left()->print(out, context)) + found = true; + out << " ? "; + if (has_right() && right()->print(out, context)) + found = true; + break; + + case O_COLON: + if (left() && left()->print(out, context)) + found = true; + out << " : "; + if (has_right() && right()->print(out, context)) + found = true; + break; + + case O_CONS: + found = print_cons(out, this, context); + break; + + case O_SEQ: + out << "("; + found = print_seq(out, this, context); + out << ")"; + break; + + case O_DEFINE: + if (left() && left()->print(out, context)) + found = true; + out << " := "; + if (has_right() && right()->print(out, context)) + found = true; + break; + + case O_LOOKUP: + if (left() && left()->print(out, context)) + found = true; + out << "."; + if (has_right() && right()->print(out, context)) + found = true; + break; + + case O_CALL: + if (left() && left()->print(out, context)) + found = true; + if (has_right()) { + if (right()->kind == O_SEQ) { + if (right()->print(out, context)) + found = true; + } else { + out << "("; + if (has_right() && right()->print(out, context)) + found = true; + out << ")"; + } + } else { + out << "()"; + } + break; + + case O_MATCH: + if (left() && left()->print(out, context)) + found = true; + out << " =~ "; + if (has_right() && right()->print(out, context)) + found = true; + break; + + case LAST: + default: + assert(false); + break; + } + + if (! symbol.empty()) { + if (commodity_pool_t::current_pool->find(symbol)) + out << '@'; + out << symbol; + } + + if (context.end_pos && this == context.op_to_find) { + *context.end_pos = out.tellp(); + *context.end_pos -= 1; + } + + return found; +} + +void expr_t::op_t::dump(std::ostream& out, const int depth) const +{ + out.setf(std::ios::left); + out.width((sizeof(void *) * 2) + 2); + out << this; + + for (int i = 0; i < depth; i++) + out << " "; + + switch (kind) { + case VALUE: + out << "VALUE: "; + as_value().dump(out); + break; + + case IDENT: + out << "IDENT: " << as_ident(); + break; + + case FUNCTION: + out << "FUNCTION"; + break; + + case O_DEFINE: out << "O_DEFINE"; break; + case O_LOOKUP: out << "O_LOOKUP"; break; + case O_CALL: out << "O_CALL"; break; + case O_MATCH: out << "O_MATCH"; break; + + case O_NOT: out << "O_NOT"; break; + case O_NEG: out << "O_NEG"; break; + + case O_ADD: out << "O_ADD"; break; + case O_SUB: out << "O_SUB"; break; + case O_MUL: out << "O_MUL"; break; + case O_DIV: out << "O_DIV"; break; + + case O_EQ: out << "O_EQ"; break; + case O_LT: out << "O_LT"; break; + case O_LTE: out << "O_LTE"; break; + case O_GT: out << "O_GT"; break; + case O_GTE: out << "O_GTE"; break; + + case O_AND: out << "O_AND"; break; + case O_OR: out << "O_OR"; break; + + case O_QUERY: out << "O_QUERY"; break; + case O_COLON: out << "O_COLON"; break; + + case O_CONS: out << "O_CONS"; break; + case O_SEQ: out << "O_SEQ"; break; + + case LAST: + default: + assert(false); + break; + } + + out << " (" << refc << ')' << std::endl; + + // 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 (left()) { + left()->dump(out, depth + 1); + if (kind > UNARY_OPERATORS && has_right()) + right()->dump(out, depth + 1); + } + else if (kind > UNARY_OPERATORS) { + assert(! has_right()); + } + } +} + +string op_context(const expr_t::ptr_op_t op, + const expr_t::ptr_op_t locus) +{ + ostream_pos_type start_pos, end_pos; + expr_t::op_t::context_t context(op, locus, &start_pos, &end_pos); + std::ostringstream buf; + buf << " "; + if (op->print(buf, context)) { + buf << "\n"; + for (int i = 0; i <= end_pos; i++) { + if (i > start_pos) + buf << "^"; + else + buf << " "; + } + } + return buf.str(); +} + +} // namespace ledger diff --git a/src/op.h b/src/op.h new file mode 100644 index 00000000..347eac1d --- /dev/null +++ b/src/op.h @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2003-2009, 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 op.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _OP_H +#define _OP_H + +#include "expr.h" + +namespace ledger { + +class expr_t::op_t : public noncopyable +{ + friend class expr_t; + friend class expr_t::parser_t; + +public: + typedef expr_t::ptr_op_t ptr_op_t; + +private: + mutable short refc; + ptr_op_t left_; + + variant<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 + > data; + +public: + enum kind_t { + // Constants + VALUE, + IDENT, + + CONSTANTS, + + FUNCTION, + + TERMINALS, + + // Binary operators + O_NOT, + O_NEG, + + UNARY_OPERATORS, + + O_EQ, + O_LT, + O_LTE, + O_GT, + O_GTE, + + O_AND, + O_OR, + + O_ADD, + O_SUB, + O_MUL, + O_DIV, + + O_QUERY, + O_COLON, + + O_CONS, + O_SEQ, + + O_DEFINE, + O_LOOKUP, + O_CALL, + O_MATCH, + + BINARY_OPERATORS, + + OPERATORS, + + UNKNOWN, + + LAST + }; + + kind_t kind; + + explicit op_t() : refc(0), kind(UNKNOWN) { + TRACE_CTOR(op_t, ""); + } + explicit op_t(const kind_t _kind) : refc(0), kind(_kind) { + TRACE_CTOR(op_t, "const kind_t"); + } + ~op_t() { + TRACE_DTOR(op_t); + assert(refc == 0); + } + + bool is_value() const { + if (kind == VALUE) { + assert(data.type() == typeid(value_t)); + return true; + } + return false; + } + value_t& as_value_lval() { + assert(is_value()); + value_t& val(boost::get<value_t>(data)); + VERIFY(val.valid()); + return val; + } + const value_t& as_value() const { + return const_cast<op_t *>(this)->as_value_lval(); + } + void set_value(const value_t& val) { + VERIFY(val.valid()); + data = val; + } + + bool is_ident() const { + if (kind == IDENT) { + assert(data.type() == typeid(string)); + return true; + } + return false; + } + string& as_ident_lval() { + assert(is_ident()); + return boost::get<string>(data); + } + const string& as_ident() const { + return const_cast<op_t *>(this)->as_ident_lval(); + } + void set_ident(const string& val) { + data = val; + } + + bool is_function() const { + return kind == FUNCTION; + } + expr_t::func_t& as_function_lval() { + assert(kind == FUNCTION); + return boost::get<expr_t::func_t>(data); + } + const expr_t::func_t& as_function() const { + return const_cast<op_t *>(this)->as_function_lval(); + } + void set_function(const expr_t::func_t& val) { + data = val; + } + + ptr_op_t& left() { + assert(kind > TERMINALS || kind == IDENT); + return left_; + } + const ptr_op_t& left() const { + assert(kind > TERMINALS || kind == IDENT); + return left_; + } + void set_left(const ptr_op_t& expr) { + assert(kind > TERMINALS || kind == IDENT); + left_ = expr; + } + + ptr_op_t& as_op_lval() { + assert(kind > TERMINALS || kind == IDENT); + return boost::get<ptr_op_t>(data); + } + const ptr_op_t& as_op() const { + return const_cast<op_t *>(this)->as_op_lval(); + } + + ptr_op_t& right() { + assert(kind > TERMINALS); + return as_op_lval(); + } + const ptr_op_t& right() const { + assert(kind > TERMINALS); + return as_op(); + } + void set_right(const ptr_op_t& expr) { + assert(kind > TERMINALS); + data = expr; + } + bool has_right() const { + if (kind < TERMINALS) + return false; + return as_op(); + } + +private: + void acquire() const { + DEBUG("op.memory", + "Acquiring " << this << ", refc now " << refc + 1); + assert(refc >= 0); + refc++; + } + void release() const { + DEBUG("op.memory", + "Releasing " << this << ", refc now " << refc - 1); + assert(refc > 0); + if (--refc == 0) + 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(); + } + + ptr_op_t copy(ptr_op_t _left = NULL, ptr_op_t _right = NULL) const { + ptr_op_t node(new_node(kind, _left, _right)); + if (kind < TERMINALS) + node->data = data; + return node; + } + +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); + value_t calc(scope_t& scope, ptr_op_t * locus = NULL, + const int depth = 0); + + struct context_t + { + ptr_op_t expr_op; + ptr_op_t op_to_find; + ostream_pos_type * start_pos; + ostream_pos_type * end_pos; + bool relaxed; + + context_t(const ptr_op_t& _expr_op = NULL, + const ptr_op_t& _op_to_find = NULL, + ostream_pos_type * const _start_pos = NULL, + ostream_pos_type * const _end_pos = NULL, + const bool _relaxed = true) + : expr_op(_expr_op), op_to_find(_op_to_find), + start_pos(_start_pos), end_pos(_end_pos), + relaxed(_relaxed) {} + }; + + bool print(std::ostream& out, const context_t& context = context_t()) const; + void dump(std::ostream& out, const int depth) const; + + static ptr_op_t wrap_value(const value_t& val); + static ptr_op_t wrap_functor(const expr_t::func_t& fobj); + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & refc; + ar & kind; + if (Archive::is_loading::value || ! left_ || left_->kind != FUNCTION) { + ar & left_; + } else { + ptr_op_t temp_op; + ar & temp_op; + } + if (Archive::is_loading::value || kind == VALUE || kind == IDENT || + (kind > UNARY_OPERATORS && + (! has_right() || ! right()->is_function()))) { + ar & data; + } else { + variant<ptr_op_t, value_t, string, expr_t::func_t> temp_data; + ar & temp_data; + } + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline expr_t::ptr_op_t +expr_t::op_t::new_node(kind_t _kind, ptr_op_t _left, ptr_op_t _right) +{ + ptr_op_t node(new op_t(_kind)); + if (_left) node->set_left(_left); + if (_right) node->set_right(_right); + return node; +} + +inline expr_t::ptr_op_t expr_t::op_t::wrap_value(const value_t& val) { + ptr_op_t temp(new op_t(op_t::VALUE)); + temp->set_value(val); + return temp; +} + +inline expr_t::ptr_op_t +expr_t::op_t::wrap_functor(const expr_t::func_t& fobj) { + ptr_op_t temp(new op_t(op_t::FUNCTION)); + temp->set_function(fobj); + return temp; +} + +#define MAKE_FUNCTOR(x) expr_t::op_t::wrap_functor(bind(&x, this, _1)) +#define WRAP_FUNCTOR(x) expr_t::op_t::wrap_functor(x) + +string op_context(const expr_t::ptr_op_t op, + const expr_t::ptr_op_t locus = NULL); + +} // namespace ledger + +#endif // _OP_H diff --git a/src/option.cc b/src/option.cc new file mode 100644 index 00000000..d2ec8808 --- /dev/null +++ b/src/option.cc @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2003-2009, 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 "option.h" + +namespace ledger { + +namespace { + typedef std::pair<expr_t::ptr_op_t, bool> op_bool_tuple; + + op_bool_tuple find_option(scope_t& scope, const string& name) + { + char buf[128]; + char * p = buf; + foreach (char ch, name) { + if (ch == '-') + *p++ = '_'; + else + *p++ = ch; + } + *p++ = '_'; + *p = '\0'; + + if (expr_t::ptr_op_t op = scope.lookup(symbol_t::OPTION, buf)) + return op_bool_tuple(op, true); + + *--p = '\0'; + + return op_bool_tuple(scope.lookup(symbol_t::OPTION, buf), false); + } + + op_bool_tuple find_option(scope_t& scope, const char letter) + { + char buf[4]; + buf[0] = letter; + buf[1] = '_'; + buf[2] = '\0'; + + if (expr_t::ptr_op_t op = scope.lookup(symbol_t::OPTION, buf)) + return op_bool_tuple(op, true); + + buf[1] = '\0'; + + return op_bool_tuple(scope.lookup(symbol_t::OPTION, buf), false); + } + + void process_option(const string& whence, const expr_t::func_t& opt, + scope_t& scope, const char * arg, const string& name) + { + try { + call_scope_t args(scope); + + args.push_back(string_value(whence)); + if (arg) + args.push_back(string_value(arg)); + + opt(args); + } + catch (const std::exception& err) { + if (name[0] == '-') + add_error_context(_("While parsing option '%1'") << name); + + else + add_error_context(_("While parsing environent variable '%1'") << name); + throw; + } + } +} + +bool process_option(const string& whence, const string& name, scope_t& scope, + const char * arg, const string& varname) +{ + op_bool_tuple opt(find_option(scope, name)); + if (opt.first) { + process_option(whence, opt.first->as_function(), scope, arg, varname); + return true; + } + return false; +} + +void process_environment(const char ** envp, const string& tag, + scope_t& scope) +{ + const char * tag_p = tag.c_str(); + string::size_type tag_len = tag.length(); + + assert(tag_p); + assert(tag_len > 0); + + for (const char ** p = envp; *p; p++) { + if (std::strlen(*p) >= tag_len && std::strncmp(*p, tag_p, tag_len) == 0) { + char buf[8192]; + char * r = buf; + const char * q; + for (q = *p + tag_len; + *q && *q != '=' && r - buf < 8191; + q++) + if (*q == '_') + *r++ = '-'; + else + *r++ = static_cast<char>(std::tolower(*q)); + *r = '\0'; + + if (*q == '=') { + try { + string value = string(*p, q - *p); + if (! value.empty()) + process_option(string("$") + buf, string(buf), scope, q + 1, value); + } + catch (const std::exception& err) { + add_error_context(_("While parsing environment variable option '%1':") + << *p); + throw; + } + } + } + } +} + +namespace { + struct op_bool_char_tuple { + expr_t::ptr_op_t op; + bool truth; + char ch; + + op_bool_char_tuple(expr_t::ptr_op_t _op, bool _truth, char _ch) + : op(_op), truth(_truth), ch(_ch) {} + }; +} + +strings_list process_arguments(strings_list args, scope_t& scope) +{ + bool anywhere = true; + + strings_list remaining; + + for (strings_list::iterator i = args.begin(); + i != args.end(); + i++) { + DEBUG("option.args", "Examining argument '" << *i << "'"); + + if (! anywhere || (*i)[0] != '-') { + DEBUG("option.args", " adding to list of real args"); + remaining.push_back(*i); + continue; + } + + // --long-option or -s + if ((*i)[1] == '-') { + if ((*i)[2] == '\0') { + DEBUG("option.args", " it's a --, ending options processing"); + anywhere = false; + continue; + } + + DEBUG("option.args", " it's an option string"); + + string opt_name; + const char * name = (*i).c_str() + 2; + const char * value = NULL; + + if (const char * p = std::strchr(name, '=')) { + opt_name = string(name, p - name); + value = ++p; + DEBUG("option.args", " read option value from option: " << value); + } else { + opt_name = name; + } + + op_bool_tuple opt(find_option(scope, opt_name)); + if (! opt.first) + throw_(option_error, _("Illegal option --%1") << name); + + if (opt.second && ! value && ++i != args.end() && value == NULL) { + value = (*i).c_str(); + DEBUG("option.args", " read option value from arg: " << value); + if (value == NULL) + throw_(option_error, _("Missing option argument for --%1") << name); + } + process_option(string("--") + name, + opt.first->as_function(), scope, value, + string("--") + name); + } + else if ((*i)[1] == '\0') { + throw_(option_error, _("illegal option -%1") << (*i)[0]); + } + else { + DEBUG("option.args", " single-char option"); + + std::list<op_bool_char_tuple> option_queue; + + int x = 1; + for (char c = (*i)[x]; c != '\0'; x++, c = (*i)[x]) { + op_bool_tuple opt(find_option(scope, c)); + if (! opt.first) + throw_(option_error, _("Illegal option -%1") << c); + + option_queue.push_back(op_bool_char_tuple(opt.first, opt.second, c)); + } + + foreach (op_bool_char_tuple& o, option_queue) { + const char * value = NULL; + if (o.truth && ++i != args.end()) { + value = (*i).c_str(); + DEBUG("option.args", " read option value from arg: " << value); + if (value == NULL) + throw_(option_error, + _("Missing option argument for -%1") << o.ch); + } + process_option(string("-") + o.ch, o.op->as_function(), scope, value, + string("-") + o.ch); + } + } + } + + return remaining; +} + +} // namespace ledger diff --git a/src/option.h b/src/option.h new file mode 100644 index 00000000..b81c2ce7 --- /dev/null +++ b/src/option.h @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2003-2009, 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 option.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _OPTION_H +#define _OPTION_H + +#include "scope.h" + +namespace ledger { + +class call_scope_t; + +template <typename T> +class option_t +{ +protected: + const char * name; + string::size_type name_len; + const char ch; + bool handled; + optional<string> source; + + option_t& operator=(const option_t&); + +public: + T * parent; + value_t 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); + } + option_t(const option_t& other) + : name(other.name), + name_len(other.name_len), + ch(other.ch), + handled(other.handled), + parent(NULL), + value(other.value), + wants_arg(other.wants_arg) + { + TRACE_CTOR(option_t, "copy"); + } + + virtual ~option_t() { + TRACE_DTOR(option_t); + } + + void report(std::ostream& out) const { + if (handled && source) { + if (wants_arg) { + out << desc() << " => "; + value.dump(out); + } else { + out << desc(); + } + out << " <" << *source << ">" << std::endl; + } + } + + string desc() const { + std::ostringstream out; + out << "--"; + for (const char * p = name; *p; p++) { + if (*p == '_') { + if (*(p + 1)) + out << '-'; + } else { + out << *p; + } + } + if (ch) + out << " (-" << ch << ")"; + return out.str(); + } + + operator bool() const { + return handled; + } + + string& str() { + assert(handled); + if (! value) + throw_(std::runtime_error, _("No argument provided for %1") << desc()); + return value.as_string_lval(); + } + + string str() const { + assert(handled); + if (! value) + throw_(std::runtime_error, _("No argument provided for %1") << desc()); + return value.as_string(); + } + + 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)); + } + virtual void on_with(const optional<string>& whence, + const value_t& val) { + handled = true; + value = val; + source = whence; + } + + void off() { + handled = false; + value = value_t(); + source = none; + } + + virtual void handler_thunk(call_scope_t&) {} + + virtual void handler(call_scope_t& args) { + if (wants_arg) { + if (args.size() < 2) + throw_(std::runtime_error, _("No argument provided for %1") << desc()); + else if (args.size() > 2) + 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[0].as_string(), args[1]); + } + else if (args.size() < 1) { + throw_(std::runtime_error, _("No argument provided for %1") << desc()); + } + else if (! args[0].is_string()) { + throw_(std::runtime_error, _("Context argument for %1 not a string") << desc()); + } + else { + on_only(args[0].as_string()); + } + + 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); + } + else if (wants_arg) { + if (handled) + return value; + else + return NULL_VALUE; + } + else { + return handled; + } + } +}; + +#define BEGIN(type, name) \ + struct name ## _option_t : public option_t<type> + +#define CTOR(type, name) \ + name ## _option_t() : option_t<type>(#name) +#define DECL1(type, name, vartype, var, value) \ + vartype var ; \ + 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 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)) + +#define MAKE_OPT_FUNCTOR(type, x) \ + expr_t::op_t::wrap_functor(bind(&option_t<type>::operator(), x, _1)) + +inline bool is_eq(const char * p, const char * n) { + // Test whether p matches n, substituting - in p for _ in n. + for (; *p && *n; p++, n++) { + if (! (*p == '-' && *n == '_' ) && *p != *n) + return false; + } + // Ignore any trailing underscore + return *p == *n || (! *p && *n == '_' && ! *(n + 1)); +} + +#define OPT(name) \ + if (is_eq(p, #name)) \ + return ((name ## _handler).parent = this, &(name ## _handler)) + +#define OPT_ALT(name, alt) \ + if (is_eq(p, #name) || is_eq(p, #alt)) \ + return ((name ## _handler).parent = this, &(name ## _handler)) + +#define OPT_(name) \ + if (! *(p + 1) || \ + ((name ## _handler).wants_arg && \ + *(p + 1) == '_' && ! *(p + 2)) || \ + is_eq(p, #name)) \ + return ((name ## _handler).parent = this, &(name ## _handler)) + +#define OPT_CH(name) \ + if (! *(p + 1) || \ + ((name ## _handler).wants_arg && \ + *(p + 1) == '_' && ! *(p + 2))) \ + return ((name ## _handler).parent = this, &(name ## _handler)) + +#define HANDLER(name) name ## _handler +#define HANDLED(name) HANDLER(name) + +#define OPTION(type, name) \ + BEGIN(type, name) \ + { \ + CTOR(type, name) {} \ + } \ + END(name) + +#define OPTION_(type, name, body) \ + BEGIN(type, name) \ + { \ + CTOR(type, name) {} \ + body \ + } \ + END(name) + +#define OPTION__(type, name, body) \ + BEGIN(type, name) \ + { \ + body \ + } \ + END(name) + +bool process_option(const string& whence, const string& name, scope_t& scope, + const char * arg, const string& varname); + +void process_environment(const char ** envp, const string& tag, + scope_t& scope); + +strings_list process_arguments(strings_list args, scope_t& scope); + +DECLARE_EXCEPTION(option_error, std::runtime_error); + +} // namespace ledger + +#endif // _OPTION_H diff --git a/src/output.cc b/src/output.cc new file mode 100644 index 00000000..71ec6d88 --- /dev/null +++ b/src/output.cc @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2003-2009, 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 "output.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "session.h" +#include "report.h" + +namespace ledger { + +format_posts::format_posts(report_t& _report, + const string& format, + bool _print_raw, + const optional<string>& _prepend_format) + : report(_report), last_xact(NULL), last_post(NULL), + print_raw(_print_raw) +{ + TRACE_CTOR(format_posts, "report&, const string&, bool"); + + const char * f = format.c_str(); + + if (const char * p = std::strstr(f, "%/")) { + first_line_format.parse_format(string(f, 0, p - f)); + const char * n = p + 2; + if (const char * p = std::strstr(n, "%/")) { + next_lines_format.parse_format(string(n, 0, p - n), + first_line_format); + between_format.parse_format(string(p + 2), + first_line_format); + } else { + next_lines_format.parse_format(string(n), first_line_format); + } + } else { + first_line_format.parse_format(format); + next_lines_format.parse_format(format); + } + + if (_prepend_format) + prepend_format.parse_format(*_prepend_format); +} + +void format_posts::flush() +{ + report.output_stream.flush(); +} + +void format_posts::operator()(post_t& post) +{ + std::ostream& out(report.output_stream); + + if (print_raw) { + if (! post.has_xdata() || + ! post.xdata().has_flags(POST_EXT_DISPLAYED)) { + if (last_xact != post.xact) { + if (last_xact) { + bind_scope_t xact_scope(report, *last_xact); + out << between_format(xact_scope); + } + print_item(out, *post.xact); + out << '\n'; + last_xact = post.xact; + } + post.xdata().add_flags(POST_EXT_DISPLAYED); + last_post = &post; + } + } + else if (! post.has_xdata() || + ! post.xdata().has_flags(POST_EXT_DISPLAYED)) { + bind_scope_t bound_scope(report, post); + + if (prepend_format) + out << prepend_format(bound_scope); + + if (last_xact != post.xact) { + if (last_xact) { + bind_scope_t xact_scope(report, *last_xact); + out << between_format(xact_scope); + } + out << first_line_format(bound_scope); + last_xact = post.xact; + } + else if (last_post && last_post->date() != post.date()) { + out << first_line_format(bound_scope); + } + else { + out << next_lines_format(bound_scope); + } + + post.xdata().add_flags(POST_EXT_DISPLAYED); + last_post = &post; + } +} + +format_accounts::format_accounts(report_t& _report, + const string& format, + const optional<string>& _prepend_format) + : report(_report), disp_pred() +{ + TRACE_CTOR(format_accounts, "report&, const string&"); + + const char * f = format.c_str(); + + if (const char * p = std::strstr(f, "%/")) { + account_line_format.parse_format(string(f, 0, p - f)); + const char * n = p + 2; + if (const char * p = std::strstr(n, "%/")) { + total_line_format.parse_format(string(n, 0, p - n), account_line_format); + separator_format.parse_format(string(p + 2), account_line_format); + } else { + total_line_format.parse_format(n, account_line_format); + } + } else { + account_line_format.parse_format(format); + total_line_format.parse_format(format, account_line_format); + } + + if (_prepend_format) + prepend_format.parse_format(*_prepend_format); +} + +std::size_t format_accounts::post_account(account_t& account, const bool flat) +{ + if (account.xdata().has_flags(ACCOUNT_EXT_TO_DISPLAY) && + ! account.xdata().has_flags(ACCOUNT_EXT_DISPLAYED)) { + if (! flat && account.parent && + account.parent->xdata().has_flags(ACCOUNT_EXT_TO_DISPLAY) && + ! account.parent->xdata().has_flags(ACCOUNT_EXT_DISPLAYED)) + post_account(*account.parent, flat); + + account.xdata().add_flags(ACCOUNT_EXT_DISPLAYED); + + bind_scope_t bound_scope(report, account); + + if (prepend_format) + static_cast<std::ostream&>(report.output_stream) + << prepend_format(bound_scope); + + static_cast<std::ostream&>(report.output_stream) + << account_line_format(bound_scope); + + return 1; + } + return 0; +} + +std::pair<std::size_t, std::size_t> +format_accounts::mark_accounts(account_t& account, const bool flat) +{ + std::size_t visited = 0; + std::size_t to_display = 0; + + foreach (accounts_map::value_type& pair, account.accounts) { + std::pair<std::size_t, std::size_t> i = mark_accounts(*pair.second, flat); + visited += i.first; + to_display += i.second; + } + +#if defined(DEBUG_ON) + DEBUG("account.display", "Considering account: " << account.fullname()); + if (account.has_xflags(ACCOUNT_EXT_VISITED)) + DEBUG("account.display", " it was visited itself"); + DEBUG("account.display", " it has " << visited << " visited children"); + DEBUG("account.display", + " it has " << to_display << " children to display"); +#endif + + if (account.parent && + (account.has_xflags(ACCOUNT_EXT_VISITED) || (! flat && visited > 0))) { + bind_scope_t bound_scope(report, account); + if ((! flat && to_display > 1) || + ((flat || to_display != 1 || + account.has_xflags(ACCOUNT_EXT_VISITED)) && + disp_pred(bound_scope))) { + account.xdata().add_flags(ACCOUNT_EXT_TO_DISPLAY); + DEBUG("account.display", "Marking account as TO_DISPLAY"); + to_display = 1; + } + visited = 1; + } + + return std::pair<std::size_t, std::size_t>(visited, to_display); +} + +void format_accounts::flush() +{ + std::ostream& out(report.output_stream); + + if (report.HANDLED(display_)) { + DEBUG("account.display", + "Account display predicate: " << report.HANDLER(display_).str()); + disp_pred.parse(report.HANDLER(display_).str()); + } + + mark_accounts(*report.session.journal->master, report.HANDLED(flat)); + + std::size_t displayed = 0; + + foreach (account_t * account, posted_accounts) + displayed += post_account(*account, report.HANDLED(flat)); + + if (displayed > 1 && + ! report.HANDLED(no_total) && ! report.HANDLED(percent)) { + bind_scope_t bound_scope(report, *report.session.journal->master); + out << separator_format(bound_scope); + + if (prepend_format) + static_cast<std::ostream&>(report.output_stream) + << prepend_format(bound_scope); + + out << total_line_format(bound_scope); + } + + out.flush(); +} + +void format_accounts::operator()(account_t& account) +{ + DEBUG("account.display", "Posting account: " << account.fullname()); + posted_accounts.push_back(&account); +} + +} // namespace ledger diff --git a/src/output.h b/src/output.h new file mode 100644 index 00000000..778a9335 --- /dev/null +++ b/src/output.h @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file output.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _OUTPUT_H +#define _OUTPUT_H + +#include "chain.h" +#include "predicate.h" +#include "format.h" +#include "account.h" + +namespace ledger { + +class xact_t; +class post_t; +class report_t; + +class format_posts : public item_handler<post_t> +{ +protected: + report_t& report; + format_t first_line_format; + format_t next_lines_format; + format_t between_format; + format_t prepend_format; + xact_t * last_xact; + post_t * last_post; + bool print_raw; + +public: + format_posts(report_t& _report, const string& format, + bool _print_raw = false, + const optional<string>& _prepend_format = none); + virtual ~format_posts() { + TRACE_DTOR(format_posts); + } + + virtual void flush(); + virtual void operator()(post_t& post); +}; + +class format_accounts : public item_handler<account_t> +{ +protected: + report_t& report; + format_t account_line_format; + format_t total_line_format; + format_t separator_format; + format_t prepend_format; + predicate_t disp_pred; + + std::list<account_t *> posted_accounts; + +public: + format_accounts(report_t& _report, const string& _format, + const optional<string>& _prepend_format = none); + virtual ~format_accounts() { + TRACE_DTOR(format_accounts); + } + + std::pair<std::size_t, std::size_t> + mark_accounts(account_t& account, const bool flat); + + virtual std::size_t post_account(account_t& account, const bool flat); + virtual void flush(); + + virtual void operator()(account_t& account); +}; + +} // namespace ledger + +#endif // _OUTPUT_H diff --git a/src/parser.cc b/src/parser.cc new file mode 100644 index 00000000..ef778411 --- /dev/null +++ b/src/parser.cc @@ -0,0 +1,517 @@ +/* + * Copyright (c) 2003-2009, 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 "parser.h" + +namespace ledger { + +expr_t::ptr_op_t +expr_t::parser_t::parse_value_term(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node; + + token_t& tok = next_token(in, tflags); + + switch (tok.kind) { + case token_t::VALUE: + node = new op_t(op_t::VALUE); + node->set_value(tok.value); + break; + + case token_t::IDENT: { + string ident = tok.value.as_string(); + + 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) { + ptr_op_t call_node(new op_t(op_t::O_CALL)); + 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))); + + if (node->has_right() && node->right()->kind == op_t::O_CONS) + node->set_right(node->right()->left()); + } else { + push_token(tok); + } + break; + } + + case token_t::LPAREN: + node = parse_value_expr(in, tflags.plus_flags(PARSE_PARTIAL) + .minus_flags(PARSE_SINGLE)); + tok = next_token(in, tflags); + if (tok.kind != token_t::RPAREN) + tok.expected(')'); + + if (node->kind == op_t::O_CONS) { + ptr_op_t prev(node); + node = new op_t(op_t::O_SEQ); + node->set_left(prev); + } + break; + + default: + 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_value_term(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)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + } else { + push_token(tok); + break; + } + } + } + + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_unary_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node; + + token_t& tok = next_token(in, tflags); + + switch (tok.kind) { + case token_t::EXCLAM: { + ptr_op_t term(parse_dot_expr(in, tflags)); + if (! term) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + + // A very quick optimization + if (term->kind == op_t::VALUE) { + term->as_value_lval().in_place_not(); + node = term; + } else { + node = new op_t(op_t::O_NOT); + node->set_left(term); + } + break; + } + + case token_t::MINUS: { + ptr_op_t term(parse_dot_expr(in, tflags)); + if (! term) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + + // A very quick optimization + if (term->kind == op_t::VALUE) { + term->as_value_lval().in_place_negate(); + node = term; + } else { + node = new op_t(op_t::O_NEG); + node->set_left(term); + } + break; + } + + default: + push_token(tok); + node = parse_dot_expr(in, tflags); + break; + } + + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_mul_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_unary_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::STAR || tok.kind == token_t::SLASH || + tok.kind == token_t::KW_DIV) { + ptr_op_t prev(node); + node = new op_t(tok.kind == token_t::STAR ? + op_t::O_MUL : op_t::O_DIV); + node->set_left(prev); + node->set_right(parse_unary_expr(in, tflags)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + } else { + push_token(tok); + break; + } + } + } + + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_add_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_mul_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::PLUS || + tok.kind == token_t::MINUS) { + ptr_op_t prev(node); + node = new op_t(tok.kind == token_t::PLUS ? + op_t::O_ADD : op_t::O_SUB); + node->set_left(prev); + node->set_right(parse_mul_expr(in, tflags)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + } else { + push_token(tok); + break; + } + } + } + + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_logic_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_add_expr(in, tflags)); + + if (node && ! tflags.has_flags(PARSE_SINGLE)) { + while (true) { + op_t::kind_t kind = op_t::LAST; + parse_flags_t _flags = tflags; + token_t& tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + bool negate = false; + + switch (tok.kind) { + case token_t::DEFINE: + kind = op_t::O_DEFINE; + break; + case token_t::EQUAL: + if (tflags.has_flags(PARSE_NO_ASSIGN)) + tok.rewind(in); + else + kind = op_t::O_EQ; + break; + case token_t::NEQUAL: + kind = op_t::O_EQ; + negate = true; + break; + case token_t::MATCH: + kind = op_t::O_MATCH; + break; + case token_t::NMATCH: + kind = op_t::O_MATCH; + negate = true; + break; + case token_t::LESS: + kind = op_t::O_LT; + break; + case token_t::LESSEQ: + kind = op_t::O_LTE; + break; + case token_t::GREATER: + kind = op_t::O_GT; + break; + case token_t::GREATEREQ: + kind = op_t::O_GTE; + break; + default: + push_token(tok); + goto exit_loop; + } + + if (kind != op_t::LAST) { + ptr_op_t prev(node); + node = new op_t(kind); + node->set_left(prev); + node->set_right(parse_add_expr(in, _flags)); + + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + + if (negate) { + prev = node; + node = new op_t(op_t::O_NOT); + node->set_left(prev); + } + } + } + } + + exit_loop: + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_and_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_logic_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::KW_AND) { + ptr_op_t prev(node); + node = new op_t(op_t::O_AND); + node->set_left(prev); + node->set_right(parse_logic_expr(in, tflags)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + } else { + push_token(tok); + break; + } + } + } + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_or_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_and_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::KW_OR) { + ptr_op_t prev(node); + node = new op_t(op_t::O_OR); + node->set_left(prev); + node->set_right(parse_and_expr(in, tflags)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + } else { + push_token(tok); + break; + } + } + } + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_querycolon_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_or_expr(in, tflags)); + + if (node && ! tflags.has_flags(PARSE_SINGLE)) { + token_t& tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + + if (tok.kind == token_t::QUERY) { + ptr_op_t prev(node); + node = new op_t(op_t::O_QUERY); + node->set_left(prev); + node->set_right(parse_or_expr(in, tflags)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + + token_t& next_tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + if (next_tok.kind != token_t::COLON) + next_tok.expected(':'); + + prev = node->right(); + ptr_op_t subnode = new op_t(op_t::O_COLON); + subnode->set_left(prev); + subnode->set_right(parse_or_expr(in, tflags)); + if (! subnode->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + + node->set_right(subnode); + } + else if (tok.kind == token_t::KW_IF) { + ptr_op_t if_op(parse_or_expr(in, tflags)); + if (! if_op) + throw_(parse_error, _("'if' keyword not followed by argument")); + + tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + if (tok.kind == token_t::KW_ELSE) { + ptr_op_t else_op(parse_or_expr(in, tflags)); + if (! else_op) + throw_(parse_error, _("'else' keyword not followed by argument")); + + ptr_op_t subnode = new op_t(op_t::O_COLON); + subnode->set_left(node); + subnode->set_right(else_op); + + node = new op_t(op_t::O_QUERY); + node->set_left(if_op); + node->set_right(subnode); + } else { + ptr_op_t null_node = new op_t(op_t::VALUE); + null_node->set_value(NULL_VALUE); + + ptr_op_t subnode = new op_t(op_t::O_COLON); + subnode->set_left(node); + subnode->set_right(null_node); + + node = new op_t(op_t::O_QUERY); + node->set_left(if_op); + node->set_right(subnode); + + push_token(tok); + } + } + else { + push_token(tok); + } + } + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse_value_expr(std::istream& in, + const parse_flags_t& tflags) const +{ + ptr_op_t node(parse_querycolon_expr(in, tflags)); + + if (node && ! tflags.has_flags(PARSE_SINGLE)) { + token_t& tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + + if (tok.kind == token_t::COMMA || tok.kind == token_t::SEMI) { + bool comma_op = tok.kind == token_t::COMMA; + + ptr_op_t prev(node); + node = new op_t(comma_op ? op_t::O_CONS : op_t::O_SEQ); + node->set_left(prev); + node->set_right(parse_value_expr(in, tflags)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol); + + tok = next_token(in, tflags.plus_flags(PARSE_OP_CONTEXT)); + } + + if (tok.kind != token_t::TOK_EOF) { + if (tflags.has_flags(PARSE_PARTIAL)) + push_token(tok); + else + tok.unexpected(); + } + } + else if (! tflags.has_flags(PARSE_PARTIAL) && + ! tflags.has_flags(PARSE_SINGLE)) { + throw_(parse_error, _("Failed to parse value expression")); + } + + return node; +} + +expr_t::ptr_op_t +expr_t::parser_t::parse(std::istream& in, + const parse_flags_t& flags, + const optional<string>& original_string) +{ + try { + ptr_op_t top_node = parse_value_expr(in, flags); + + if (use_lookahead) { + use_lookahead = false; + lookahead.rewind(in); + } + lookahead.clear(); + + return top_node; + } + catch (const std::exception& err) { + if (original_string) { + add_error_context(_("While parsing value expression:")); + + std::streamoff end_pos = 0; + if (in.good()) + end_pos = in.tellg(); + std::streamoff pos = end_pos; + + if (pos > 0) + pos -= lookahead.length; + + DEBUG("parser.error", "original_string = '" << *original_string << "'"); + DEBUG("parser.error", " pos = " << pos); + DEBUG("parser.error", " end_pos = " << end_pos); + DEBUG("parser.error", " token kind = " << int(lookahead.kind)); + DEBUG("parser.error", " token length = " << lookahead.length); + + add_error_context(line_context(*original_string, + static_cast<string::size_type>(pos), + static_cast<string::size_type>(end_pos))); + } + throw; + } +} + +} // namespace ledger diff --git a/src/parser.h b/src/parser.h new file mode 100644 index 00000000..7a69fe08 --- /dev/null +++ b/src/parser.h @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2003-2009, 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 parser.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _PARSER_H +#define _PARSER_H + +#include "token.h" +#include "op.h" + +namespace ledger { + +class expr_t::parser_t : public noncopyable +{ + mutable token_t lookahead; + mutable bool use_lookahead; + + token_t& next_token(std::istream& in, const parse_flags_t& tflags) const { + if (use_lookahead) + use_lookahead = false; + else + lookahead.next(in, tflags); + return lookahead; + } + + void push_token(const token_t& tok) const { + assert(&tok == &lookahead); + use_lookahead = true; + } + void push_token() const { + use_lookahead = true; + } + + ptr_op_t parse_value_term(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, + const parse_flags_t& flags) const; + ptr_op_t parse_mul_expr(std::istream& in, + const parse_flags_t& flags) const; + ptr_op_t parse_add_expr(std::istream& in, + const parse_flags_t& flags) const; + ptr_op_t parse_logic_expr(std::istream& in, + const parse_flags_t& flags) const; + ptr_op_t parse_and_expr(std::istream& in, + const parse_flags_t& flags) const; + ptr_op_t parse_or_expr(std::istream& in, + const parse_flags_t& flags) const; + ptr_op_t parse_querycolon_expr(std::istream& in, + const parse_flags_t& flags) const; + ptr_op_t parse_value_expr(std::istream& in, + const parse_flags_t& flags) const; + +public: + parser_t() : use_lookahead(false) { + TRACE_CTOR(parser_t, ""); + } + ~parser_t() throw() { + TRACE_DTOR(parser_t); + } + + ptr_op_t parse(std::istream& in, + const parse_flags_t& flags = PARSE_DEFAULT, + const optional<string>& original_string = NULL); +}; + +} // namespace ledger + +#endif // _PARSER_H diff --git a/src/pool.cc b/src/pool.cc new file mode 100644 index 00000000..70f4eed6 --- /dev/null +++ b/src/pool.cc @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2003-2009, 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 "amount.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" +#include "quotes.h" + +namespace ledger { + +shared_ptr<commodity_pool_t> commodity_pool_t::current_pool; + +commodity_pool_t::commodity_pool_t() + : default_commodity(NULL), keep_base(false), + 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); +} + +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)); + + DEBUG("amounts.commodities", "Creating base commodity " << symbol); + + // Create the "qualified symbol" version of this commodity's symbol + if (commodity_t::symbol_needs_quotes(symbol)) { + commodity->qualified_symbol = "\""; + *commodity->qualified_symbol += symbol; + *commodity->qualified_symbol += "\""; + } + + DEBUG("amounts.commodities", + "Creating commodity '" << commodity->symbol() << "'"); + + std::pair<commodities_map::iterator, bool> result + = commodities.insert(commodities_map::value_type(commodity->mapping_key(), + commodity.get())); + assert(result.second); + + return commodity.release(); +} + +commodity_t * commodity_pool_t::find_or_create(const string& symbol) +{ + DEBUG("amounts.commodities", "Find-or-create commodity " << symbol); + + commodity_t * commodity = find(symbol); + if (commodity) + return commodity; + return create(symbol); +} + +commodity_t * commodity_pool_t::find(const string& symbol) +{ + DEBUG("amounts.commodities", "Find commodity " << symbol); + + commodities_map::const_iterator i = commodities.find(symbol); + if (i != commodities.end()) + return (*i).second; + return NULL; +} + +commodity_t * +commodity_pool_t::create(const string& symbol, const annotation_t& details) +{ + commodity_t * new_comm = create(symbol); + if (! new_comm) + return NULL; + + if (details) + return find_or_create(*new_comm, details); + else + return new_comm; +} + +string commodity_pool_t::make_qualified_name(const commodity_t& comm, + const annotation_t& details) +{ + assert(details); + + if (details.price && details.price->sign() < 0) + throw_(amount_error, _("A commodity's price may not be negative")); + + std::ostringstream name; + comm.print(name); + details.print(name, comm.pool().keep_base); + + DEBUG("amounts.commodities", "make_qualified_name for " + << *comm.qualified_symbol << std::endl << details); + DEBUG("amounts.commodities", "qualified_name is " << name.str()); + + return name.str(); +} + +commodity_t * +commodity_pool_t::find(const string& symbol, const annotation_t& details) +{ + commodity_t * comm = find(symbol); + if (! comm) + return NULL; + + if (details) { + string name = make_qualified_name(*comm, details); + + if (commodity_t * ann_comm = find(name)) { + assert(ann_comm->annotated && as_annotated_commodity(*ann_comm).details); + return ann_comm; + } + return NULL; + } else { + return comm; + } +} + +commodity_t * +commodity_pool_t::find_or_create(const string& symbol, + const annotation_t& details) +{ + commodity_t * comm = find(symbol); + if (! comm) + return NULL; + + if (details) + return find_or_create(*comm, details); + else + return comm; +} + +commodity_t * +commodity_pool_t::create(commodity_t& comm, + const annotation_t& details, + const string& mapping_key) +{ + assert(comm); + assert(details); + assert(! mapping_key.empty()); + + std::auto_ptr<commodity_t> commodity + (new annotated_commodity_t(&comm, details)); + + commodity->qualified_symbol = comm.symbol(); + assert(! commodity->qualified_symbol->empty()); + + DEBUG("amounts.commodities", "Creating annotated commodity " + << "symbol " << commodity->symbol() + << " key " << mapping_key << 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())); + assert(result.second); + + 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); +} + +void commodity_pool_t::exchange(commodity_t& commodity, + const amount_t& per_unit_cost, + const datetime_t& moment) +{ + DEBUG("commodity.prices.add", "exchanging commodity " << commodity + << " at per unit cost " << per_unit_cost << " on " << moment); + + commodity_t& base_commodity + (commodity.annotated ? + as_annotated_commodity(commodity).referent() : commodity); + + base_commodity.add_price(moment, per_unit_cost); +} + +cost_breakdown_t +commodity_pool_t::exchange(const amount_t& amount, + const amount_t& cost, + const bool is_per_unit, + const optional<datetime_t>& moment, + const optional<string>& tag) +{ + DEBUG("commodity.prices.add", "exchange: " << amount << " for " << cost); + DEBUG("commodity.prices.add", "exchange: is-per-unit = " << is_per_unit); +#if defined(DEBUG_ON) + if (moment) + DEBUG("commodity.prices.add", "exchange: moment = " << *moment); + if (tag) + DEBUG("commodity.prices.add", "exchange: tag = " << *tag); +#endif + + commodity_t& commodity(amount.commodity()); + + annotation_t * current_annotation = NULL; + if (commodity.annotated) + current_annotation = &as_annotated_commodity(commodity).details; + + amount_t per_unit_cost = + (is_per_unit || amount.is_realzero() ? cost : cost / amount).abs(); + + DEBUG("commodity.prices.add", "exchange: per-unit-cost = " << per_unit_cost); + + if (! per_unit_cost.is_realzero()) + exchange(commodity, per_unit_cost, moment ? *moment : CURRENT_TIME()); + + cost_breakdown_t breakdown; + breakdown.final_cost = ! is_per_unit ? cost : cost * amount; + + DEBUG("commodity.prices.add", + "exchange: final-cost = " << breakdown.final_cost); + + if (current_annotation && current_annotation->price) + breakdown.basis_cost + = (*current_annotation->price * amount).unrounded(); + else + breakdown.basis_cost = breakdown.final_cost; + + DEBUG("commodity.prices.add", + "exchange: basis-cost = " << breakdown.basis_cost); + + annotation_t annotation(per_unit_cost, moment ? + moment->date() : optional<date_t>(), tag); + + annotation.add_flags(ANNOTATION_PRICE_CALCULATED); + if (moment) + annotation.add_flags(ANNOTATION_DATE_CALCULATED); + if (tag) + annotation.add_flags(ANNOTATION_TAG_CALCULATED); + + breakdown.amount = amount_t(amount, annotation); + + DEBUG("commodity.prices.add", + "exchange: amount = " << breakdown.amount); + + return breakdown; +} + +optional<std::pair<commodity_t *, price_point_t> > +commodity_pool_t::parse_price_directive(char * line, bool do_not_add_price) +{ + char * date_field_ptr = line; + char * time_field_ptr = next_element(date_field_ptr); + if (! time_field_ptr) return none; + string date_field = date_field_ptr; + + char * symbol_and_price; + datetime_t datetime; + string symbol; + + if (std::isdigit(time_field_ptr[0])) { + symbol_and_price = next_element(time_field_ptr); + if (! symbol_and_price) return none; + + datetime = parse_datetime(date_field + " " + time_field_ptr); + } + else if (std::isdigit(date_field_ptr[0])) { + symbol_and_price = time_field_ptr; + datetime = datetime_t(parse_date(date_field)); + } + else { + symbol = date_field_ptr; + symbol_and_price = time_field_ptr; + datetime = CURRENT_TIME(); + } + + if (symbol.empty()) + commodity_t::parse_symbol(symbol_and_price, symbol); + + price_point_t point; + point.when = datetime; + point.price.parse(symbol_and_price, PARSE_NO_MIGRATE); + VERIFY(point.price.valid()); + + DEBUG("commodity.download", "Looking up symbol: " << symbol); + if (commodity_t * commodity = find_or_create(symbol)) { + DEBUG("commodity.download", "Adding price for " << symbol << ": " + << point.when << " " << point.price); + if (! do_not_add_price) + commodity->add_price(point.when, point.price, true); + commodity->add_flags(COMMODITY_KNOWN); + return std::pair<commodity_t *, price_point_t>(commodity, point); + } + + return none; +} + +commodity_t * +commodity_pool_t::parse_price_expression(const std::string& str, + const bool add_prices, + const optional<datetime_t>& moment) +{ + scoped_array<char> buf(new char[str.length() + 1]); + + std::strcpy(buf.get(), str.c_str()); + + char * price = std::strchr(buf.get(), '='); + if (price) + *price++ = '\0'; + + if (commodity_t * commodity = find_or_create(trim_ws(buf.get()))) { + if (price && add_prices) { + for (char * p = std::strtok(price, ";"); + p; + p = std::strtok(NULL, ";")) { + commodity->add_price(moment ? *moment : CURRENT_TIME(), amount_t(p)); + } + } + return commodity; + } + return NULL; +} + +} // namespace ledger diff --git a/src/pool.h b/src/pool.h new file mode 100644 index 00000000..995ab23c --- /dev/null +++ b/src/pool.h @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2003-2009, 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 pool.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Types for managing commodity pools + * + * Long. + */ +#ifndef _POOL_H +#define _POOL_H + +namespace ledger { + +struct cost_breakdown_t +{ + amount_t amount; + amount_t final_cost; + amount_t basis_cost; +}; + +class commodity_pool_t : public noncopyable +{ +public: + /** + * The commodities collection in commodity_pool_t maintains pointers to all + * the commodities which have ever been created by the user, whether + * 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 + + optional<path> price_db; // --price-db= + long quote_leeway; // --leeway= + bool get_quotes; // --download + + static shared_ptr<commodity_pool_t> current_pool; + + function<optional<price_point_t> + (commodity_t& commodity, const optional<commodity_t&>& in_terms_of)> + get_commodity_quote; + + 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 * 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 * create(commodity_t& comm, + const annotation_t& details, + const string& mapping_key); + + commodity_t * find_or_create(commodity_t& comm, + const annotation_t& details); + + // Exchange one commodity for another, while recording the factored price. + + void exchange(commodity_t& commodity, + const amount_t& per_unit_cost, + const datetime_t& moment); + + cost_breakdown_t exchange(const amount_t& amount, + const amount_t& cost, + const bool is_per_unit = false, + const optional<datetime_t>& moment = none, + const optional<string>& tag = none); + + // Parse commodity prices from a textual representation + + optional<std::pair<commodity_t *, price_point_t> > + parse_price_directive(char * line, bool do_not_add_price = false); + + commodity_t * + parse_price_expression(const std::string& str, + const bool add_prices = true, + const optional<datetime_t>& moment = 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 & current_pool; + ar & commodities; + ar & null_commodity; + ar & default_commodity; + ar & keep_base; + ar & price_db; + ar & quote_leeway; + ar & get_quotes; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +} // namespace ledger + +#endif // _POOL_H diff --git a/src/post.cc b/src/post.cc new file mode 100644 index 00000000..34284e1b --- /dev/null +++ b/src/post.cc @@ -0,0 +1,576 @@ +/* + * Copyright (c) 2003-2009, 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 "post.h" +#include "xact.h" +#include "account.h" +#include "journal.h" +#include "interactive.h" +#include "format.h" + +namespace ledger { + +bool post_t::has_tag(const string& tag) const +{ + if (item_t::has_tag(tag)) + return true; + if (xact) + return xact->has_tag(tag); + return false; +} + +bool post_t::has_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask) const +{ + if (item_t::has_tag(tag_mask, value_mask)) + return true; + if (xact) + return xact->has_tag(tag_mask, value_mask); + return false; +} + +optional<string> post_t::get_tag(const string& tag) const +{ + if (optional<string> value = item_t::get_tag(tag)) + return value; + if (xact) + return xact->get_tag(tag); + return none; +} + +optional<string> post_t::get_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask) const +{ + if (optional<string> value = item_t::get_tag(tag_mask, value_mask)) + return value; + if (xact) + return xact->get_tag(tag_mask, value_mask); + return none; +} + +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 (! _date) { + assert(xact); + return xact->date(); + } + return *_date; +} + +optional<date_t> post_t::effective_date() const +{ + optional<date_t> date = item_t::effective_date(); + if (! date && xact) + return xact->effective_date(); + return date; +} + +namespace { + value_t get_this(post_t& post) { + return value_t(static_cast<scope_t *>(&post)); + } + + value_t get_is_calculated(post_t& post) { + return post.has_flags(POST_CALCULATED); + } + + value_t get_is_cost_calculated(post_t& post) { + return post.has_flags(POST_COST_CALCULATED); + } + + value_t get_virtual(post_t& post) { + return post.has_flags(POST_VIRTUAL); + } + + value_t get_real(post_t& post) { + return ! post.has_flags(POST_VIRTUAL); + } + + value_t get_xact(post_t& post) { + return value_t(static_cast<scope_t *>(post.xact)); + } + + value_t get_code(post_t& post) { + if (post.xact->code) + return string_value(*post.xact->code); + else + return string_value(empty_string); + } + + value_t get_payee(post_t& post) { + return string_value(post.xact->payee); + } + + value_t get_note(post_t& post) { + string note = post.note ? *post.note : empty_string; + note += post.xact->note ? *post.xact->note : empty_string; + return string_value(note); + } + + 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) { + if (post.has_xdata() && post.xdata().has_flags(POST_EXT_COMPOUND)) + return post.xdata().compound_value; + else if (post.amount.is_null()) + return 0L; + else + return post.amount; + } + + value_t get_use_direct_amount(post_t& post) { + return post.has_xdata() && post.xdata().has_flags(POST_EXT_DIRECT_AMT); + } + + value_t get_commodity(post_t& post) { + return string_value(post.amount.commodity().symbol()); + } + + value_t get_commodity_is_primary(post_t& post) { + return post.amount.commodity().has_flags(COMMODITY_PRIMARY); + } + + value_t get_has_cost(post_t& post) { + return post.cost ? true : false; + } + + value_t get_cost(post_t& post) { + if (post.cost) + return *post.cost; + else if (post.has_xdata() && + post.xdata().has_flags(POST_EXT_COMPOUND)) + return post.xdata().compound_value; + else if (post.amount.is_null()) + return 0L; + else + return post.amount; + } + + value_t get_total(post_t& post) { + if (post.xdata_ && ! post.xdata_->total.is_null()) + return post.xdata_->total; + else if (post.amount.is_null()) + return 0L; + else + return post.amount; + } + + value_t get_count(post_t& post) { + if (post.xdata_) + return long(post.xdata_->count); + else + return 1L; + } + + value_t get_account(call_scope_t& scope) + { + in_context_t<post_t> env(scope, "&v"); + + string name; + + if (env.has(0)) { + if (env.value_at(0).is_long()) { + if (env.get<long>(0) > 2) + name = format_t::truncate(env->reported_account()->fullname(), + env.get<long>(0) - 2, + 2 /* account_abbrev_length */); + else + name = env->reported_account()->fullname(); + } else { + account_t * account = NULL; + account_t * master = env->account; + while (master->parent) + master = master->parent; + + if (env.value_at(0).is_string()) { + name = env.get<string>(0); + account = master->find_account(name, false); + } + else if (env.value_at(0).is_mask()) { + name = env.get<mask_t>(0).str(); + account = master->find_account_re(name); + } + else { + throw_(std::runtime_error, + _("Expected string or mask for argument 1, but received %1") + << env.value_at(0).label()); + } + + if (! account) + throw_(std::runtime_error, + _("Could not find an account matching ") << env.value_at(0)); + else + return value_t(static_cast<scope_t *>(account)); + } + } else { + name = env->reported_account()->fullname(); + } + + if (env->has_flags(POST_VIRTUAL)) { + if (env->must_balance()) + name = string("[") + name + "]"; + else + name = string("(") + name + ")"; + } + return string_value(name); + } + + value_t get_account_base(post_t& post) { + return string_value(post.reported_account()->name); + } + + value_t get_account_depth(post_t& post) { + return long(post.reported_account()->depth); + } + + value_t get_datetime(post_t& post) { + return post.xdata().datetime; + } + + template <value_t (*Func)(post_t&)> + value_t get_wrapper(call_scope_t& scope) { + return (*Func)(find_scope<post_t>(scope)); + } +} + +expr_t::ptr_op_t post_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (kind != symbol_t::FUNCTION) + return item_t::lookup(kind, name); + + switch (name[0]) { + case 'a': + if (name[1] == '\0' || name == "amount") + return WRAP_FUNCTOR(get_wrapper<&get_amount>); + else if (name == "account") + return WRAP_FUNCTOR(get_account); + else if (name == "account_base") + return WRAP_FUNCTOR(get_wrapper<&get_account_base>); + break; + + case 'b': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_cost>); + break; + + case 'c': + if (name == "code") + return WRAP_FUNCTOR(get_wrapper<&get_code>); + else if (name == "cost") + return WRAP_FUNCTOR(get_wrapper<&get_cost>); + else if (name == "cost_calculated") + return WRAP_FUNCTOR(get_wrapper<&get_is_cost_calculated>); + else if (name == "count") + return WRAP_FUNCTOR(get_wrapper<&get_count>); + else if (name == "calculated") + return WRAP_FUNCTOR(get_wrapper<&get_is_calculated>); + else if (name == "commodity") + return WRAP_FUNCTOR(get_wrapper<&get_commodity>); + break; + + case 'd': + if (name == "depth") + return WRAP_FUNCTOR(get_wrapper<&get_account_depth>); + else if (name == "datetime") + return WRAP_FUNCTOR(get_wrapper<&get_datetime>); + break; + + case 'h': + if (name == "has_cost") + return WRAP_FUNCTOR(get_wrapper<&get_has_cost>); + break; + + 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': + if (name == "magnitude") + return WRAP_FUNCTOR(get_wrapper<&get_magnitude>); + break; + + case 'n': + if (name == "note") + return WRAP_FUNCTOR(get_wrapper<&get_note>); + else if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_count>); + break; + + case 'p': + if (name == "post") + return WRAP_FUNCTOR(get_wrapper<&get_this>); + else if (name == "payee") + return WRAP_FUNCTOR(get_wrapper<&get_payee>); + else if (name == "primary") + return WRAP_FUNCTOR(get_wrapper<&get_commodity_is_primary>); + else if (name == "parent") + return WRAP_FUNCTOR(get_wrapper<&get_xact>); + break; + + case 'r': + if (name == "real") + return WRAP_FUNCTOR(get_wrapper<&get_real>); + break; + + case 't': + if (name == "total") + return WRAP_FUNCTOR(get_wrapper<&get_total>); + break; + + case 'u': + if (name == "use_direct_amount") + return WRAP_FUNCTOR(get_wrapper<&get_use_direct_amount>); + break; + + case 'v': + if (name == "virtual") + return WRAP_FUNCTOR(get_wrapper<&get_virtual>); + break; + + case 'x': + if (name == "xact") + return WRAP_FUNCTOR(get_wrapper<&get_xact>); + break; + + case 'N': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_count>); + break; + + case 'O': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_total>); + break; + + case 'R': + if (name[1] == '\0') + return WRAP_FUNCTOR(get_wrapper<&get_real>); + break; + } + + return item_t::lookup(kind, name); +} + +bool post_t::valid() const +{ + if (! xact) { + DEBUG("ledger.validate", "post_t: ! xact"); + return false; + } + + posts_list::const_iterator i = + std::find(xact->posts.begin(), + xact->posts.end(), this); + if (i == xact->posts.end()) { + DEBUG("ledger.validate", "post_t: ! found"); + return false; + } + + if (! account) { + DEBUG("ledger.validate", "post_t: ! account"); + return false; + } + + if (! amount.valid()) { + DEBUG("ledger.validate", "post_t: ! amount.valid()"); + return false; + } + + if (cost) { + if (! cost->valid()) { + DEBUG("ledger.validate", "post_t: cost && ! cost->valid()"); + return false; + } + if (! cost->keep_precision()) { + DEBUG("ledger.validate", "post_t: ! cost->keep_precision()"); + return false; + } + } + + return true; +} + +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); + } + else if (expr) { + bind_scope_t bound_scope(*expr->get_context(), + const_cast<post_t&>(*this)); +#if 1 + value_t temp(expr->calc(bound_scope)); + add_or_set_value(value, temp); +#else + if (! xdata_) xdata_ = xdata_t(); + xdata_->value = expr->calc(bound_scope); + xdata_->add_flags(POST_EXT_COMPOUND); + + add_or_set_value(value, xdata_->value); +#endif + } + else if (xdata_ && xdata_->has_flags(POST_EXT_VISITED) && + ! xdata_->visited_value.is_null()) { + add_or_set_value(value, xdata_->visited_value); + } + else { + add_or_set_value(value, amount); + } +} + +void post_t::set_reported_account(account_t * account) +{ + xdata().account = account; + account->xdata().reported_posts.push_back(this); +} + +void to_xml(std::ostream& out, const post_t& post) +{ + push_xml x(out, "posting", true); + + if (post.state() == item_t::CLEARED) + out << " state=\"cleared\""; + else if (post.state() == item_t::PENDING) + out << " state=\"pending\""; + + if (post.has_flags(POST_VIRTUAL)) + out << " virtual=\"true\""; + if (post.has_flags(ITEM_GENERATED)) + out << " generated=\"true\""; + + x.close_attrs(); + + if (post._date) { + 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.account) { + push_xml y(out, "account", true); + + out << " ref=\""; + out.width(sizeof(unsigned long) * 2); + out.fill('0'); + out << std::hex << reinterpret_cast<unsigned long>(post.account); + out << '"'; + y.close_attrs(); + + { + push_xml z(out, "name"); + out << z.guard(post.account->fullname()); + } + } + + { + push_xml y(out, "post-amount"); + if (post.has_xdata() && post.xdata().has_flags(POST_EXT_COMPOUND)) + to_xml(out, post.xdata().compound_value); + else + to_xml(out, post.amount); + } + + if (post.cost) { + push_xml y(out, "cost"); + to_xml(out, *post.cost); + } + + if (post.assigned_amount) { + if (post.has_flags(POST_CALCULATED)) { + push_xml y(out, "balance-assertion"); + to_xml(out, *post.assigned_amount); + } else { + push_xml y(out, "balance-assignment"); + to_xml(out, *post.assigned_amount); + } + } + + if (post.note) { + push_xml y(out, "note"); + out << y.guard(*post.note); + } + + if (post.metadata) { + push_xml y(out, "metadata"); + foreach (const item_t::string_map::value_type& pair, *post.metadata) { + if (pair.second) { + push_xml z(out, "variable"); + { + push_xml z(out, "key"); + out << y.guard(pair.first); + } + { + push_xml z(out, "value"); + out << y.guard(*pair.second); + } + } else { + push_xml z(out, "tag"); + out << y.guard(pair.first); + } + } + } + + if (post.xdata_ && ! post.xdata_->total.is_null()) { + push_xml y(out, "total"); + to_xml(out, post.xdata_->total); + } +} + +} // namespace ledger diff --git a/src/post.h b/src/post.h new file mode 100644 index 00000000..a0ab5ffd --- /dev/null +++ b/src/post.h @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2003-2009, 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 post.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _POST_H +#define _POST_H + +#include "item.h" + +namespace ledger { + +class xact_t; +class account_t; + +class post_t : public item_t +{ +public: +#define POST_VIRTUAL 0x10 // the account was specified with (parens) +#define POST_MUST_BALANCE 0x20 // posting must balance in the transaction +#define POST_CALCULATED 0x40 // posting's amount was calculated +#define POST_COST_CALCULATED 0x80 // posting's cost was calculated + + 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; + + post_t(account_t * _account = NULL, + flags_t _flags = ITEM_NORMAL) + : item_t(_flags), + xact(NULL), account(_account) + { + TRACE_CTOR(post_t, "account_t *, flags_t"); + } + post_t(account_t * _account, + const amount_t& _amount, + flags_t _flags = ITEM_NORMAL, + const optional<string>& _note = none) + : item_t(_flags, _note), + xact(NULL), account(_account), amount(_amount) + { + TRACE_CTOR(post_t, "account_t *, const amount_t&, flags_t, const optional<string>&"); + } + post_t(const post_t& post) + : item_t(post), + xact(post.xact), + account(post.account), + amount(post.amount), + cost(post.cost), + assigned_amount(post.assigned_amount), + xdata_(post.xdata_) + { + TRACE_CTOR(post_t, "copy"); + } + virtual ~post_t() { + TRACE_DTOR(post_t); + } + + virtual bool has_tag(const string& tag) const; + virtual bool has_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask = none) const; + + virtual optional<string> get_tag(const string& tag) const; + virtual optional<string> get_tag(const mask_t& tag_mask, + const optional<mask_t>& value_mask = none) const; + + virtual date_t date() const; + virtual optional<date_t> effective_date() const; + + bool must_balance() const { + return ! has_flags(POST_VIRTUAL) || has_flags(POST_MUST_BALANCE); + } + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + bool valid() const; + + struct xdata_t : public supports_flags<uint_least16_t> + { +#define POST_EXT_RECEIVED 0x0001 +#define POST_EXT_HANDLED 0x0002 +#define POST_EXT_DISPLAYED 0x0004 +#define POST_EXT_DIRECT_AMT 0x0008 +#define POST_EXT_SORT_CALC 0x0010 +#define POST_EXT_COMPOUND 0x0020 +#define POST_EXT_VISITED 0x0040 +#define POST_EXT_MATCHES 0x0080 +#define POST_EXT_CONSIDERED 0x0100 + + value_t visited_value; + value_t compound_value; + value_t total; + std::size_t count; + date_t date; + datetime_t datetime; + account_t * account; + + std::list<sort_value_t> sort_values; + + xdata_t() + : supports_flags<uint_least16_t>(), count(0), account(NULL) { + TRACE_CTOR(post_t::xdata_t, ""); + } + xdata_t(const xdata_t& other) + : supports_flags<uint_least16_t>(other.flags()), + visited_value(other.visited_value), + compound_value(other.compound_value), + total(other.total), + count(other.count), + date(other.date), + account(other.account), + sort_values(other.sort_values) + { + TRACE_CTOR(post_t::xdata_t, "copy"); + } + ~xdata_t() throw() { + TRACE_DTOR(post_t::xdata_t); + } + }; + + // This variable holds optional "extended data" which is usually produced + // only during reporting, and only for the posting set being reported. + // It's a memory-saving measure to delay allocation until the last possible + // moment. + mutable optional<xdata_t> xdata_; + + bool has_xdata() const { + return xdata_; + } + void clear_xdata() { + xdata_ = none; + } + xdata_t& xdata() { + if (! xdata_) + xdata_ = xdata_t(); + return *xdata_; + } + const xdata_t& xdata() const { + return const_cast<post_t *>(this)->xdata(); + } + + void add_to_value(value_t& value, + const optional<expr_t&>& expr = none) const; + + void set_reported_account(account_t * account); + + account_t * reported_account() { + if (xdata_) + if (account_t * acct = xdata_->account) + return acct; + return account; + } + + const account_t * reported_account() const { + return const_cast<post_t *>(this)->reported_account(); + } + + friend class xact_t; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<item_t>(*this); + ar & xact; + ar & account; + ar & amount; + ar & amount_expr; + ar & cost; + ar & assigned_amount; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +void to_xml(std::ostream& out, const post_t& post); + +} // namespace ledger + +#endif // _POST_H diff --git a/src/precmd.cc b/src/precmd.cc new file mode 100644 index 00000000..31249016 --- /dev/null +++ b/src/precmd.cc @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2003-2009, 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 "precmd.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "query.h" +#include "session.h" +#include "report.h" +#include "format.h" + +namespace ledger { + +namespace { + post_t * get_sample_xact(report_t& report) + { + { + string str; + { + std::ostringstream buf; + + buf << "2004/05/27 Book Store\n" + << " ; This note applies to all postings. :SecondTag:\n" + << " Expenses:Books 20 BOOK @ $10\n" + << " ; Metadata: Some Value\n" + << " ; :ExampleTag:\n" + << " ; Here follows a note describing the posting.\n" + << " Liabilities:MasterCard $-200.00\n"; + + str = buf.str(); + } + + std::ostream& out(report.output_stream); + + 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); + report.session.journal->clear_xdata(); + } + } + xact_t * first = report.session.journal->xacts.front(); + return first->posts.front(); + } +} + +value_t parse_command(call_scope_t& args) +{ + string arg = join_args(args); + if (arg.empty()) { + throw std::logic_error(_("Usage: parse TEXT")); + return 1L; + } + + report_t& report(find_scope<report_t>(args)); + std::ostream& out(report.output_stream); + + post_t * post = get_sample_xact(report); + + out << _("--- Input expression ---") << std::endl; + out << arg << std::endl; + + out << std::endl << _("--- Text as parsed ---") << std::endl; + expr_t expr(arg); + expr.print(out); + out << std::endl; + + out << std::endl << _("--- Expression tree ---") << std::endl; + expr.dump(out); + + bind_scope_t bound_scope(args, *post); + expr.compile(bound_scope); + out << std::endl << _("--- Compiled tree ---") << std::endl; + expr.dump(out); + + out << std::endl << _("--- Calculated value ---") << std::endl; + value_t result(expr.calc()); + result.strip_annotations(report.what_to_keep()).dump(out); + out << std::endl; + + return NULL_VALUE; +} + +value_t eval_command(call_scope_t& args) +{ + report_t& report(find_scope<report_t>(args)); + expr_t expr(join_args(args)); + value_t result(expr.calc(args).strip_annotations(report.what_to_keep())); + + if (! result.is_null()) + report.output_stream << result << std::endl; + + return NULL_VALUE; +} + +value_t format_command(call_scope_t& args) +{ + string arg = join_args(args); + if (arg.empty()) { + throw std::logic_error(_("Usage: format TEXT")); + return 1L; + } + + report_t& report(find_scope<report_t>(args)); + std::ostream& out(report.output_stream); + + post_t * post = get_sample_xact(report); + + out << _("--- Input format string ---") << std::endl; + out << arg << std::endl << std::endl; + + out << _("--- Format elements ---") << std::endl; + format_t fmt(arg); + fmt.dump(out); + + out << std::endl << _("--- Formatted string ---") << std::endl; + bind_scope_t bound_scope(args, *post); + out << '"'; + out << fmt(bound_scope); + out << "\"\n"; + + return NULL_VALUE; +} + +value_t period_command(call_scope_t& args) +{ + string arg = join_args(args); + if (arg.empty()) { + throw std::logic_error(_("Usage: period TEXT")); + return 1L; + } + + report_t& report(find_scope<report_t>(args)); + std::ostream& out(report.output_stream); + + show_period_tokens(out, arg); + out << std::endl; + + date_interval_t interval(arg); + interval.dump(out, report.session.current_year); + + return NULL_VALUE; +} + +value_t args_command(call_scope_t& args) +{ + report_t& report(find_scope<report_t>(args)); + std::ostream& out(report.output_stream); + + out << _("--- Input arguments ---") << std::endl; + args.value().dump(out); + out << std::endl << std::endl; + + query_t query(args.value(), report.what_to_keep()); + if (query) { + call_scope_t sub_args(static_cast<scope_t&>(args)); + sub_args.push_back(string_value(query.text())); + + parse_command(sub_args); + } + + if (query.tokens_remaining()) { + out << std::endl << _("====== Display predicate ======") + << std::endl << std::endl; + + query.parse_again(); + + if (query) { + call_scope_t disp_sub_args(static_cast<scope_t&>(args)); + disp_sub_args.push_back(string_value(query.text())); + + parse_command(disp_sub_args); + } + } + + return NULL_VALUE; +} + +} // namespace ledger diff --git a/src/precmd.h b/src/precmd.h new file mode 100644 index 00000000..e0f81cf8 --- /dev/null +++ b/src/precmd.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file precmd.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _PRECMD_H +#define _PRECMD_H + +#include "value.h" + +namespace ledger { + +class call_scope_t; + +value_t parse_command(call_scope_t& args); +value_t eval_command(call_scope_t& args); +value_t format_command(call_scope_t& args); +value_t period_command(call_scope_t& args); +value_t args_command(call_scope_t& args); + +} // namespace ledger + +#endif // _PRECMD_H diff --git a/src/predicate.cc b/src/predicate.cc new file mode 100644 index 00000000..4da4decf --- /dev/null +++ b/src/predicate.cc @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2003-2009, 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 "predicate.h" +#include "query.h" +#include "op.h" + +namespace ledger { + +predicate_t::predicate_t(const query_t& other) + : expr_t(other), what_to_keep(other.what_to_keep) +{ + TRACE_CTOR(predicate_t, "query_t"); +} + +} // namespace ledger diff --git a/src/predicate.h b/src/predicate.h new file mode 100644 index 00000000..b3b81f9b --- /dev/null +++ b/src/predicate.h @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2003-2009, 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 predicate.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _PREDICATE_H +#define _PREDICATE_H + +#include "expr.h" +#include "commodity.h" +#include "annotate.h" + +namespace ledger { + +class query_t; + +class predicate_t : public expr_t +{ +public: + keep_details_t what_to_keep; + + predicate_t(const keep_details_t& _what_to_keep = keep_details_t()) + : what_to_keep(_what_to_keep) { + TRACE_CTOR(predicate_t, ""); + } + predicate_t(const predicate_t& other) + : expr_t(other), what_to_keep(other.what_to_keep) { + TRACE_CTOR(predicate_t, "copy"); + } + predicate_t(const query_t& other); + + predicate_t(const string& str, const keep_details_t& _what_to_keep, + const parse_flags_t& flags = PARSE_DEFAULT) + : expr_t(str, flags), what_to_keep(_what_to_keep) { + TRACE_CTOR(predicate_t, "string, keep_details_t, parse_flags_t"); + } + predicate_t(std::istream& in, const keep_details_t& _what_to_keep, + const parse_flags_t& flags = PARSE_DEFAULT) + : expr_t(in, flags), what_to_keep(_what_to_keep) { + TRACE_CTOR(predicate_t, "std::istream&, keep_details_t, parse_flags_t"); + } + virtual ~predicate_t() { + TRACE_DTOR(predicate_t); + } + + virtual value_t real_calc(scope_t& scope) { + return (*this ? + expr_t::real_calc(scope) + .strip_annotations(what_to_keep) + .to_boolean() : + true); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<expr_t>(*this); + ar & what_to_keep; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +} // namespace ledger + +#endif // _PREDICATE_H diff --git a/src/pstream.h b/src/pstream.h new file mode 100644 index 00000000..00caf4e0 --- /dev/null +++ b/src/pstream.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file pstream.h + * @author John Wiegley + * + * @ingroup util + */ +#ifndef _PSTREAM_H +#define _PSTREAM_H + +//#include <istream> +//#include <streambuf> + +class ptristream : public std::istream +{ + class ptrinbuf : public std::streambuf + { + ptrinbuf(const ptrinbuf&); + ptrinbuf& operator=(const ptrinbuf&); + + protected: + char * ptr; + std::size_t len; + + public: + ptrinbuf(char * _ptr, std::size_t _len) : ptr(_ptr), len(_len) { + if (*ptr && len == 0) + len = std::strlen(ptr); + + setg(ptr, // beginning of putback area + ptr, // read position + ptr+len); // end position + } + + protected: + virtual int_type underflow() { + // is read position before end of buffer? + if (gptr() < egptr()) + return traits_type::to_int_type(*gptr()); + else + return EOF; + } + + virtual pos_type seekoff(off_type off, ios_base::seekdir way, + ios_base::openmode) + { + switch (way) { + case std::ios::cur: + setg(ptr, gptr()+off, ptr+len); + break; + case std::ios::beg: + setg(ptr, ptr+off, ptr+len); + break; + case std::ios::end: + setg(ptr, egptr()+off, ptr+len); + break; + + default: + return pos_type(off_type(-1)); + } + return pos_type(gptr() - ptr); + } + }; + +protected: + ptrinbuf buf; + +public: + ptristream(char * ptr, std::size_t len = 0) + : std::istream(0), buf(ptr, len) { + rdbuf(&buf); + } +}; + +#endif // _PSTREAM_H diff --git a/src/py_account.cc b/src/py_account.cc new file mode 100644 index 00000000..056cd722 --- /dev/null +++ b/src/py_account.cc @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2003-2009, 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 "account.h" +#include "post.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + long accounts_len(account_t& account) + { + return account.accounts.size(); + } + + account_t& accounts_getitem(account_t& account, long i) + { + static long last_index = 0; + static account_t * last_account = NULL; + static accounts_map::iterator elem; + + long len = account.accounts.size(); + + if (labs(i) >= len) { + PyErr_SetString(PyExc_IndexError, _("Index out of range")); + throw_error_already_set(); + } + + if (&account == last_account && i == last_index + 1) { + last_index = i; + return *(*++elem).second; + } + + long x = i < 0 ? len + i : i; + elem = account.accounts.begin(); + while (--x >= 0) + elem++; + + last_account = &account; + last_index = i; + + return *(*elem).second; + } + + account_t * py_find_account_1(journal_t& journal, const string& name) + { + return journal.find_account(name); + } + + account_t * py_find_account_2(journal_t& journal, const string& name, + const bool auto_create) + { + return journal.find_account(name, auto_create); + } + + account_t::xdata_t& py_xdata(account_t& account) { + return account.xdata(); + } + + PyObject * py_account_unicode(account_t& account) { + return str_to_py_unicode(account.fullname()); + } + +} // unnamed namespace + +void export_account() +{ + scope().attr("ACCOUNT_EXT_SORT_CALC") = ACCOUNT_EXT_SORT_CALC; + scope().attr("ACCOUNT_EXT_HAS_NON_VIRTUALS") = ACCOUNT_EXT_HAS_NON_VIRTUALS; + scope().attr("ACCOUNT_EXT_HAS_UNB_VIRTUALS") = ACCOUNT_EXT_HAS_UNB_VIRTUALS; + scope().attr("ACCOUNT_EXT_AUTO_VIRTUALIZE") = ACCOUNT_EXT_AUTO_VIRTUALIZE; + scope().attr("ACCOUNT_EXT_VISITED") = ACCOUNT_EXT_VISITED; + scope().attr("ACCOUNT_EXT_MATCHING") = ACCOUNT_EXT_MATCHING; + scope().attr("ACCOUNT_EXT_TO_DISPLAY") = ACCOUNT_EXT_TO_DISPLAY; + scope().attr("ACCOUNT_EXT_DISPLAYED") = ACCOUNT_EXT_DISPLAYED; + + class_< account_t::xdata_t::details_t > ("AccountXDataDetails") + .def_readonly("total", &account_t::xdata_t::details_t::total) + .def_readonly("calculated", &account_t::xdata_t::details_t::calculated) + .def_readonly("gathered", &account_t::xdata_t::details_t::gathered) + + .def_readonly("posts_count", &account_t::xdata_t::details_t::posts_count) + .def_readonly("posts_virtuals_count", + &account_t::xdata_t::details_t::posts_virtuals_count) + .def_readonly("posts_cleared_count", + &account_t::xdata_t::details_t::posts_cleared_count) + .def_readonly("posts_last_7_count", + &account_t::xdata_t::details_t::posts_last_7_count) + .def_readonly("posts_last_30_count", + &account_t::xdata_t::details_t::posts_last_30_count) + .def_readonly("posts_this_month_count", + &account_t::xdata_t::details_t::posts_this_month_count) + + .def_readonly("earliest_post", + &account_t::xdata_t::details_t::earliest_post) + .def_readonly("earliest_cleared_post", + &account_t::xdata_t::details_t::earliest_cleared_post) + .def_readonly("latest_post", + &account_t::xdata_t::details_t::latest_post) + .def_readonly("latest_cleared_post", + &account_t::xdata_t::details_t::latest_cleared_post) + + .def_readonly("filenames", &account_t::xdata_t::details_t::filenames) + .def_readonly("accounts_referenced", + &account_t::xdata_t::details_t::accounts_referenced) + .def_readonly("payees_referenced", + &account_t::xdata_t::details_t::payees_referenced) + + .def(self += self) + + .def("update", &account_t::xdata_t::details_t::update) + ; + + class_< account_t::xdata_t > ("AccountXData") +#if 1 + .add_property("flags", + &supports_flags<uint_least16_t>::flags, + &supports_flags<uint_least16_t>::set_flags) + .def("has_flags", &supports_flags<uint_least16_t>::has_flags) + .def("clear_flags", &supports_flags<uint_least16_t>::clear_flags) + .def("add_flags", &supports_flags<uint_least16_t>::add_flags) + .def("drop_flags", &supports_flags<uint_least16_t>::drop_flags) +#endif + + .def_readonly("self_details", &account_t::xdata_t::self_details) + .def_readonly("family_details", &account_t::xdata_t::family_details) + .def_readonly("reported_posts", &account_t::xdata_t::reported_posts) + .def_readonly("sort_values", &account_t::xdata_t::sort_values) + ; + + scope().attr("ACCOUNT_NORMAL") = ACCOUNT_NORMAL; + scope().attr("ACCOUNT_KNOWN") = ACCOUNT_KNOWN; + scope().attr("ACCOUNT_TEMP") = ACCOUNT_TEMP; + + class_< account_t > ("Account") +#if 1 + .add_property("flags", + &supports_flags<>::flags, + &supports_flags<>::set_flags) + .def("has_flags", &supports_flags<>::has_flags) + .def("clear_flags", &supports_flags<>::clear_flags) + .def("add_flags", &supports_flags<>::add_flags) + .def("drop_flags", &supports_flags<>::drop_flags) +#endif + + .add_property("parent", + make_getter(&account_t::parent, + return_internal_reference<>())) + + .def_readwrite("name", &account_t::name) + .def_readwrite("note", &account_t::note) + .def_readonly("depth", &account_t::depth) + + .def("__str__", &account_t::fullname) + .def("__unicode__", py_account_unicode) + + .def("fullname", &account_t::fullname) + .def("partial_name", &account_t::partial_name) + + .def("add_account", &account_t::add_account) + .def("remove_account", &account_t::remove_account) + + .def("find_account", &account_t::find_account, + return_internal_reference<>()) + .def("find_account_re", &account_t::find_account, + return_internal_reference<>()) + + .def("add_post", &account_t::add_post) + .def("remove_post", &account_t::remove_post) + + .def("valid", &account_t::valid) + + .def("__len__", accounts_len) + .def("__getitem__", accounts_getitem, return_internal_reference<>()) + + .def("__iter__", range<return_internal_reference<> > + (&account_t::accounts_begin, &account_t::accounts_end)) + .def("accounts", range<return_internal_reference<> > + (&account_t::accounts_begin, &account_t::accounts_end)) + .def("posts", range<return_internal_reference<> > + (&account_t::posts_begin, &account_t::posts_end)) + + .def("has_xdata", &account_t::has_xdata) + .def("clear_xdata", &account_t::clear_xdata) + .def("xdata", py_xdata, + return_internal_reference<>()) + + .def("amount", &account_t::amount) + .def("total", &account_t::total) + + .def("self_details", &account_t::self_details, + return_internal_reference<>()) + .def("family_details", &account_t::family_details, + return_internal_reference<>()) + + .def("has_xflags", &account_t::has_xflags) + .def("children_with_flags", &account_t::children_with_flags) + ; +} + +} // namespace ledger diff --git a/src/py_amount.cc b/src/py_amount.cc new file mode 100644 index 00000000..8fb507a3 --- /dev/null +++ b/src/py_amount.cc @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2003-2009, 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 "pyfstream.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + boost::optional<amount_t> py_value_0(const amount_t& amount) { + return amount.value(false, CURRENT_TIME()); + } + boost::optional<amount_t> py_value_1(const amount_t& amount, + commodity_t& in_terms_of) { + return amount.value(false, CURRENT_TIME(), in_terms_of); + } + boost::optional<amount_t> py_value_2(const amount_t& amount, + commodity_t& in_terms_of, + datetime_t& moment) { + return amount.value(false, moment, in_terms_of); + } + + void py_parse_2(amount_t& amount, object in, unsigned char flags) { + if (PyFile_Check(in.ptr())) { + pyifstream instr(reinterpret_cast<PyFileObject *>(in.ptr())); + amount.parse(instr, flags); + } else { + PyErr_SetString(PyExc_IOError, + _("Argument to amount.parse(file) is not a file object")); + } + } + void py_parse_1(amount_t& amount, object in) { + py_parse_2(amount, in, 0); + } + + void py_parse_str_1(amount_t& amount, const string& str) { + amount.parse(str); + } + void py_parse_str_2(amount_t& amount, const string& str, unsigned char flags) { + amount.parse(str, flags); + } + + void py_print(amount_t& amount, object out) { + if (PyFile_Check(out.ptr())) { + pyofstream outstr(reinterpret_cast<PyFileObject *>(out.ptr())); + amount.print(outstr); + } else { + PyErr_SetString(PyExc_IOError, + _("Argument to amount.print_(file) is not a file object")); + } + } + + annotation_t& py_amount_annotation(amount_t& amount) { + return amount.annotation(); + } + + amount_t py_strip_annotations_0(amount_t& amount) { + return amount.strip_annotations(keep_details_t()); + } + amount_t py_strip_annotations_1(amount_t& amount, const keep_details_t& keep) { + return amount.strip_annotations(keep); + } + + PyObject * py_amount_unicode(amount_t& amount) { + return str_to_py_unicode(amount.to_string()); + } + +} // unnamed namespace + +#define EXC_TRANSLATOR(type) \ + void exc_translate_ ## type(const type& err) { \ + PyErr_SetString(PyExc_ArithmeticError, err.what()); \ + } + +EXC_TRANSLATOR(amount_error) + +void export_amount() +{ + class_< amount_t > ("Amount") + .def("initialize", &amount_t::initialize) // only for the PyUnitTests + .staticmethod("initialize") + .def("shutdown", &amount_t::shutdown) + .staticmethod("shutdown") + + .add_static_property("is_initialized", + make_getter(&amount_t::is_initialized), + make_setter(&amount_t::is_initialized)) + .add_static_property("stream_fullstrings", + make_getter(&amount_t::stream_fullstrings), + make_setter(&amount_t::stream_fullstrings)) + + .def(init<long>()) + .def(init<std::string>()) + + .def("exact", &amount_t::exact, args("value"), + _("Construct an amount object whose display precision is always equal to its\n\ +internal precision.")) + .staticmethod("exact") + + .def(init<amount_t>()) + + .def("compare", &amount_t::compare, args("amount"), + _("Compare two amounts for equality, returning <0, 0 or >0.")) + + .def(self == self) + .def(self == long()) + .def(long() == self) + + .def(self != self) + .def(self != long()) + .def(long() != self) + + .def(! self) + + .def(self < self) + .def(self < long()) + .def(long() < self) + + .def(self <= self) + .def(self <= long()) + .def(long() <= self) + + .def(self > self) + .def(self > long()) + .def(long() > self) + + .def(self >= self) + .def(self >= long()) + .def(long() >= self) + + .def(self += self) + .def(self += long()) + + .def(self + self) + .def(self + long()) + .def(long() + self) + + .def(self -= self) + .def(self -= long()) + + .def(self - self) + .def(self - long()) + .def(long() - self) + + .def(self *= self) + .def(self *= long()) + + .def(self * self) + .def(self * long()) + .def(long() * self) + + .def(self /= self) + .def(self /= long()) + + .def(self / self) + .def(self / long()) + .def(long() / self) + + .add_property("precision", &amount_t::precision) + .add_property("display_precision", &amount_t::display_precision) + .add_property("keep_precision", + &amount_t::keep_precision, + &amount_t::set_keep_precision) + + .def("negated", &amount_t::negated) + .def("in_place_negate", &amount_t::in_place_negate, + return_internal_reference<>()) + .def(- self) + + .def("abs", &amount_t::abs) + .def("__abs__", &amount_t::abs) + + .def("inverted", &amount_t::inverted) + + .def("rounded", &amount_t::rounded) + .def("in_place_round", &amount_t::in_place_round, + return_internal_reference<>()) + + .def("truncated", &amount_t::truncated) + .def("in_place_truncate", &amount_t::in_place_truncate, + return_internal_reference<>()) + + .def("floored", &amount_t::floored) + .def("in_place_floor", &amount_t::in_place_floor, + return_internal_reference<>()) + + .def("unrounded", &amount_t::unrounded) + .def("in_place_unround", &amount_t::in_place_unround, + return_internal_reference<>()) + + .def("reduced", &amount_t::reduced) + .def("in_place_reduce", &amount_t::in_place_reduce, + return_internal_reference<>()) + + .def("unreduced", &amount_t::unreduced) + .def("in_place_unreduce", &amount_t::in_place_unreduce, + return_internal_reference<>()) + + .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("price", &amount_t::price) + + .def("sign", &amount_t::sign) + .def("__nonzero__", &amount_t::is_nonzero) + .def("is_nonzero", &amount_t::is_nonzero) + .def("is_zero", &amount_t::is_zero) + .def("is_realzero", &amount_t::is_realzero) + .def("is_null", &amount_t::is_null) + + .def("to_double", &amount_t::to_double) + .def("__float__", &amount_t::to_double) + .def("to_long", &amount_t::to_long) + .def("__int__", &amount_t::to_long) + .def("fits_in_long", &amount_t::fits_in_long) + + .def("__str__", &amount_t::to_string) + .def("to_string", &amount_t::to_string) + .def("__unicode__", py_amount_unicode) + .def("to_fullstring", &amount_t::to_fullstring) + .def("__repr__", &amount_t::to_fullstring) + .def("quantity_string", &amount_t::quantity_string) + + .add_property("commodity", + make_function(&amount_t::commodity, + return_value_policy<reference_existing_object>()), + make_function(&amount_t::set_commodity, + with_custodian_and_ward<1, 2>())) + .def("has_commodity", &amount_t::has_commodity) + .def("clear_commodity", &amount_t::clear_commodity) + + .def("number", &amount_t::number) + + .def("annotate", &amount_t::annotate) + .def("has_annotation", &amount_t::has_annotation) + .add_property("annotation", + make_function(py_amount_annotation, + return_internal_reference<>())) + .def("strip_annotations", py_strip_annotations_0) + .def("strip_annotations", py_strip_annotations_1) + + .def("parse", py_parse_1) + .def("parse", py_parse_2) + .def("parse", py_parse_str_1) + .def("parse", py_parse_str_2) + + .def("parse_conversion", &amount_t::parse_conversion) + .staticmethod("parse_conversion") + + .def("valid", &amount_t::valid) + ; + + enum_< parse_flags_enum_t >("ParseFlags") + .value("Default", PARSE_DEFAULT) + .value("Partial", PARSE_PARTIAL) + .value("Single", PARSE_SINGLE) + .value("NoMigrate", PARSE_NO_MIGRATE) + .value("NoReduce", PARSE_NO_REDUCE) + .value("NoAssign", PARSE_NO_ASSIGN) + .value("NoDates", PARSE_NO_DATES) + .value("OpContext", PARSE_OP_CONTEXT) + .value("SoftFail", PARSE_SOFT_FAIL) + ; + + register_optional_to_python<amount_t>(); + + implicitly_convertible<long, amount_t>(); + implicitly_convertible<string, amount_t>(); + +#define EXC_TRANSLATE(type) \ + register_exception_translator<type>(&exc_translate_ ## type); + + EXC_TRANSLATE(amount_error); +} + +} // namespace ledger diff --git a/src/py_balance.cc b/src/py_balance.cc new file mode 100644 index 00000000..8c0c4c58 --- /dev/null +++ b/src/py_balance.cc @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2003-2009, 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 "pyfstream.h" +#include "commodity.h" +#include "annotate.h" +#include "balance.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + boost::optional<balance_t> py_value_0(const balance_t& balance) { + return balance.value(false, CURRENT_TIME()); + } + boost::optional<balance_t> py_value_1(const balance_t& balance, + commodity_t& in_terms_of) { + return balance.value(false, CURRENT_TIME(), in_terms_of); + } + boost::optional<balance_t> py_value_2(const balance_t& balance, + commodity_t& in_terms_of, + datetime_t& moment) { + return balance.value(false, moment, in_terms_of); + } + + boost::optional<amount_t> + py_commodity_amount_0(const balance_t& balance) { + return balance.commodity_amount(); + } + + boost::optional<amount_t> + py_commodity_amount_1(const balance_t& balance, + const boost::optional<const commodity_t&>& commodity) { + return balance.commodity_amount(commodity); + } + + void py_print(balance_t& balance, object out) { + if (PyFile_Check(out.ptr())) { + pyofstream outstr(reinterpret_cast<PyFileObject *>(out.ptr())); + balance.print(outstr); + } else { + PyErr_SetString(PyExc_IOError, + _("Argument to balance.print_(file) is not a file object")); + } + } + + long balance_len(balance_t& bal) { + return bal.amounts.size(); + } + + amount_t balance_getitem(balance_t& bal, long i) { + long len = bal.amounts.size(); + + if (labs(i) >= len) { + PyErr_SetString(PyExc_IndexError, _("Index out of range")); + throw_error_already_set(); + } + + long x = i < 0 ? len + i : i; + balance_t::amounts_map::iterator elem = bal.amounts.begin(); + while (--x >= 0) + elem++; + + return (*elem).second; + } + + balance_t py_strip_annotations_0(balance_t& balance) { + return balance.strip_annotations(keep_details_t()); + } + balance_t py_strip_annotations_1(balance_t& balance, const keep_details_t& keep) { + return balance.strip_annotations(keep); + } + + PyObject * py_balance_unicode(balance_t& balance) { + return str_to_py_unicode(balance.to_string()); + } + +} // unnamed namespace + +#define EXC_TRANSLATOR(type) \ + void exc_translate_ ## type(const type& err) { \ + PyErr_SetString(PyExc_ArithmeticError, err.what()); \ + } + +EXC_TRANSLATOR(balance_error) + +void export_balance() +{ + class_< balance_t > ("Balance") + .def(init<balance_t>()) + .def(init<amount_t>()) + .def(init<long>()) + .def(init<string>()) + + .def(self += self) + .def(self += other<amount_t>()) + .def(self += long()) + .def(self + self) + .def(self + other<amount_t>()) + .def(self + long()) + .def(self -= self) + .def(self -= other<amount_t>()) + .def(self -= long()) + .def(self - self) + .def(self - other<amount_t>()) + .def(self - long()) + .def(self *= other<amount_t>()) + .def(self *= long()) + .def(self * other<amount_t>()) + .def(self * long()) + .def(self /= other<amount_t>()) + .def(self /= long()) + .def(self / other<amount_t>()) + .def(self / long()) + .def(- self) + + .def(self == self) + .def(self == other<amount_t>()) + .def(self == long()) + .def(self != self) + .def(self != other<amount_t>()) + .def(self != long()) + .def(! self) + + .def("__str__", &balance_t::to_string) + .def("to_string", &balance_t::to_string) + .def("__unicode__", py_balance_unicode) + + .def("negated", &balance_t::negated) + .def("in_place_negate", &balance_t::in_place_negate, + return_internal_reference<>()) + .def(- self) + + .def("abs", &balance_t::abs) + .def("__abs__", &balance_t::abs) + + .def("__len__", balance_len) + .def("__getitem__", balance_getitem) + + .def("rounded", &balance_t::rounded) + .def("in_place_round", &balance_t::in_place_round, + return_internal_reference<>()) + + .def("truncated", &balance_t::truncated) + .def("in_place_truncate", &balance_t::in_place_truncate, + return_internal_reference<>()) + + .def("floored", &balance_t::floored) + .def("in_place_floor", &balance_t::in_place_floor, + return_internal_reference<>()) + + .def("unrounded", &balance_t::unrounded) + .def("in_place_unround", &balance_t::in_place_unround, + return_internal_reference<>()) + + .def("reduced", &balance_t::reduced) + .def("in_place_reduce", &balance_t::in_place_reduce, + return_internal_reference<>()) + + .def("unreduced", &balance_t::unreduced) + .def("in_place_unreduce", &balance_t::in_place_unreduce, + return_internal_reference<>()) + + .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("price", &balance_t::price) + + .def("__nonzero__", &balance_t::is_nonzero) + .def("is_nonzero", &balance_t::is_nonzero) + .def("is_zero", &balance_t::is_zero) + .def("is_realzero", &balance_t::is_realzero) + + .def("is_empty", &balance_t::is_empty) + .def("single_amount", &balance_t::single_amount) + + .def("to_amount", &balance_t::to_amount) + + .def("commodity_count", &balance_t::commodity_count) + .def("commodity_amount", py_commodity_amount_0) + .def("commodity_amount", py_commodity_amount_1) + + .def("number", &balance_t::number) + + .def("strip_annotations", py_strip_annotations_0) + .def("strip_annotations", py_strip_annotations_1) + + .def("valid", &balance_t::valid) + ; + + register_optional_to_python<balance_t>(); + + implicitly_convertible<long, balance_t>(); + implicitly_convertible<string, balance_t>(); + implicitly_convertible<amount_t, balance_t>(); + +#define EXC_TRANSLATE(type) \ + register_exception_translator<type>(&exc_translate_ ## type); + + EXC_TRANSLATE(balance_error); +} + +} // namespace ledger diff --git a/src/py_commodity.cc b/src/py_commodity.cc new file mode 100644 index 00000000..984be5f0 --- /dev/null +++ b/src/py_commodity.cc @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2003-2009, 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 "commodity.h" +#include "annotate.h" +#include "pool.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + commodity_t * py_create_1(commodity_pool_t& pool, + const string& symbol) + { + return pool.create(symbol); + } + commodity_t * py_create_2(commodity_pool_t& pool, + const string& symbol, + const annotation_t& details) + { + return pool.create(symbol, details); + } + + commodity_t * py_find_or_create_1(commodity_pool_t& pool, + const string& symbol) + { + return pool.find_or_create(symbol); + } + commodity_t * py_find_or_create_2(commodity_pool_t& pool, + const string& symbol, + const annotation_t& details) + { + return pool.find_or_create(symbol, details); + } + + commodity_t * py_find_1(commodity_pool_t& pool, + const string& name) + { + return pool.find(name); + } + + commodity_t * py_find_2(commodity_pool_t& pool, + const string& symbol, + const annotation_t& details) + { + return pool.find(symbol, details); + } + + // Exchange one commodity for another, while recording the factored price. + + void py_exchange_2(commodity_pool_t& pool, + commodity_t& commodity, + const amount_t& per_unit_cost) + { + pool.exchange(commodity, per_unit_cost, CURRENT_TIME()); + } + void py_exchange_3(commodity_pool_t& pool, + commodity_t& commodity, + const amount_t& per_unit_cost, + const datetime_t& moment) + { + pool.exchange(commodity, per_unit_cost, moment); + } + + cost_breakdown_t py_exchange_5(commodity_pool_t& pool, + const amount_t& amount, + const amount_t& cost, + const bool is_per_unit, + const boost::optional<datetime_t>& moment, + const boost::optional<string>& tag) + { + return pool.exchange(amount, cost, is_per_unit, moment, tag); + } + + commodity_t * py_pool_getitem(commodity_pool_t& pool, const string& symbol) + { + commodity_pool_t::commodities_map::iterator i = + pool.commodities.find(symbol); + if (i == pool.commodities.end()) { + PyErr_SetString(PyExc_ValueError, + (string("Could not find commodity ") + symbol).c_str()); + throw boost::python::error_already_set(); + } + return (*i).second; + } + + python::list py_pool_keys(commodity_pool_t& pool) { + python::list keys; + BOOST_REVERSE_FOREACH + (const commodity_pool_t::commodities_map::value_type& pair, + pool.commodities) { + keys.insert(0, pair.first); + } + return keys; + } + + bool py_pool_contains(commodity_pool_t& pool, const string& symbol) { + return pool.commodities.find(symbol) != pool.commodities.end(); + } + + commodity_pool_t::commodities_map::iterator + py_pool_commodities_begin(commodity_pool_t& pool) { + return pool.commodities.begin(); + } + commodity_pool_t::commodities_map::iterator + py_pool_commodities_end(commodity_pool_t& pool) { + return pool.commodities.end(); + } + + typedef transform_iterator + <function<string(commodity_pool_t::commodities_map::value_type&)>, + commodity_pool_t::commodities_map::iterator> + commodities_map_firsts_iterator; + commodities_map_firsts_iterator + + py_pool_commodities_keys_begin(commodity_pool_t& pool) { + return make_transform_iterator + (pool.commodities.begin(), + bind(&commodity_pool_t::commodities_map::value_type::first, _1)); + } + commodities_map_firsts_iterator + py_pool_commodities_keys_end(commodity_pool_t& pool) { + return make_transform_iterator + (pool.commodities.end(), + bind(&commodity_pool_t::commodities_map::value_type::first, _1)); + } + + typedef transform_iterator + <function<commodity_t *(commodity_pool_t::commodities_map::value_type&)>, + commodity_pool_t::commodities_map::iterator> + commodities_map_seconds_iterator; + + commodities_map_seconds_iterator + 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)); + } + 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)); + } + + void py_add_price_2(commodity_t& commodity, + const datetime_t& date, const amount_t& price) { + commodity.add_price(date, price); + } + + void py_add_price_3(commodity_t& commodity, const datetime_t& date, + const amount_t& price, const bool reflexive) { + commodity.add_price(date, price, reflexive); + } + + bool py_keep_all_0(keep_details_t& details) { + return details.keep_all(); + } + bool py_keep_all_1(keep_details_t& details, const commodity_t& comm) { + return details.keep_all(comm); + } + + bool py_keep_any_0(keep_details_t& details) { + return details.keep_any(); + } + bool py_keep_any_1(keep_details_t& details, const commodity_t& comm) { + return details.keep_any(comm); + } + + commodity_t& py_commodity_referent(commodity_t& comm) { + return comm.referent(); + } + commodity_t& py_annotated_commodity_referent(annotated_commodity_t& comm) { + return comm.referent(); + } + + commodity_t& py_strip_annotations_0(commodity_t& comm) { + return comm.strip_annotations(keep_details_t()); + } + commodity_t& py_strip_annotations_1(commodity_t& comm, + const keep_details_t& keep) { + return comm.strip_annotations(keep); + } + + commodity_t& py_strip_ann_annotations_0(annotated_commodity_t& comm) { + return comm.strip_annotations(keep_details_t()); + } + commodity_t& py_strip_ann_annotations_1(annotated_commodity_t& comm, + const keep_details_t& keep) { + return comm.strip_annotations(keep); + } + + boost::optional<amount_t> py_price(annotation_t& ann) { + return ann.price; + } + boost::optional<amount_t> py_set_price(annotation_t& ann, + const boost::optional<amount_t>& price) { + return ann.price = price; + } + + PyObject * py_commodity_unicode(commodity_t& commodity) { + return str_to_py_unicode(commodity.symbol()); + } + +} // unnamed namespace + +void export_commodity() +{ + class_< commodity_pool_t, shared_ptr<commodity_pool_t>, + boost::noncopyable > ("CommodityPool", no_init) + .add_property("null_commodity", + make_getter(&commodity_pool_t::null_commodity, + return_internal_reference<>())) + .add_property("default_commodity", + make_getter(&commodity_pool_t::default_commodity, + return_internal_reference<>()), + make_setter(&commodity_pool_t::default_commodity, + with_custodian_and_ward<1, 2>())) + + .add_property("keep_base", + 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)) + .add_property("quote_leeway", + make_getter(&commodity_pool_t::quote_leeway), + make_setter(&commodity_pool_t::quote_leeway)) + .add_property("get_quotes", + make_getter(&commodity_pool_t::get_quotes), + make_setter(&commodity_pool_t::get_quotes)) + .add_property("get_commodity_quote", + 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_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_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("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("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>()) + + .def("__getitem__", py_pool_getitem, + return_value_policy<reference_existing_object>()) + .def("keys", py_pool_keys) + .def("has_key", py_pool_contains) + .def("__contains__", py_pool_contains) + .def("__iter__", range<return_value_policy<reference_existing_object> > + (py_pool_commodities_begin, py_pool_commodities_end)) + .def("iteritems", range<return_value_policy<reference_existing_object> > + (py_pool_commodities_begin, py_pool_commodities_end)) + .def("iterkeys", range<>(py_pool_commodities_keys_begin, + py_pool_commodities_keys_end)) + .def("itervalues", range<return_value_policy<reference_existing_object> > + (py_pool_commodities_values_begin, py_pool_commodities_values_end)) + ; + + map_value_type_converter<commodity_pool_t::commodities_map>(); + + scope().attr("commodities") = commodity_pool_t::current_pool; + + scope().attr("COMMODITY_STYLE_DEFAULTS") = COMMODITY_STYLE_DEFAULTS; + scope().attr("COMMODITY_STYLE_SUFFIXED") = COMMODITY_STYLE_SUFFIXED; + scope().attr("COMMODITY_STYLE_SEPARATED") = COMMODITY_STYLE_SEPARATED; + scope().attr("COMMODITY_STYLE_EUROPEAN") = COMMODITY_STYLE_EUROPEAN; + scope().attr("COMMODITY_STYLE_THOUSANDS") = COMMODITY_STYLE_THOUSANDS; + scope().attr("COMMODITY_NOMARKET") = COMMODITY_NOMARKET; + scope().attr("COMMODITY_BUILTIN") = COMMODITY_BUILTIN; + scope().attr("COMMODITY_WALKED") = COMMODITY_WALKED; + scope().attr("COMMODITY_KNOWN") = COMMODITY_KNOWN; + scope().attr("COMMODITY_PRIMARY") = COMMODITY_PRIMARY; + + class_< commodity_t, boost::noncopyable > ("Commodity", no_init) +#if 1 + .add_property("flags", + &supports_flags<uint_least16_t>::flags, + &supports_flags<uint_least16_t>::set_flags) + .def("has_flags", &delegates_flags<uint_least16_t>::has_flags) + .def("clear_flags", &delegates_flags<uint_least16_t>::clear_flags) + .def("add_flags", &delegates_flags<uint_least16_t>::add_flags) + .def("drop_flags", &delegates_flags<uint_least16_t>::drop_flags) +#endif + + .add_static_property("european_by_default", + make_getter(&commodity_t::european_by_default), + make_setter(&commodity_t::european_by_default)) + + .def("__str__", &commodity_t::symbol) + .def("__unicode__", py_commodity_unicode) + .def("__nonzero__", &commodity_t::operator bool) + + .def(self == self) + + .def("symbol_needs_quotes", &commodity_t::symbol_needs_quotes) + .staticmethod("symbol_needs_quotes") + + .add_property("referent", + make_function(py_commodity_referent, + return_value_policy<reference_existing_object>())) + + .def("has_annotation", &commodity_t::has_annotation) + .def("strip_annotations", py_strip_annotations_0, + return_value_policy<reference_existing_object>()) + .def("strip_annotations", py_strip_annotations_1, + return_value_policy<reference_existing_object>()) + .def("write_annotations", &commodity_t::write_annotations) + + .def("pool", &commodity_t::pool, + return_value_policy<reference_existing_object>()) + + .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) + .add_property("precision", &commodity_t::precision, + &commodity_t::set_precision) + .add_property("smaller", &commodity_t::smaller, &commodity_t::set_smaller) + .add_property("larger", &commodity_t::larger, &commodity_t::set_larger) + + .def("add_price", py_add_price_2) + .def("add_price", py_add_price_3) + .def("remove_price", &commodity_t::remove_price, + with_custodian_and_ward<1, 3>()) + .def("find_price", &commodity_t::find_price) + .def("check_for_updated_price", &commodity_t::check_for_updated_price) + + .def("valid", &commodity_t::valid) + ; + + class_< annotation_t > ("Annotation", no_init) +#if 1 + .add_property("flags", &supports_flags<>::flags, + &supports_flags<>::set_flags) + .def("has_flags", &supports_flags<>::has_flags) + .def("clear_flags", &supports_flags<>::clear_flags) + .def("add_flags", &supports_flags<>::add_flags) + .def("drop_flags", &supports_flags<>::drop_flags) +#endif + + .add_property("price", py_price, py_set_price) + .add_property("date", + make_getter(&annotation_t::date), + make_setter(&annotation_t::date)) + .add_property("tag", + make_getter(&annotation_t::tag), + make_setter(&annotation_t::tag)) + + .def("__nonzero__", &annotation_t::operator bool) + + .def(self == self) + + .def("valid", &annotation_t::valid) + ; + + class_< keep_details_t > ("KeepDetails") + .def(init<bool, bool, bool, bool>()) + + .add_property("keep_price", + make_getter(&keep_details_t::keep_price), + make_setter(&keep_details_t::keep_price)) + .add_property("keep_date", + make_getter(&keep_details_t::keep_date), + make_setter(&keep_details_t::keep_date)) + .add_property("keep_tag", + make_getter(&keep_details_t::keep_tag), + make_setter(&keep_details_t::keep_tag)) + .add_property("only_actuals", + make_getter(&keep_details_t::only_actuals), + make_setter(&keep_details_t::only_actuals)) + + .def("keep_all", py_keep_all_0) + .def("keep_all", py_keep_all_1) + .def("keep_any", py_keep_any_0) + .def("keep_any", py_keep_any_1) + ; + + class_< annotated_commodity_t, bases<commodity_t>, + annotated_commodity_t, boost::noncopyable > + ("AnnotatedCommodity", no_init) + .add_property("details", + make_getter(&annotated_commodity_t::details), + make_setter(&annotated_commodity_t::details)) + + .def(self == self) + .def(self == other<commodity_t>()) + + .add_property("referent", + make_function(py_annotated_commodity_referent, + return_value_policy<reference_existing_object>())) + + .def("strip_annotations", py_strip_ann_annotations_0, + return_value_policy<reference_existing_object>()) + .def("strip_annotations", py_strip_ann_annotations_1, + return_value_policy<reference_existing_object>()) + .def("write_annotations", &annotated_commodity_t::write_annotations) + ; +} + +} // namespace ledger diff --git a/src/py_expr.cc b/src/py_expr.cc new file mode 100644 index 00000000..cad2c0fa --- /dev/null +++ b/src/py_expr.cc @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2003-2009, 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" + +namespace ledger { + +using namespace boost::python; + +namespace { + value_t py_expr_call(expr_t& expr) + { + return expr.calc(); + } +} + +void export_expr() +{ + class_< expr_t > ("Expr") + .def(init<string>()) + + .def("__nonzero__", &expr_t::operator bool) + .def("text", &expr_t::text) + .def("set_text", &expr_t::set_text) + + //.def("parse", &expr_t::parse) + + .def("__call__", py_expr_call) + .def("compile", &expr_t::compile) + //.def("calc", &expr_t::calc) + + .def("is_constant", &expr_t::is_constant) + + //.def("constant_value", &expr_t::constant_value) + ; +} + +} // namespace ledger diff --git a/src/py_format.cc b/src/py_format.cc new file mode 100644 index 00000000..d4825b42 --- /dev/null +++ b/src/py_format.cc @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2003-2009, 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" + +namespace ledger { + +using namespace boost::python; + +#define EXC_TRANSLATOR(type) \ + void exc_translate_ ## type(const type& err) { \ + PyErr_SetString(PyExc_ArithmeticError, err.what()); \ + } + +//EXC_TRANSLATOR(format_error) + +void export_format() +{ +#if 0 + class_< format_t > ("Format") + ; +#endif + + //register_optional_to_python<amount_t>(); + + //implicitly_convertible<string, amount_t>(); + +#define EXC_TRANSLATE(type) \ + register_exception_translator<type>(&exc_translate_ ## type); + + //EXC_TRANSLATE(format_error); +} + +} // namespace ledger diff --git a/src/py_item.cc b/src/py_item.cc new file mode 100644 index 00000000..8a6e495a --- /dev/null +++ b/src/py_item.cc @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2003-2009, 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 "scope.h" +#include "mask.h" +#include "item.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + bool py_has_tag_1s(item_t& item, const string& tag) { + return item.has_tag(tag); + } + bool py_has_tag_1m(item_t& item, const mask_t& tag_mask) { + return item.has_tag(tag_mask); + } + bool py_has_tag_2m(item_t& item, const mask_t& tag_mask, + const boost::optional<mask_t>& value_mask) { + return item.has_tag(tag_mask, value_mask); + } + + boost::optional<string> py_get_tag_1s(item_t& item, const string& tag) { + return item.get_tag(tag); + } + boost::optional<string> py_get_tag_1m(item_t& item, const mask_t& tag_mask) { + return item.get_tag(tag_mask); + } + boost::optional<string> py_get_tag_2m(item_t& item, const mask_t& tag_mask, + const boost::optional<mask_t>& value_mask) { + return item.get_tag(tag_mask, value_mask); + } + +} // unnamed namespace + +#define EXC_TRANSLATOR(type) \ + void exc_translate_ ## type(const type& err) { \ + PyErr_SetString(PyExc_ArithmeticError, err.what()); \ + } + +//EXC_TRANSLATOR(item_error) + +void export_item() +{ + class_< position_t > ("Position") + .add_property("pathname", + make_getter(&position_t::pathname), + make_setter(&position_t::pathname)) + .add_property("beg_pos", + make_getter(&position_t::beg_pos), + make_setter(&position_t::beg_pos)) + .add_property("beg_line", + make_getter(&position_t::beg_line), + make_setter(&position_t::beg_line)) + .add_property("end_pos", + make_getter(&position_t::end_pos), + make_setter(&position_t::end_pos)) + .add_property("end_line", + make_getter(&position_t::end_line), + make_setter(&position_t::end_line)) + ; + + scope().attr("ITEM_NORMAL") = ITEM_NORMAL; + scope().attr("ITEM_GENERATED") = ITEM_GENERATED; + scope().attr("ITEM_TEMP") = ITEM_TEMP; + + enum_< item_t::state_t > ("State") + .value("Uncleared", item_t::UNCLEARED) + .value("Cleared", item_t::CLEARED) + .value("Pending", item_t::PENDING) + ; + +#if 0 + class_< item_t, bases<scope_t> > ("JournalItem", init<uint_least8_t>()) +#else + class_< item_t > ("JournalItem", init<uint_least8_t>()) +#endif +#if 1 + .add_property("flags", &supports_flags<>::flags, + &supports_flags<>::set_flags) + .def("has_flags", &supports_flags<>::has_flags) + .def("clear_flags", &supports_flags<>::clear_flags) + .def("add_flags", &supports_flags<>::add_flags) + .def("drop_flags", &supports_flags<>::drop_flags) +#endif + + .add_property("note", + make_getter(&item_t::note), + make_setter(&item_t::note)) + .add_property("pos", + make_getter(&item_t::pos), + make_setter(&item_t::pos)) + .add_property("metadata", + make_getter(&item_t::metadata), + make_setter(&item_t::metadata)) + + .def("copy_details", &item_t::copy_details) + + .def(self == self) + .def(self != self) + + .def("has_tag", py_has_tag_1s) + .def("has_tag", py_has_tag_1m) + .def("has_tag", py_has_tag_2m) + .def("get_tag", py_get_tag_1s) + .def("get_tag", py_get_tag_1m) + .def("get_tag", py_get_tag_2m) + .def("tag", py_get_tag_1s) + .def("tag", py_get_tag_1m) + .def("tag", py_get_tag_2m) + + .def("set_tag", &item_t::set_tag) + + .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_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("state", &item_t::state, &item_t::set_state) + + .def("lookup", &item_t::lookup) + + .def("valid", &item_t::valid) + ; +} + +} // namespace ledger diff --git a/src/py_journal.cc b/src/py_journal.cc new file mode 100644 index 00000000..5be9cbe1 --- /dev/null +++ b/src/py_journal.cc @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2003-2009, 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 "journal.h" +#include "xact.h" +#include "post.h" +#include "chain.h" +#include "filters.h" +#include "iterators.h" +#include "scope.h" +#include "report.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + account_t& py_account_master(journal_t& journal) { + return *journal.master; + } + + long xacts_len(journal_t& journal) + { + return journal.xacts.size(); + } + + xact_t& xacts_getitem(journal_t& journal, long i) + { + static long last_index = 0; + static journal_t * last_journal = NULL; + static xacts_list::iterator elem; + + long len = journal.xacts.size(); + + if (labs(i) >= len) { + PyErr_SetString(PyExc_IndexError, _("Index out of range")); + throw_error_already_set(); + } + + if (&journal == last_journal && i == last_index + 1) { + last_index = i; + return **++elem; + } + + long x = i < 0 ? len + i : i; + elem = journal.xacts.begin(); + while (--x >= 0) + elem++; + + last_journal = &journal; + last_index = i; + + return **elem; + } + + long accounts_len(account_t& account) + { + return account.accounts.size(); + } + + account_t& accounts_getitem(account_t& account, long i) + { + static long last_index = 0; + static account_t * last_account = NULL; + static accounts_map::iterator elem; + + long len = account.accounts.size(); + + if (labs(i) >= len) { + PyErr_SetString(PyExc_IndexError, _("Index out of range")); + throw_error_already_set(); + } + + if (&account == last_account && i == last_index + 1) { + last_index = i; + return *(*++elem).second; + } + + long x = i < 0 ? len + i : i; + elem = account.accounts.begin(); + while (--x >= 0) + elem++; + + last_account = &account; + last_index = i; + + return *(*elem).second; + } + + account_t * py_find_account_1(journal_t& journal, const string& name) + { + return journal.find_account(name); + } + + account_t * py_find_account_2(journal_t& journal, const string& name, + const bool auto_create) + { + return journal.find_account(name, auto_create); + } + + std::size_t py_read(journal_t& journal, const string& pathname) + { + return journal.read(pathname); + } + + struct collector_wrapper + { + journal_t& journal; + report_t report; + collect_posts * posts_collector; + post_handler_ptr chain; + + collector_wrapper(journal_t& _journal, report_t& base) + : journal(_journal), report(base), + posts_collector(new collect_posts) {} + ~collector_wrapper() { + journal.clear_xdata(); + } + + std::size_t length() const { + return posts_collector->length(); + } + + std::vector<post_t *>::iterator begin() { + return posts_collector->begin(); + } + std::vector<post_t *>::iterator end() { + return posts_collector->end(); + } + }; + + shared_ptr<collector_wrapper> + py_collect(journal_t& journal, const string& query) + { + if (journal.has_xdata()) { + PyErr_SetString(PyExc_RuntimeError, + _("Cannot have multiple journal collections open at once")); + 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)); + std::auto_ptr<journal_t> save_journal + (current_report.session.journal.release()); + current_report.session.journal.reset(&journal); + + try { + strings_list remaining = + process_arguments(split_arguments(query.c_str()), coll->report); + coll->report.normalize_options("register"); + + value_t args; + foreach (const string& arg, remaining) + args.push_back(string_value(arg)); + coll->report.parse_query_args(args, "@Journal.collect"); + + journal_posts_iterator walker(coll->journal); + coll->chain = + chain_post_handlers(coll->report, + post_handler_ptr(coll->posts_collector)); + pass_down_posts(coll->chain, walker); + } + catch (...) { + current_report.session.journal.release(); + current_report.session.journal.reset(save_journal.release()); + throw; + } + current_report.session.journal.release(); + current_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[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; + } + +} // unnamed namespace + +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__", 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__", range<return_value_policy<reference_existing_object, + with_custodian_and_ward_postcall<0, 1> > > + (&collector_wrapper::begin, &collector_wrapper::end)) + ; + + class_< journal_t::fileinfo_t > ("FileInfo") + .def(init<path>()) + + .add_property("filename", + make_getter(&journal_t::fileinfo_t::filename), + make_setter(&journal_t::fileinfo_t::filename)) + .add_property("size", + make_getter(&journal_t::fileinfo_t::size), + make_setter(&journal_t::fileinfo_t::size)) + .add_property("modtime", + make_getter(&journal_t::fileinfo_t::modtime), + make_setter(&journal_t::fileinfo_t::modtime)) + .add_property("from_stream", + make_getter(&journal_t::fileinfo_t::from_stream), + make_setter(&journal_t::fileinfo_t::from_stream)) + ; + + class_< journal_t, boost::noncopyable > ("Journal") + .def(init<path>()) + .def(init<string>()) + + .add_property("master", + make_getter(&journal_t::master, + return_internal_reference<1, + with_custodian_and_ward_postcall<1, 0> >())) + .add_property("bucket", + make_getter(&journal_t::bucket, + return_internal_reference<1, + with_custodian_and_ward_postcall<1, 0> >()), + make_setter(&journal_t::bucket)) + .add_property("was_loaded", make_getter(&journal_t::was_loaded)) + + .def("add_account", &journal_t::add_account) + .def("remove_account", &journal_t::remove_account) + + .def("find_account", py_find_account_1, + return_internal_reference<1, + with_custodian_and_ward_postcall<0, 1> >()) + .def("find_account", py_find_account_2, + return_internal_reference<1, + with_custodian_and_ward_postcall<0, 1> >()) + .def("find_account_re", &journal_t::find_account_re, + return_internal_reference<1, + with_custodian_and_ward_postcall<0, 1> >()) + + .def("add_xact", &journal_t::add_xact) + .def("remove_xact", &journal_t::remove_xact) + + .def("__len__", xacts_len) +#if 0 + .def("__getitem__", xacts_getitem, + return_internal_reference<1, + with_custodian_and_ward_postcall<0, 1> >()) +#endif + + .def("__iter__", range<return_internal_reference<> > + (&journal_t::xacts_begin, &journal_t::xacts_end)) + .def("xacts", range<return_internal_reference<> > + (&journal_t::xacts_begin, &journal_t::xacts_end)) + .def("auto_xacts", range<return_internal_reference<> > + (&journal_t::auto_xacts_begin, &journal_t::auto_xacts_end)) + .def("period_xacts", range<return_internal_reference<> > + (&journal_t::period_xacts_begin, &journal_t::period_xacts_end)) + .def("sources", range<return_internal_reference<> > + (&journal_t::sources_begin, &journal_t::sources_end)) + + .def("read", py_read) + + .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("valid", &journal_t::valid) + ; +} + +} // namespace ledger diff --git a/src/py_post.cc b/src/py_post.cc new file mode 100644 index 00000000..20cdba6b --- /dev/null +++ b/src/py_post.cc @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2003-2009, 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 "post.h" +#include "xact.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + bool py_has_tag_1s(post_t& post, const string& tag) { + return post.has_tag(tag); + } + bool py_has_tag_1m(post_t& post, const mask_t& tag_mask) { + return post.has_tag(tag_mask); + } + bool py_has_tag_2m(post_t& post, const mask_t& tag_mask, + const boost::optional<mask_t>& value_mask) { + return post.has_tag(tag_mask, value_mask); + } + + boost::optional<string> py_get_tag_1s(post_t& post, const string& tag) { + return post.get_tag(tag); + } + boost::optional<string> py_get_tag_1m(post_t& post, const mask_t& tag_mask) { + return post.get_tag(tag_mask); + } + boost::optional<string> py_get_tag_2m(post_t& post, const mask_t& tag_mask, + const boost::optional<mask_t>& value_mask) { + return post.get_tag(tag_mask, value_mask); + } + + post_t::xdata_t& py_xdata(post_t& post) { + return post.xdata(); + } + + account_t * py_reported_account(post_t& post) { + return post.reported_account(); + } + +} // unnamed namespace + +void export_post() +{ + scope().attr("POST_EXT_RECEIVED") = POST_EXT_RECEIVED; + scope().attr("POST_EXT_HANDLED") = POST_EXT_HANDLED; + scope().attr("POST_EXT_DISPLAYED") = POST_EXT_DISPLAYED; + scope().attr("POST_EXT_DIRECT_AMT") = POST_EXT_DIRECT_AMT; + scope().attr("POST_EXT_SORT_CALC") = POST_EXT_SORT_CALC; + scope().attr("POST_EXT_COMPOUND") = POST_EXT_COMPOUND; + scope().attr("POST_EXT_VISITED") = POST_EXT_VISITED; + scope().attr("POST_EXT_MATCHES") = POST_EXT_MATCHES; + scope().attr("POST_EXT_CONSIDERED") = POST_EXT_CONSIDERED; + + class_< post_t::xdata_t > ("PostingXData") +#if 1 + .add_property("flags", + &supports_flags<uint_least16_t>::flags, + &supports_flags<uint_least16_t>::set_flags) + .def("has_flags", &supports_flags<uint_least16_t>::has_flags) + .def("clear_flags", &supports_flags<uint_least16_t>::clear_flags) + .def("add_flags", &supports_flags<uint_least16_t>::add_flags) + .def("drop_flags", &supports_flags<uint_least16_t>::drop_flags) +#endif + + .add_property("visited_value", + make_getter(&post_t::xdata_t::visited_value), + make_setter(&post_t::xdata_t::visited_value)) + .add_property("compound_value", + make_getter(&post_t::xdata_t::compound_value), + make_setter(&post_t::xdata_t::compound_value)) + .add_property("total", + make_getter(&post_t::xdata_t::total), + make_setter(&post_t::xdata_t::total)) + .add_property("count", + make_getter(&post_t::xdata_t::count), + make_setter(&post_t::xdata_t::count)) + .add_property("date", + make_getter(&post_t::xdata_t::date), + make_setter(&post_t::xdata_t::date)) + .add_property("datetime", + make_getter(&post_t::xdata_t::datetime), + make_setter(&post_t::xdata_t::datetime)) + .add_property("account", + make_getter(&post_t::xdata_t::account, + return_value_policy<reference_existing_object>()), + make_setter(&post_t::xdata_t::account, + with_custodian_and_ward<1, 2>())) + .add_property("sort_values", + make_getter(&post_t::xdata_t::sort_values), + make_setter(&post_t::xdata_t::sort_values)) + ; + + scope().attr("POST_VIRTUAL") = POST_VIRTUAL; + scope().attr("POST_MUST_BALANCE") = POST_MUST_BALANCE; + scope().attr("POST_CALCULATED") = POST_CALCULATED; + scope().attr("POST_COST_CALCULATED") = POST_COST_CALCULATED; + + class_< post_t, bases<item_t> > ("Posting") + //.def(init<account_t *>()) + + .add_property("xact", + make_getter(&post_t::xact, + return_internal_reference<>()), + make_setter(&post_t::xact, + with_custodian_and_ward<1, 2>())) + .add_property("account", + make_getter(&post_t::account, + return_internal_reference<>()), + make_setter(&post_t::account, + with_custodian_and_ward<1, 2>())) + .add_property("amount", + make_getter(&post_t::amount), + make_setter(&post_t::amount)) + .add_property("cost", + make_getter(&post_t::cost), + make_setter(&post_t::cost)) + .add_property("assigned_amount", + make_getter(&post_t::assigned_amount), + make_setter(&post_t::assigned_amount)) + + .def("has_tag", py_has_tag_1s) + .def("has_tag", py_has_tag_1m) + .def("has_tag", py_has_tag_2m) + .def("get_tag", py_get_tag_1s) + .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) + + .def("must_balance", &post_t::must_balance) + + .def("lookup", &post_t::lookup) + + .def("valid", &post_t::valid) + + .def("has_xdata", &post_t::has_xdata) + .def("clear_xdata", &post_t::clear_xdata) + .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) + + .def("reported_account", py_reported_account, + return_internal_reference<>()) + ; +} + +} // namespace ledger diff --git a/src/py_times.cc b/src/py_times.cc new file mode 100644 index 00000000..a62bb9b6 --- /dev/null +++ b/src/py_times.cc @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2003-2009, 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 "times.h" + +// jww (2007-05-04): Convert time duration objects to PyDelta + +namespace ledger { + +using namespace boost::python; + +#define MY_PyDateTime_IMPORT \ + PyDateTimeAPI = (PyDateTime_CAPI*) \ + PyCObject_Import(const_cast<char *>("datetime"), \ + const_cast<char *>("datetime_CAPI")) + +struct date_to_python +{ + static PyObject* convert(const date_t& dte) + { + MY_PyDateTime_IMPORT; + return PyDate_FromDate(dte.year(), dte.month(), dte.day()); + } +}; + +struct date_from_python +{ + static void* convertible(PyObject* obj_ptr) + { + MY_PyDateTime_IMPORT; + if (PyDate_Check(obj_ptr)) return obj_ptr; + return 0; + } + + static void construct(PyObject * obj_ptr, + converter::rvalue_from_python_stage1_data * data) + { + MY_PyDateTime_IMPORT; + + int year = PyDateTime_GET_YEAR(obj_ptr); + date::year_type y = gregorian::greg_year(static_cast<unsigned short>(year)); + date::month_type m = + static_cast<date::month_type>(PyDateTime_GET_MONTH(obj_ptr)); + date::day_type d = + static_cast<date::day_type>(PyDateTime_GET_DAY(obj_ptr)); + + date_t * dte = new date_t(y, m, d); + + data->convertible = (void *) dte; + } +}; + +typedef register_python_conversion<date_t, date_to_python, date_from_python> + date_python_conversion; + + +struct datetime_to_python +{ + static PyObject* convert(const datetime_t& moment) + { + MY_PyDateTime_IMPORT; + + date_t dte = moment.date(); + datetime_t::time_duration_type tod = moment.time_of_day(); + + return PyDateTime_FromDateAndTime + (static_cast<int>(dte.year()), static_cast<int>(dte.month()), + static_cast<int>(dte.day()), static_cast<int>(tod.hours()), + static_cast<int>(tod.minutes()), static_cast<int>(tod.seconds()), + static_cast<int>(tod.total_microseconds() % 1000000)); + } +}; + +struct datetime_from_python +{ + static void* convertible(PyObject* obj_ptr) + { + MY_PyDateTime_IMPORT; + if(PyDateTime_Check(obj_ptr)) return obj_ptr; + return 0; + } + + static void construct(PyObject * obj_ptr, + converter::rvalue_from_python_stage1_data * data) + { + MY_PyDateTime_IMPORT; + + int year = PyDateTime_GET_YEAR(obj_ptr); + date::year_type y = gregorian::greg_year(static_cast<unsigned short>(year)); + date::month_type m = + static_cast<date::month_type>(PyDateTime_GET_MONTH(obj_ptr)); + date::day_type d = + static_cast<date::day_type>(PyDateTime_GET_DAY(obj_ptr)); + + datetime_t::time_duration_type::hour_type h = + static_cast<datetime_t::time_duration_type::hour_type> + (PyDateTime_DATE_GET_HOUR(obj_ptr)); + datetime_t::time_duration_type::min_type min = + static_cast<datetime_t::time_duration_type::min_type> + (PyDateTime_DATE_GET_MINUTE(obj_ptr)); + datetime_t::time_duration_type::sec_type s = + static_cast<datetime_t::time_duration_type::sec_type> + (PyDateTime_DATE_GET_SECOND(obj_ptr)); + datetime_t::time_duration_type::fractional_seconds_type ms = + static_cast<datetime_t::time_duration_type::fractional_seconds_type> + (PyDateTime_DATE_GET_MICROSECOND(obj_ptr)) * 1000000; + + datetime_t * moment + = new datetime_t(date_t(y, m, d), + datetime_t::time_duration_type(h, min, s, ms)); + + data->convertible = (void *) moment; + } +}; + +typedef register_python_conversion<datetime_t, + datetime_to_python, datetime_from_python> + datetime_python_conversion; + + +/* Convert time_duration to/from python */ +struct duration_to_python +{ + static int get_usecs(boost::posix_time::time_duration const& d) + { + static int64_t resolution = + boost::posix_time::time_duration::ticks_per_second(); + int64_t fracsecs = d.fractional_seconds(); + if (resolution > 1000000) + return static_cast<int>(fracsecs / (resolution / 1000000)); + else + return static_cast<int>(fracsecs * (1000000 / resolution)); + } + + static PyObject * convert(posix_time::time_duration d) + { + int days = d.hours() / 24; + if (days < 0) + days --; + int seconds = d.total_seconds() - days*(24*3600); + int usecs = get_usecs(d); + if (days < 0) + usecs = 1000000-1 - usecs; + return PyDelta_FromDSU(days, seconds, usecs); + } +}; + +/* Should support the negative values, but not the special boost time + durations */ +struct duration_from_python +{ + static void* convertible(PyObject * obj_ptr) + { + if ( ! PyDelta_Check(obj_ptr)) + return 0; + return obj_ptr; + } + + static void construct(PyObject* obj_ptr, + python::converter::rvalue_from_python_stage1_data * data) + { + PyDateTime_Delta const* pydelta + = reinterpret_cast<PyDateTime_Delta*>(obj_ptr); + + int days = pydelta->days; + bool is_negative = (days < 0); + if (is_negative) + days = -days; + + // Create time duration object + posix_time::time_duration + duration = (posix_time::hours(24) * days + + posix_time::seconds(pydelta->seconds) + + posix_time::microseconds(pydelta->microseconds)); + if (is_negative) + duration = duration.invert_sign(); + + void * storage = + reinterpret_cast<converter::rvalue_from_python_storage + <posix_time::time_duration> *> + (data)->storage.bytes; + + new (storage) posix_time::time_duration(duration); + data->convertible = storage; + } +}; + +typedef register_python_conversion<time_duration_t, + duration_to_python, duration_from_python> + duration_python_conversion; + + +datetime_t py_parse_datetime(const string& str) { + return parse_datetime(str); +} + +date_t py_parse_date(const string& str) { + return parse_date(str); +} + +void export_times() +{ + datetime_python_conversion(); + date_python_conversion(); + duration_python_conversion(); + + register_optional_to_python<datetime_t>(); + register_optional_to_python<date_t>(); + + scope().attr("parse_datetime") = &py_parse_datetime; + scope().attr("parse_date") = &py_parse_date; + scope().attr("times_initialize") = ×_initialize; + scope().attr("times_shutdown") = ×_shutdown; + +#if 0 + class_< interval_t > ("Interval") + ; +#endif +} + +} // namespace ledger diff --git a/src/py_utils.cc b/src/py_utils.cc new file mode 100644 index 00000000..5203599f --- /dev/null +++ b/src/py_utils.cc @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2003-2009, 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 "pyfstream.h" + +namespace ledger { + +using namespace boost::python; + +struct bool_to_python +{ + static PyObject * convert(const bool truth) + { + if (truth) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; + } +}; + +struct bool_from_python +{ + static void* convertible(PyObject* obj_ptr) + { + if (!PyBool_Check(obj_ptr)) return 0; + return obj_ptr; + } + + static void construct(PyObject* obj_ptr, + converter::rvalue_from_python_stage1_data* data) + { + void * storage = + ((converter::rvalue_from_python_storage<bool>*) data)->storage.bytes; + if (obj_ptr == Py_True) + new (storage) bool(true); + else + new (storage) bool(false); + data->convertible = storage; + } +}; + +typedef register_python_conversion<bool, bool_to_python, bool_from_python> + bool_python_conversion; + + +struct string_to_python +{ + static PyObject* convert(const string& str) + { + // Return bytes, not characters; see __unicode__ methods for that + return incref(object(static_cast<const std::string&>(str)).ptr()); + } +}; + +struct string_from_python +{ + static void* convertible(PyObject* obj_ptr) + { + if (!PyUnicode_Check(obj_ptr) && + !PyString_Check(obj_ptr)) return 0; + return obj_ptr; + } + + static void construct(PyObject* obj_ptr, + converter::rvalue_from_python_stage1_data* data) + { + if (PyString_Check(obj_ptr)) { + const char* value = PyString_AsString(obj_ptr); + if (value == 0) throw_error_already_set(); + void* storage = + reinterpret_cast<converter::rvalue_from_python_storage<string> *> + (data)->storage.bytes; + new (storage) string(value); + data->convertible = storage; + } else { + VERIFY(PyUnicode_Check(obj_ptr)); + + Py_ssize_t size = PyUnicode_GET_SIZE(obj_ptr); + const Py_UNICODE* value = PyUnicode_AS_UNICODE(obj_ptr); + + string str; + if (sizeof(Py_UNICODE) == 2) // UTF-16 + utf8::unchecked::utf16to8(value, value + size, std::back_inserter(str)); + else if (sizeof(Py_UNICODE) == 4) // UTF-32 + utf8::unchecked::utf32to8(value, value + size, std::back_inserter(str)); + else + assert(! "Py_UNICODE has an unexpected size"); + + if (value == 0) throw_error_already_set(); + void* storage = + reinterpret_cast<converter::rvalue_from_python_storage<string> *> + (data)->storage.bytes; + new (storage) string(str); + data->convertible = storage; + } + } +}; + +typedef register_python_conversion<string, string_to_python, string_from_python> + string_python_conversion; + + +struct istream_to_python +{ + static PyObject* convert(const std::istream&) + { + return incref(boost::python::detail::none()); + } +}; + +struct istream_from_python +{ + static void* convertible(PyObject* obj_ptr) + { + if (!PyFile_Check(obj_ptr)) return 0; + return obj_ptr; + } + + static void construct(PyObject* obj_ptr, + converter::rvalue_from_python_stage1_data* data) + { + void* storage = + reinterpret_cast<converter::rvalue_from_python_storage<pyifstream> *> + (data)->storage.bytes; + new (storage) pyifstream(reinterpret_cast<PyFileObject *>(obj_ptr)); + data->convertible = storage; + } +}; + +typedef register_python_conversion<std::istream, + istream_to_python, istream_from_python> + istream_python_conversion; + + +struct ostream_to_python +{ + static PyObject* convert(const std::ostream&) + { + return incref(boost::python::detail::none()); + } +}; + +struct ostream_from_python +{ + static void* convertible(PyObject* obj_ptr) + { + if (!PyFile_Check(obj_ptr)) return 0; + return obj_ptr; + } + + static void construct(PyObject* obj_ptr, + converter::rvalue_from_python_stage1_data* data) + { + void* storage = + reinterpret_cast<converter::rvalue_from_python_storage<pyofstream> *> + (data)->storage.bytes; + new (storage) pyofstream(reinterpret_cast<PyFileObject *>(obj_ptr)); + data->convertible = storage; + } +}; + +typedef register_python_conversion<std::ostream, + ostream_to_python, ostream_from_python> + ostream_python_conversion; + + +void export_utils() +{ + class_< supports_flags<uint_least8_t> > ("SupportFlags8") + .def(init<supports_flags<uint_least8_t> >()) + .def(init<uint_least8_t>()) + + .add_property("flags", + &supports_flags<uint_least8_t>::flags, + &supports_flags<uint_least8_t>::set_flags) + .def("has_flags", &supports_flags<uint_least8_t>::has_flags) + .def("clear_flags", &supports_flags<uint_least8_t>::clear_flags) + .def("add_flags", &supports_flags<uint_least8_t>::add_flags) + .def("drop_flags", &supports_flags<uint_least8_t>::drop_flags) + ; + + class_< supports_flags<uint_least16_t> > ("SupportFlags16") + .def(init<supports_flags<uint_least16_t> >()) + .def(init<uint_least16_t>()) + + .add_property("flags", + &supports_flags<uint_least16_t>::flags, + &supports_flags<uint_least16_t>::set_flags) + .def("has_flags", &supports_flags<uint_least16_t>::has_flags) + .def("clear_flags", &supports_flags<uint_least16_t>::clear_flags) + .def("add_flags", &supports_flags<uint_least16_t>::add_flags) + .def("drop_flags", &supports_flags<uint_least16_t>::drop_flags) + ; + +#if 0 + class_< basic_flags_t<uint_least8_t>, + bases<supports_flags<uint_least8_t> > > ("BasicFlags8") + .def(init<uint_least8_t>()) + + .def("plus_flags", &basic_flags_t<uint_least8_t>::plus_flags) + .def("minus_flags", &basic_flags_t<uint_least8_t>::minus_flags) + ; +#endif + + class_< delegates_flags<uint_least16_t>, + boost::noncopyable > ("DelegatesFlags16", no_init) + .add_property("flags", + &delegates_flags<uint_least16_t>::flags, + &delegates_flags<uint_least16_t>::set_flags) + .def("has_flags", &delegates_flags<uint_least16_t>::has_flags) + .def("clear_flags", &delegates_flags<uint_least16_t>::clear_flags) + .def("add_flags", &delegates_flags<uint_least16_t>::add_flags) + .def("drop_flags", &delegates_flags<uint_least16_t>::drop_flags) + ; + + bool_python_conversion(); + string_python_conversion(); + istream_python_conversion(); + ostream_python_conversion(); +} + +} // namespace ledger diff --git a/src/py_value.cc b/src/py_value.cc new file mode 100644 index 00000000..713dc3d4 --- /dev/null +++ b/src/py_value.cc @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2003-2009, 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 "commodity.h" +#include "annotate.h" + +namespace ledger { + +using namespace boost::python; + +BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(value_overloads, value, 0, 2) +BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(exchange_commodities_overloads, + exchange_commodities, 1, 2) +BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(set_string_overloads, set_string, 0, 2) + +namespace { + + boost::optional<value_t> py_value_0(const value_t& value) { + return value.value(false, CURRENT_TIME()); + } + boost::optional<value_t> py_value_1(const value_t& value, + commodity_t& in_terms_of) { + return value.value(false, CURRENT_TIME(), in_terms_of); + } + boost::optional<value_t> py_value_2(const value_t& value, + commodity_t& in_terms_of, + datetime_t& moment) { + return value.value(false, moment, in_terms_of); + } + + PyObject * py_base_type(value_t& value) + { + if (value.is_boolean()) { + return (PyObject *)&PyBool_Type; + } + else if (value.is_long()) { + return (PyObject *)&PyInt_Type; + } + else if (value.is_string()) { + return (PyObject *)&PyUnicode_Type; + } + else { + object typeobj(object(value).attr("__class__")); + return typeobj.ptr(); + } + } + + string py_dump(const value_t& value) { + std::ostringstream buf; + value.dump(buf); + return buf.str(); + } + + string py_dump_relaxed(const value_t& value) { + std::ostringstream buf; + value.dump(buf, true); + return buf.str(); + } + + void py_set_string(value_t& value, const string& str) { + return value.set_string(str); + } + + annotation_t& py_value_annotation(value_t& value) { + return value.annotation(); + } + + value_t py_strip_annotations_0(value_t& value) { + return value.strip_annotations(keep_details_t()); + } + value_t py_strip_annotations_1(value_t& value, const keep_details_t& keep) { + return value.strip_annotations(keep); + } + + PyObject * py_value_unicode(value_t& value) { + return str_to_py_unicode(value.to_string()); + } + +} // unnamed namespace + +#define EXC_TRANSLATOR(type) \ + void exc_translate_ ## type(const type& err) { \ + PyErr_SetString(PyExc_ArithmeticError, err.what()); \ + } + +EXC_TRANSLATOR(value_error) + +void export_value() +{ + enum_< value_t::type_t >("ValueType") + .value("Void", value_t::VOID) + .value("Boolean", value_t::BOOLEAN) + .value("DateTime", value_t::DATETIME) + .value("Date", value_t::DATE) + .value("Integer", value_t::INTEGER) + .value("Amount", value_t::AMOUNT) + .value("Balance", value_t::BALANCE) + .value("String", value_t::STRING) + .value("Sequence", value_t::SEQUENCE) + .value("Scope", value_t::SCOPE) + ; + + class_< value_t > ("Value") + .def("initialize", &value_t::initialize) + .staticmethod("initialize") + .def("shutdown", &value_t::shutdown) + .staticmethod("shutdown") + + .def(init<bool>()) + .def(init<datetime_t>()) + .def(init<date_t>()) + .def(init<long>()) + .def(init<double>()) + .def(init<amount_t>()) + .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 + //.def(init<value_t::sequence_t>()) + .def(init<value_t>()) + + .def("is_equal_to", &value_t::is_equal_to) + .def("is_less_than", &value_t::is_less_than) + .def("is_greater_than", &value_t::is_greater_than) + + .def(self == self) + .def(self == long()) + .def(long() == self) + .def(self == other<amount_t>()) + .def(other<amount_t>() == self) + .def(self == other<balance_t>()) + .def(other<balance_t>() == self) + + .def(self != self) + .def(self != long()) + .def(long() != self) + .def(self != other<amount_t>()) + .def(other<amount_t>() != self) + .def(self != other<balance_t>()) + .def(other<balance_t>() != self) + + .def(! self) + + .def(self < self) + .def(self < long()) + .def(long() < self) + .def(self < other<amount_t>()) + .def(other<amount_t>() < self) + + .def(self <= self) + .def(self <= long()) + .def(long() <= self) + .def(self <= other<amount_t>()) + .def(other<amount_t>() <= self) + + .def(self > self) + .def(self > long()) + .def(long() > self) + .def(self > other<amount_t>()) + .def(other<amount_t>() > self) + + .def(self >= self) + .def(self >= long()) + .def(long() >= self) + .def(self >= other<amount_t>()) + .def(other<amount_t>() >= self) + + .def(self += self) + .def(self += long()) + .def(self += other<amount_t>()) + .def(self += other<balance_t>()) + + .def(self + self) + .def(self + long()) + .def(long() + self) + .def(self + other<amount_t>()) + .def(other<amount_t>() + self) + .def(self + other<balance_t>()) + + .def(self -= self) + .def(self -= long()) + .def(self -= other<amount_t>()) + .def(self -= other<balance_t>()) + + .def(self - self) + .def(self - long()) + .def(long() - self) + .def(self - other<amount_t>()) + .def(other<amount_t>() - self) + .def(self - other<balance_t>()) + + .def(self *= self) + .def(self *= long()) + .def(self *= other<amount_t>()) + + .def(self * self) + .def(self * long()) + .def(long() * self) + .def(self * other<amount_t>()) + .def(other<amount_t>() * self) + + .def(self /= self) + .def(self /= long()) + .def(self /= other<amount_t>()) + + .def(self / self) + .def(self / long()) + .def(long() / self) + .def(self / other<amount_t>()) + .def(other<amount_t>() / self) + + .def("negated", &value_t::negated) + .def("in_place_negate", &value_t::in_place_negate) + .def("in_place_not", &value_t::in_place_not) + .def(- self) + + .def("abs", &value_t::abs) + .def("__abs__", &value_t::abs) + + .def("rounded", &value_t::rounded) + .def("in_place_round", &value_t::in_place_round) + .def("truncated", &value_t::truncated) + .def("in_place_truncate", &value_t::in_place_truncate) + .def("floored", &value_t::floored) + .def("in_place_floor", &value_t::in_place_floor) + .def("unrounded", &value_t::unrounded) + .def("in_place_unround", &value_t::in_place_unround) + .def("reduced", &value_t::reduced) + .def("in_place_reduce", &value_t::in_place_reduce) + .def("unreduced", &value_t::unreduced) + .def("in_place_unreduce", &value_t::in_place_unreduce) + + .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", &value_t::value, value_overloads()) + .def("price", &value_t::price) + .def("exchange_commodities", &value_t::exchange_commodities, + exchange_commodities_overloads()) + + .def("__nonzero__", &value_t::is_nonzero) + .def("is_nonzero", &value_t::is_nonzero) + .def("is_realzero", &value_t::is_realzero) + .def("is_zero", &value_t::is_zero) + .def("is_null", &value_t::is_null) + + .def("type", &value_t::type) + .def("is_type", &value_t::is_type) + + .def("is_boolean", &value_t::is_boolean) + .def("set_boolean", &value_t::set_boolean) + + .def("is_datetime", &value_t::is_datetime) + .def("set_datetime", &value_t::set_datetime) + + .def("is_date", &value_t::is_date) + .def("set_date", &value_t::set_date) + + .def("is_long", &value_t::is_long) + .def("set_long", &value_t::set_long) + + .def("is_amount", &value_t::is_amount) + .def("is_amount", &value_t::is_amount) + + .def("is_balance", &value_t::is_balance) + .def("is_balance", &value_t::is_balance) + + .def("is_string", &value_t::is_string) + .def("set_string", py_set_string) + + .def("is_mask", &value_t::is_mask) + .def("is_mask", &value_t::is_mask) + + .def("is_sequence", &value_t::is_sequence) + .def("set_sequence", &value_t::set_sequence) + + .def("to_boolean", &value_t::to_boolean) + .def("to_long", &value_t::to_long) + .def("__int__", &value_t::to_long) + .def("to_datetime", &value_t::to_datetime) + .def("to_date", &value_t::to_date) + .def("to_amount", &value_t::to_amount) + .def("to_balance", &value_t::to_balance) + .def("__str__", &value_t::to_string) + .def("__unicode__", py_value_unicode) + .def("to_string", &value_t::to_string) + .def("to_mask", &value_t::to_mask) + .def("to_sequence", &value_t::to_sequence) + + .def("__unicode__", py_dump_relaxed) + .def("__repr__", py_dump) + + .def("casted", &value_t::casted) + .def("in_place_cast", &value_t::in_place_cast) + .def("simplified", &value_t::simplified) + .def("in_place_simplify", &value_t::in_place_simplify) + + .def("number", &value_t::number) + + .def("annotate", &value_t::annotate) + .def("has_annotation", &value_t::has_annotation) + .add_property("annotation", + make_function(py_value_annotation, + return_internal_reference<>())) + .def("strip_annotations", py_strip_annotations_0) + .def("strip_annotations", py_strip_annotations_1) + +#if 0 + .def("__getitem__", &value_t::operator[]) +#endif + .def("push_back", &value_t::push_back) + .def("pop_back", &value_t::pop_back) + .def("size", &value_t::size) + + .def("label", &value_t::label) + + .def("valid", &value_t::valid) + + .def("basetype", py_base_type) + ; + + class_< value_t::sequence_t > ("ValueSequence") + .def(vector_indexing_suite< value_t::sequence_t >()); + ; + + scope().attr("NULL_VALUE") = NULL_VALUE; + scope().attr("string_value") = &string_value; + scope().attr("mask_value") = &mask_value; + scope().attr("value_context") = &value_context; + + register_optional_to_python<value_t>(); + + implicitly_convertible<long, value_t>(); + implicitly_convertible<string, value_t>(); + implicitly_convertible<amount_t, value_t>(); + implicitly_convertible<balance_t, value_t>(); + implicitly_convertible<mask_t, value_t>(); + implicitly_convertible<date_t, value_t>(); + implicitly_convertible<datetime_t, value_t>(); + +#define EXC_TRANSLATE(type) \ + register_exception_translator<type>(&exc_translate_ ## type); + + EXC_TRANSLATE(value_error); +} + +} // namespace ledger diff --git a/src/py_xact.cc b/src/py_xact.cc new file mode 100644 index 00000000..59c599d9 --- /dev/null +++ b/src/py_xact.cc @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2003-2009, 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 "xact.h" +#include "post.h" + +namespace ledger { + +using namespace boost::python; + +namespace { + + long posts_len(xact_base_t& xact) + { + return xact.posts.size(); + } + + post_t& posts_getitem(xact_base_t& xact, long i) + { + static long last_index = 0; + static xact_base_t * last_xact = NULL; + static posts_list::iterator elem; + + long len = xact.posts.size(); + + if (labs(i) >= len) { + PyErr_SetString(PyExc_IndexError, _("Index out of range")); + throw_error_already_set(); + } + + if (&xact == last_xact && i == last_index + 1) { + last_index = i; + return **++elem; + } + + long x = i < 0 ? len + i : i; + elem = xact.posts.begin(); + while (--x >= 0) + elem++; + + last_xact = &xact; + last_index = i; + + return **elem; + } + +} // unnamed namespace + +using namespace boost::python; + +void export_xact() +{ + class_< xact_base_t, bases<item_t> > ("TransactionBase") + .add_property("journal", + make_getter(&xact_base_t::journal, + return_internal_reference<>()), + make_setter(&xact_base_t::journal, + with_custodian_and_ward<1, 2>())) + + .def("__len__", posts_len) + .def("__getitem__", posts_getitem, + return_internal_reference<>()) + + .def("add_post", &xact_base_t::add_post, with_custodian_and_ward<1, 2>()) + .def("remove_post", &xact_base_t::add_post) + + .def("finalize", &xact_base_t::finalize) + + .def("__iter__", range<return_internal_reference<> > + (&xact_t::posts_begin, &xact_t::posts_end)) + .def("posts", range<return_internal_reference<> > + (&xact_t::posts_begin, &xact_t::posts_end)) + + .def("valid", &xact_base_t::valid) + ; + + class_< xact_t, bases<xact_base_t> > ("Transaction") + .add_property("code", + make_getter(&xact_t::code), + make_setter(&xact_t::code)) + .add_property("payee", + make_getter(&xact_t::payee), + make_setter(&xact_t::payee)) + + .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) + + .def("has_xdata", &xact_t::has_xdata) + .def("clear_xdata", &xact_t::clear_xdata) + + .def("valid", &xact_t::valid) + ; + + class_< auto_xact_t, bases<xact_base_t> > ("AutomatedTransaction") + .def(init<predicate_t>()) + + .add_property("predicate", + make_getter(&auto_xact_t::predicate), + make_setter(&auto_xact_t::predicate)) + + .def("extend_xact", &auto_xact_t::extend_xact) + ; + + class_< period_xact_t, bases<xact_base_t> > ("PeriodicTransaction") + .def(init<string>()) + + .add_property("period", + make_getter(&period_xact_t::period), + make_setter(&period_xact_t::period)) + .add_property("period_string", + make_getter(&period_xact_t::period_string), + make_setter(&period_xact_t::period_string)) + ; +} + +} // namespace ledger diff --git a/src/pyfstream.h b/src/pyfstream.h new file mode 100644 index 00000000..3da37523 --- /dev/null +++ b/src/pyfstream.h @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2003-2009, 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. + */ + +#ifndef _PYFSTREAM_H +#define _PYFSTREAM_H + +// pyofstream +// - a stream that writes on a Python file object + +class pyoutbuf : public boost::noncopyable, public std::streambuf +{ + pyoutbuf(); + +protected: + PyFileObject * fo; // Python file object + +public: + // constructor + pyoutbuf(PyFileObject * _fo) : fo(_fo) { + TRACE_CTOR(pyoutbuf, "PyFileObject *"); + } + ~pyoutbuf() throw() { + TRACE_DTOR(pyoutbuf); + } + +protected: + // write one character + virtual int_type overflow (int_type c) { + if (c != EOF) { + char z[2]; + z[0] = static_cast<char>(c); + z[1] = '\0'; + if (PyFile_WriteString(z, reinterpret_cast<PyObject *>(fo)) < 0) { + return EOF; + } + } + return c; + } + + // write multiple characters + virtual std::streamsize xsputn (const char* s, std::streamsize num) { + char * buf = new char[num + 1]; + std::strncpy(buf, s, num); + buf[num] = '\0'; + if (PyFile_WriteString(buf, reinterpret_cast<PyObject *>(fo)) < 0) + num = 0; + boost::checked_array_delete(buf); + return num; + } +}; + +class pyofstream : public boost::noncopyable, public std::ostream +{ + pyofstream(); + +protected: + pyoutbuf buf; + +public: + pyofstream (PyFileObject * fo) : std::ostream(0), buf(fo) { + TRACE_CTOR(pyofstream, "PyFileObject *"); + rdbuf(&buf); + } + ~pyofstream() throw() { + TRACE_DTOR(pyofstream); + } +}; + +// pyifstream +// - a stream that reads on a file descriptor + +class pyinbuf : public boost::noncopyable, public std::streambuf +{ + pyinbuf(); + +protected: + PyFileObject * fo; // Python file object + +protected: + /* data buffer: + * - at most, pbSize characters in putback area plus + * - at most, bufSize characters in ordinary read buffer + */ + static const size_t pbSize = 4; // size of putback area + static const size_t bufSize = 1024; // size of the data buffer + char buffer[bufSize + pbSize]; // data buffer + +public: + /* constructor + * - initialize file descriptor + * - initialize empty data buffer + * - no putback area + * => 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 + } + ~pyinbuf() throw() { + TRACE_DTOR(pyinbuf); + } + +protected: + // insert new characters into the buffer + virtual int_type underflow () { +#ifndef _MSC_VER + using std::memmove; +#endif + + // is read position before end of buffer? + if (gptr() < egptr()) { + return traits_type::to_int_type(*gptr()); + } + + /* process size of putback area + * - use number of characters read + * - but at most size of putback area + */ + size_t numPutback; + numPutback = gptr() - eback(); + if (numPutback > pbSize) { + numPutback = pbSize; + } + + /* copy up to pbSize characters previously read into + * the putback area + */ + memmove (buffer+(pbSize-numPutback), gptr()-numPutback, + numPutback); + + // read at most bufSize new characters + PyObject *line = PyFile_GetLine(reinterpret_cast<PyObject *>(fo), bufSize); + if (! line || ! PyString_Check(line)) { + // ERROR or EOF + return EOF; + } + + Py_ssize_t num = PyString_Size(line); + if (num == 0) + return EOF; + + memmove (buffer+pbSize, PyString_AsString(line), num); + + // reset buffer pointers + setg (buffer+(pbSize-numPutback), // beginning of putback area + buffer+pbSize, // read position + buffer+pbSize+num); // end of buffer + + // return next character + return traits_type::to_int_type(*gptr()); + } +}; + +class pyifstream : public boost::noncopyable, public std::istream +{ + pyifstream(); + +protected: + pyinbuf buf; + +public: + pyifstream (PyFileObject * fo) : std::istream(0), buf(fo) { + TRACE_CTOR(pyifstream, "PyFileObject *"); + rdbuf(&buf); + } + ~pyifstream() throw() { + TRACE_DTOR(pyifstream); + } +}; + +#endif // _PYFSTREAM_H diff --git a/src/pyinterp.cc b/src/pyinterp.cc new file mode 100644 index 00000000..0701176f --- /dev/null +++ b/src/pyinterp.cc @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2003-2009, 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 "account.h" +#include "xact.h" +#include "post.h" + +namespace ledger { + +using namespace python; + +shared_ptr<python_interpreter_t> python_session; + +char * argv0; + +void export_account(); +void export_amount(); +void export_balance(); +void export_commodity(); +void export_expr(); +void export_format(); +void export_item(); +void export_journal(); +void export_post(); +void export_times(); +void export_utils(); +void export_value(); +void export_xact(); + +void initialize_for_python() +{ + export_times(); + export_utils(); + export_commodity(); + export_amount(); + export_value(); + export_account(); + export_balance(); + export_expr(); + export_format(); + export_item(); + export_post(); + export_xact(); + export_journal(); +} + +struct python_run +{ + object result; + + 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())))) {} + operator object() { + return result; + } +}; + +void python_interpreter_t::initialize() +{ + TRACE_START(python_init, 1, "Initialized Python"); + + try { + DEBUG("python.interp", "Initializing Python"); + + Py_Initialize(); + assert(Py_IsInitialized()); + + 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__)")); + + python::detail::init_module("ledger", &initialize_for_python); + + is_initialized = true; + } + catch (const error_already_set&) { + PyErr_Print(); + throw_(std::runtime_error, _("Python failed to initialize")); + } + + TRACE_FINISH(python_init, 1); +} + +void python_interpreter_t::hack_system_paths() +{ + // Hack ledger.__path__ so it points to a real location + python::object sys_module = python::import("sys"); + python::object sys_dict = sys_module.attr("__dict__"); + + python::list paths(sys_dict["path"]); + +#if defined(DEBUG_ON) + bool path_initialized = false; +#endif + int n = python::extract<int>(paths.attr("__len__")()); + for (int i = 0; i < n; i++) { + python::extract<std::string> str(paths[i]); + path pathname(str); + DEBUG("python.interp", "sys.path = " << pathname); + + if (exists(pathname / "ledger" / "__init__.py")) { + if (python::object module_ledger = python::import("ledger")) { + DEBUG("python.interp", + "Setting ledger.__path__ = " << (pathname / "ledger")); + + python::object ledger_dict = module_ledger.attr("__dict__"); + python::list temp_list; + temp_list.append((pathname / "ledger").string()); + + ledger_dict["__path__"] = temp_list; + } else { + throw_(std::runtime_error, + _("Python failed to initialize (couldn't find ledger)")); + } +#if defined(DEBUG_ON) + path_initialized = true; +#endif + break; + } + } +#if defined(DEBUG_ON) + if (! path_initialized) + DEBUG("python.init", + "Ledger failed to find 'ledger/__init__.py' on the PYTHONPATH"); +#endif +} + +object python_interpreter_t::import_into_main(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__"); + + python::list paths(sys_dict["path"]); + +#if BOOST_VERSION >= 103700 + paths.insert(0, file.parent_path().string()); + sys_dict["path"] = paths; + + string name = file.filename(); + if (contains(name, ".py")) + name = file.stem(); +#else // BOOST_VERSION >= 103700 + paths.insert(0, file.branch_path().string()); + sys_dict["path"] = paths; + + string name = file.leaf(); +#endif // BOOST_VERSION >= 103700 + + return python::import(python::str(name.c_str())); +} + +object python_interpreter_t::eval(std::istream& in, py_eval_mode_t mode) +{ + bool first = true; + string buffer; + buffer.reserve(4096); + + while (! in.eof()) { + char buf[256]; + in.getline(buf, 255); + if (buf[0] == '!') + break; + if (first) + first = false; + else + buffer += "\n"; + buffer += buf; + } + + if (! is_initialized) + initialize(); + + try { + int input_mode = -1; + switch (mode) { + case PY_EVAL_EXPR: input_mode = Py_eval_input; break; + case PY_EVAL_STMT: input_mode = Py_single_input; break; + case PY_EVAL_MULTI: input_mode = Py_file_input; break; + } + + return python_run(this, buffer, input_mode); + } + catch (const error_already_set&) { + PyErr_Print(); + throw_(std::runtime_error, _("Failed to evaluate Python code")); + } + return object(); +} + +object python_interpreter_t::eval(const string& str, py_eval_mode_t mode) +{ + if (! is_initialized) + initialize(); + + try { + int input_mode = -1; + switch (mode) { + case PY_EVAL_EXPR: input_mode = Py_eval_input; break; + case PY_EVAL_STMT: input_mode = Py_single_input; break; + case PY_EVAL_MULTI: input_mode = Py_file_input; break; + } + + return python_run(this, str, input_mode); + } + catch (const error_already_set&) { + PyErr_Print(); + throw_(std::runtime_error, _("Failed to evaluate Python code")); + } + return object(); +} + +value_t python_interpreter_t::python_command(call_scope_t& args) +{ + if (! is_initialized) + initialize(); + + char ** argv(new char *[args.size() + 1]); + + argv[0] = new char[std::strlen(argv0) + 1]; + std::strcpy(argv[0], argv0); + + for (std::size_t i = 0; i < args.size(); i++) { + string arg = args[i].as_string(); + argv[i + 1] = new char[arg.length() + 1]; + std::strcpy(argv[i + 1], arg.c_str()); + } + + int status = 1; + + try { + status = Py_Main(static_cast<int>(args.size()) + 1, argv); + } + catch (const error_already_set&) { + PyErr_Print(); + throw_(std::runtime_error, _("Failed to execute Python module")); + } + catch (...) { + for (std::size_t i = 0; i < args.size() + 1; i++) + delete[] argv[i]; + delete[] argv; + throw; + } + + for (std::size_t i = 0; i < args.size() + 1; i++) + delete[] argv[i]; + delete[] argv; + + if (status != 0) + throw status; + + return NULL_VALUE; +} + +value_t python_interpreter_t::server_command(call_scope_t& args) +{ + if (! is_initialized) + initialize(); + + python::object server_module; + + try { + server_module = python::import("ledger.server"); + if (! server_module) + throw_(std::runtime_error, + _("Could not import ledger.server; please check your PYTHONPATH")); + } + catch (const error_already_set&) { + PyErr_Print(); + throw_(std::runtime_error, + _("Could not import ledger.server; please check your PYTHONPATH")); + } + + if (python::object main_function = server_module.attr("main")) { + functor_t func(main_function, "main"); + try { + func(args); + } + 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!")); + } + + return false; +} + +option_t<python_interpreter_t> * +python_interpreter_t::lookup_option(const char * p) +{ + switch (*p) { + case 'i': + OPT(import_); + break; + } + return NULL; +} + +expr_t::ptr_op_t python_interpreter_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + // Give our superclass first dibs on symbol definitions + if (expr_t::ptr_op_t op = session_t::lookup(kind, name)) + return op; + + 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)); + } + break; + + case symbol_t::OPTION: + if (option_t<python_interpreter_t> * handler = lookup_option(name.c_str())) + return MAKE_OPT_HANDLER(python_interpreter_t, handler); + break; + + case symbol_t::PRECOMMAND: { + const char * p = name.c_str(); + switch (*p) { + case 'p': + if (is_eq(p, "python")) + return MAKE_FUNCTOR(python_interpreter_t::python_command); + break; + + case 's': + if (is_eq(p, "server")) + return MAKE_FUNCTOR(python_interpreter_t::server_command); + break; + } + } + + default: + break; + } + + return NULL; +} + +namespace { + void append_value(list& lst, const value_t& value) + { + 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); + } + } +} + +value_t python_interpreter_t::functor_t::operator()(call_scope_t& args) +{ + try { + std::signal(SIGINT, SIG_DFL); + + if (! PyCallable_Check(func.ptr())) { + extract<value_t> val(func); + std::signal(SIGINT, sigint_handler); + if (val.check()) + return val(); + return NULL_VALUE; + } + else if (args.size() > 0) { + list arglist; + // jww (2009-11-05): What about a single argument which is a sequence, + // rather than a sequence of arguments? + if (args.value().is_sequence()) + foreach (const value_t& value, args.value().as_sequence()) + append_value(arglist, value); + else + append_value(arglist, args.value()); + + if (PyObject * val = + PyObject_CallObject(func.ptr(), python::tuple(arglist).ptr())) { + extract<value_t> xval(val); + value_t result; + if (xval.check()) { + result = xval(); + Py_DECREF(val); + } else { + Py_DECREF(val); + throw_(calc_error, + _("Could not evaluate Python variable '%1'") << name); + } + std::signal(SIGINT, sigint_handler); + return result; + } + else if (PyErr_Occurred()) { + PyErr_Print(); + throw_(calc_error, _("Failed call to Python function '%1'") << name); + } else { + assert(false); + } + } + else { + std::signal(SIGINT, sigint_handler); + return call<value_t>(func.ptr()); + } + } + catch (const error_already_set&) { + std::signal(SIGINT, sigint_handler); + PyErr_Print(); + throw_(calc_error, _("Failed call to Python function '%1'") << name); + } + catch (...) { + std::signal(SIGINT, sigint_handler); + } + std::signal(SIGINT, sigint_handler); + + return NULL_VALUE; +} + +} // namespace ledger diff --git a/src/pyinterp.h b/src/pyinterp.h new file mode 100644 index 00000000..f2d7b760 --- /dev/null +++ b/src/pyinterp.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2003-2009, 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. + */ + +#ifndef _PYINTERP_H +#define _PYINTERP_H + +#include "session.h" + +#if defined(HAVE_BOOST_PYTHON) + +namespace ledger { + +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) { + TRACE_CTOR(python_interpreter_t, ""); + } + + virtual ~python_interpreter_t() { + TRACE_DTOR(python_interpreter_t); + + if (is_initialized) + Py_Finalize(); + } + + 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 { + PY_EVAL_EXPR, + PY_EVAL_STMT, + 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); + } + + value_t python_command(call_scope_t& scope); + value_t server_command(call_scope_t& args); + + class functor_t { + functor_t(); + + protected: + python::object func; + + public: + string name; + + functor_t(python::object _func, const string& _name) + : func(_func), name(_name) { + TRACE_CTOR(functor_t, "python::object, const string&"); + } + functor_t(const functor_t& other) + : func(other.func), name(other.name) { + TRACE_CTOR(functor_t, "copy"); + } + virtual ~functor_t() throw() { + TRACE_DTOR(functor_t); + } + virtual value_t operator()(call_scope_t& args); + }; + + option_t<python_interpreter_t> * lookup_option(const char * p); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + OPTION_(python_interpreter_t, import_, DO_(scope) { + interactive_t args(scope, "ss"); + parent->import_option(args.get<string>(1)); + }); +}; + +extern shared_ptr<python_interpreter_t> python_session; + +} // namespace ledger + +#endif // HAVE_BOOST_PYTHON + +#endif // _PYINTERP_H diff --git a/src/pyledger.cc b/src/pyledger.cc new file mode 100644 index 00000000..cf7e1527 --- /dev/null +++ b/src/pyledger.cc @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2003-2009, 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" + +using namespace boost::python; + +namespace ledger { + extern void initialize_for_python(); +} + +BOOST_PYTHON_MODULE(ledger) +{ + using namespace ledger; + + if (! python_session.get()) + python_session.reset(new python_interpreter_t); + + set_session_context(python_session.get()); + + initialize_for_python(); +} diff --git a/src/pyutils.h b/src/pyutils.h new file mode 100644 index 00000000..54d6fa28 --- /dev/null +++ b/src/pyutils.h @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2003-2009, 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. + */ + +#ifndef _PY_UTILS_H +#define _PY_UTILS_H + +template <typename T, typename TfromPy> +struct object_from_python +{ + object_from_python() { + boost::python::converter::registry::insert + (&TfromPy::convertible, &TfromPy::construct, + boost::python::type_id<T>()); + } +}; + +template <typename T, typename TtoPy, typename TfromPy> +struct register_python_conversion +{ + register_python_conversion() { + boost::python::to_python_converter<T, TtoPy>(); + object_from_python<T, TfromPy>(); + } +}; + +template <typename T> +struct register_optional_to_python : public boost::noncopyable +{ + struct optional_to_python + { + static PyObject * convert(const boost::optional<T>& value) + { + return boost::python::incref + (value ? boost::python::to_python_value<T>()(*value) : + boost::python::detail::none()); + } + }; + + struct optional_from_python + { + static void * convertible(PyObject * source) + { + using namespace boost::python::converter; + + if (source == Py_None) + return source; + + const registration& converters(registered<T>::converters); + + if (implicit_rvalue_convertible_from_python(source, converters)) { + rvalue_from_python_stage1_data data = + rvalue_from_python_stage1(source, converters); + return rvalue_from_python_stage2(source, data, converters); + } + return NULL; + } + + static void construct(PyObject * source, + boost::python::converter::rvalue_from_python_stage1_data * data) + { + using namespace boost::python::converter; + + void * const storage = + reinterpret_cast<rvalue_from_python_storage<T> *>(data)->storage.bytes; + + if (data->convertible == source) // == None + new (storage) boost::optional<T>(); // A Boost uninitialized value + else + new (storage) boost::optional<T>(*reinterpret_cast<T *>(data->convertible)); + + data->convertible = storage; + } + }; + + explicit register_optional_to_python() { + register_python_conversion<boost::optional<T>, + optional_to_python, optional_from_python>(); + } +}; + +template <typename T1, typename T2> +struct PairToTupleConverter +{ + static PyObject * convert(const std::pair<T1, T2>& pair) { + return boost::python::incref + (boost::python::make_tuple(pair.first, pair.second).ptr()); + } +}; + +template <typename MapType> +struct map_value_type_converter +{ + map_value_type_converter() { + boost::python::to_python_converter + <typename MapType::value_type, + PairToTupleConverter<const typename MapType::key_type, + typename MapType::mapped_type> >(); + } +}; + +template <typename T> +PyObject * str_to_py_unicode(const T& str) +{ + using namespace boost::python; + PyObject * pstr = PyString_FromString(str.c_str()); + PyObject * uni = PyUnicode_FromEncodedObject(pstr, "UTF-8", NULL); + return object(handle<>(borrowed(uni))).ptr(); +} + +namespace boost { namespace python { + +// Use expr to create the PyObject corresponding to x +# define BOOST_PYTHON_RETURN_TO_PYTHON_BY_VALUE(T, expr, pytype)\ + template <> struct to_python_value<T&> \ + : detail::builtin_to_python \ + { \ + inline PyObject* operator()(T const& x) const \ + { \ + return (expr); \ + } \ + inline PyTypeObject const* get_pytype() const \ + { \ + return (pytype); \ + } \ + }; \ + template <> struct to_python_value<T const&> \ + : detail::builtin_to_python \ + { \ + inline PyObject* operator()(T const& x) const \ + { \ + return (expr); \ + } \ + inline PyTypeObject const* get_pytype() const \ + { \ + return (pytype); \ + } \ + }; + +# define BOOST_PYTHON_ARG_TO_PYTHON_BY_VALUE(T, expr) \ + namespace converter \ + { \ + template <> struct arg_to_python< T > \ + : handle<> \ + { \ + arg_to_python(T const& x) \ + : python::handle<>(expr) {} \ + }; \ + } + +// Specialize argument and return value converters for T using expr +# define BOOST_PYTHON_TO_PYTHON_BY_VALUE(T, expr, pytype) \ + BOOST_PYTHON_RETURN_TO_PYTHON_BY_VALUE(T,expr, pytype) \ + BOOST_PYTHON_ARG_TO_PYTHON_BY_VALUE(T,expr) + +BOOST_PYTHON_TO_PYTHON_BY_VALUE(ledger::string, ::PyUnicode_FromEncodedObject(::PyString_FromString(x.c_str()), "UTF-8", NULL), &PyUnicode_Type) + +} } // namespace boost::python + +//boost::python::register_ptr_to_python< boost::shared_ptr<Base> >(); + +#endif // _PY_UTILS_H diff --git a/src/query.cc b/src/query.cc new file mode 100644 index 00000000..cfa321b0 --- /dev/null +++ b/src/query.cc @@ -0,0 +1,455 @@ +/* + * Copyright (c) 2003-2009, 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 "query.h" +#include "op.h" + +namespace ledger { + +query_t::lexer_t::token_t query_t::lexer_t::next_token() +{ + if (token_cache.kind != token_t::UNKNOWN) { + token_t tok = token_cache; + token_cache = token_t(); + return tok; + } + + if (arg_i == arg_end) { + if (begin == end || ++begin == end) { + return token_t(token_t::END_REACHED); + } else { + arg_i = (*begin).as_string().begin(); + arg_end = (*begin).as_string().end(); + } + } + + if (consume_next_arg) { + consume_next_arg = false; + arg_i = arg_end; + return token_t(token_t::TERM, (*begin).as_string()); + } + + resume: + bool consume_next = false; + switch (*arg_i) { + case ' ': + case '\t': + case '\r': + case '\n': + if (++arg_i == arg_end) + return next_token(); + goto resume; + + case '/': { + string pat; + bool found_end_slash = false; + for (++arg_i; arg_i != arg_end; ++arg_i) { + if (*arg_i == '\\') { + if (++arg_i == arg_end) + throw_(parse_error, _("Unexpected '\\' at end of pattern")); + } + else if (*arg_i == '/') { + ++arg_i; + found_end_slash = true; + break; + } + pat.push_back(*arg_i); + } + if (! found_end_slash) + throw_(parse_error, _("Expected '/' at end of pattern")); + if (pat.empty()) + throw_(parse_error, _("Match pattern is empty")); + + return token_t(token_t::TERM, pat); + } + + case '(': ++arg_i; return token_t(token_t::LPAREN); + case ')': ++arg_i; return token_t(token_t::RPAREN); + case '&': ++arg_i; return token_t(token_t::TOK_AND); + case '|': ++arg_i; return token_t(token_t::TOK_OR); + case '!': ++arg_i; return token_t(token_t::TOK_NOT); + case '@': ++arg_i; return token_t(token_t::TOK_PAYEE); + case '#': ++arg_i; return token_t(token_t::TOK_CODE); + case '%': ++arg_i; return token_t(token_t::TOK_META); + case '=': ++arg_i; return token_t(token_t::TOK_EQ); + + case '\\': + consume_next = true; + ++arg_i; + // fall through... + default: { + string ident; + string::const_iterator beg = arg_i; + for (; arg_i != arg_end; ++arg_i) { + switch (*arg_i) { + case ' ': + case '\t': + case '\n': + case '\r': + if (! consume_whitespace) + goto test_ident; + else + ident.push_back(*arg_i); + break; + case '(': + case ')': + case '&': + case '|': + case '!': + case '@': + case '#': + case '%': + case '=': + if (! consume_next) + goto test_ident; + // fall through... + default: + ident.push_back(*arg_i); + break; + } + } + consume_whitespace = false; + +test_ident: + if (ident == "and") + return token_t(token_t::TOK_AND); + else if (ident == "or") + return token_t(token_t::TOK_OR); + else if (ident == "not") + return token_t(token_t::TOK_NOT); + else if (ident == "code") + return token_t(token_t::TOK_CODE); + else if (ident == "desc") + return token_t(token_t::TOK_PAYEE); + else if (ident == "payee") + return token_t(token_t::TOK_PAYEE); + else if (ident == "note") + return token_t(token_t::TOK_NOTE); + else if (ident == "tag") + return token_t(token_t::TOK_META); + else if (ident == "meta") + return token_t(token_t::TOK_META); + else if (ident == "data") + return token_t(token_t::TOK_META); + else if (ident == "show") { + // The "show" keyword is special, and separates a limiting predicate + // from a display predicate. + DEBUG("pred.show", "string = " << (*begin).as_string()); + return token_t(token_t::END_REACHED); + } +#if 0 + // jww (2009-11-06): This is disabled for the time being. + else if (ident == "date") { + // The date keyword takes the whole of the next string as its argument. + consume_whitespace = true; + return token_t(token_t::TOK_DATE); + } +#endif + else if (ident == "expr") { + // The expr keyword takes the whole of the next string as its argument. + consume_next_arg = true; + return token_t(token_t::TOK_EXPR); + } + else + return token_t(token_t::TERM, ident); + break; + } + } + + return token_t(token_t::UNKNOWN); +} + +void query_t::lexer_t::token_t::unexpected() +{ + kind_t prev_kind = kind; + + kind = UNKNOWN; + + switch (prev_kind) { + case END_REACHED: + throw_(parse_error, _("Unexpected end of expression")); + case TERM: + throw_(parse_error, _("Unexpected string '%1'") << *value); + default: + throw_(parse_error, _("Unexpected token '%1'") << symbol()); + } +} + +void query_t::lexer_t::token_t::expected(char wanted, char c) +{ + kind = UNKNOWN; + + if (c == '\0' || c == -1) { + if (wanted == '\0' || wanted == -1) + throw_(parse_error, _("Unexpected end")); + else + throw_(parse_error, _("Missing '%1'") << wanted); + } else { + if (wanted == '\0' || wanted == -1) + throw_(parse_error, _("Invalid char '%1'") << c); + else + throw_(parse_error, _("Invalid char '%1' (wanted '%2')") << c << wanted); + } +} + +expr_t::ptr_op_t +query_t::parser_t::parse_query_term(query_t::lexer_t::token_t::kind_t tok_context) +{ + expr_t::ptr_op_t node; + + lexer_t::token_t tok = lexer.next_token(); + switch (tok.kind) { + case lexer_t::token_t::END_REACHED: + break; + + case lexer_t::token_t::TOK_DATE: + case lexer_t::token_t::TOK_CODE: + case lexer_t::token_t::TOK_PAYEE: + case lexer_t::token_t::TOK_NOTE: + case lexer_t::token_t::TOK_ACCOUNT: + case lexer_t::token_t::TOK_META: + case lexer_t::token_t::TOK_EXPR: + node = parse_query_term(tok.kind); + if (! node) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol()); + break; + + case lexer_t::token_t::TERM: + assert(tok.value); + switch (tok_context) { + case lexer_t::token_t::TOK_DATE: { + expr_t::ptr_op_t ident = new expr_t::op_t(expr_t::op_t::IDENT); + ident->set_ident("date"); + + date_interval_t interval(*tok.value); + + if (interval.start) { + node = new expr_t::op_t(expr_t::op_t::O_GTE); + node->set_left(ident); + + expr_t::ptr_op_t arg1 = new expr_t::op_t(expr_t::op_t::VALUE); + arg1->set_value(*interval.start); + node->set_right(arg1); + } + + if (interval.finish) { + expr_t::ptr_op_t lt = new expr_t::op_t(expr_t::op_t::O_LT); + lt->set_left(ident); + + expr_t::ptr_op_t arg1 = new expr_t::op_t(expr_t::op_t::VALUE); + arg1->set_value(*interval.finish); + lt->set_right(arg1); + + if (node) { + expr_t::ptr_op_t prev(node); + node = new expr_t::op_t(expr_t::op_t::O_AND); + node->set_left(prev); + node->set_right(lt); + } else { + node = lt; + } + } + break; + } + + case lexer_t::token_t::TOK_EXPR: + node = expr_t(*tok.value).get_op(); + break; + + case lexer_t::token_t::TOK_META: { + node = new expr_t::op_t(expr_t::op_t::O_CALL); + + expr_t::ptr_op_t ident = new expr_t::op_t(expr_t::op_t::IDENT); + ident->set_ident("has_tag"); + node->set_left(ident); + + expr_t::ptr_op_t arg1 = new expr_t::op_t(expr_t::op_t::VALUE); + arg1->set_value(mask_t(*tok.value)); + + tok = lexer.peek_token(); + if (tok.kind == lexer_t::token_t::TOK_EQ) { + tok = lexer.next_token(); + tok = lexer.next_token(); + if (tok.kind != lexer_t::token_t::TERM) + throw_(parse_error, + _("Metadata equality operator not followed by term")); + + expr_t::ptr_op_t arg2 = new expr_t::op_t(expr_t::op_t::VALUE); + assert(tok.value); + arg2->set_value(mask_t(*tok.value)); + + node->set_right(expr_t::op_t::new_node + (expr_t::op_t::O_SEQ, + expr_t::op_t::new_node + (expr_t::op_t::O_CONS, arg1, arg2))); + } else { + node->set_right(arg1); + } + break; + } + + default: { + node = new expr_t::op_t(expr_t::op_t::O_MATCH); + + expr_t::ptr_op_t ident = new expr_t::op_t(expr_t::op_t::IDENT); + switch (tok_context) { + case lexer_t::token_t::TOK_ACCOUNT: + ident->set_ident("account"); break; + case lexer_t::token_t::TOK_PAYEE: + ident->set_ident("payee"); break; + case lexer_t::token_t::TOK_CODE: + ident->set_ident("code"); break; + case lexer_t::token_t::TOK_NOTE: + ident->set_ident("note"); break; + default: + assert(false); break; + } + + expr_t::ptr_op_t mask = new expr_t::op_t(expr_t::op_t::VALUE); + DEBUG("query.mask", "Mask from string: " << *tok.value); + mask->set_value(mask_t(*tok.value)); + DEBUG("query.mask", "Mask is: " << mask->as_value().as_mask().str()); + + node->set_left(ident); + node->set_right(mask); + } + } + break; + + case lexer_t::token_t::LPAREN: + node = parse_query_expr(tok_context); + tok = lexer.next_token(); + if (tok.kind != lexer_t::token_t::RPAREN) + tok.expected(')'); + break; + + default: + lexer.push_token(tok); + break; + } + + return node; +} + +expr_t::ptr_op_t +query_t::parser_t::parse_unary_expr(lexer_t::token_t::kind_t tok_context) +{ + expr_t::ptr_op_t node; + + lexer_t::token_t tok = lexer.next_token(); + switch (tok.kind) { + case lexer_t::token_t::TOK_NOT: { + expr_t::ptr_op_t term(parse_query_term(tok_context)); + if (! term) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol()); + + node = new expr_t::op_t(expr_t::op_t::O_NOT); + node->set_left(term); + break; + } + + default: + lexer.push_token(tok); + node = parse_query_term(tok_context); + break; + } + + return node; +} + +expr_t::ptr_op_t +query_t::parser_t::parse_and_expr(lexer_t::token_t::kind_t tok_context) +{ + if (expr_t::ptr_op_t node = parse_unary_expr(tok_context)) { + while (true) { + lexer_t::token_t tok = lexer.next_token(); + if (tok.kind == lexer_t::token_t::TOK_AND) { + expr_t::ptr_op_t prev(node); + node = new expr_t::op_t(expr_t::op_t::O_AND); + node->set_left(prev); + node->set_right(parse_unary_expr(tok_context)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol()); + } else { + lexer.push_token(tok); + break; + } + } + return node; + } + return expr_t::ptr_op_t(); +} + +expr_t::ptr_op_t +query_t::parser_t::parse_or_expr(lexer_t::token_t::kind_t tok_context) +{ + if (expr_t::ptr_op_t node = parse_and_expr(tok_context)) { + while (true) { + lexer_t::token_t tok = lexer.next_token(); + if (tok.kind == lexer_t::token_t::TOK_OR) { + expr_t::ptr_op_t prev(node); + node = new expr_t::op_t(expr_t::op_t::O_OR); + node->set_left(prev); + node->set_right(parse_and_expr(tok_context)); + if (! node->right()) + throw_(parse_error, + _("%1 operator not followed by argument") << tok.symbol()); + } else { + lexer.push_token(tok); + break; + } + } + return node; + } + return expr_t::ptr_op_t(); +} + +expr_t::ptr_op_t +query_t::parser_t::parse_query_expr(lexer_t::token_t::kind_t tok_context) +{ + if (expr_t::ptr_op_t node = parse_or_expr(tok_context)) { + if (expr_t::ptr_op_t next = parse_query_expr(tok_context)) { + expr_t::ptr_op_t prev(node); + node = new expr_t::op_t(expr_t::op_t::O_OR); + node->set_left(prev); + node->set_right(next); + } + return node; + } + return expr_t::ptr_op_t(); +} + +} // namespace ledger diff --git a/src/query.h b/src/query.h new file mode 100644 index 00000000..ebc14020 --- /dev/null +++ b/src/query.h @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2003-2009, 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 predicate.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _QUERY_H +#define _QUERY_H + +#include "predicate.h" + +namespace ledger { + +class query_t : public predicate_t +{ +public: + class lexer_t + { + friend class query_t; + friend class parser_t; + + value_t::sequence_t::const_iterator begin; + value_t::sequence_t::const_iterator end; + + string::const_iterator arg_i; + string::const_iterator arg_end; + + bool consume_whitespace; + bool consume_next_arg; + + public: + struct token_t + { + enum kind_t { + UNKNOWN, + + LPAREN, + RPAREN, + + TOK_NOT, + TOK_AND, + TOK_OR, + TOK_EQ, + + TOK_DATE, + TOK_CODE, + TOK_PAYEE, + TOK_NOTE, + TOK_ACCOUNT, + TOK_META, + TOK_EXPR, + + TERM, + + END_REACHED + + } kind; + + optional<string> value; + + explicit token_t(kind_t _kind = UNKNOWN, + const optional<string>& _value = none) + : kind(_kind), value(_value) { + TRACE_CTOR(query_t::lexer_t::token_t, ""); + } + token_t(const token_t& tok) + : kind(tok.kind), value(tok.value) { + TRACE_CTOR(query_t::lexer_t::token_t, "copy"); + } + ~token_t() throw() { + TRACE_DTOR(query_t::lexer_t::token_t); + } + + token_t& operator=(const token_t& tok) { + if (this != &tok) { + kind = tok.kind; + value = tok.value; + } + return *this; + } + + operator bool() const { + return kind != END_REACHED; + } + + string to_string() const { + switch (kind) { + case UNKNOWN: return "UNKNOWN"; + case LPAREN: return "LPAREN"; + case RPAREN: return "RPAREN"; + case TOK_NOT: return "TOK_NOT"; + case TOK_AND: return "TOK_AND"; + case TOK_OR: return "TOK_OR"; + case TOK_EQ: return "TOK_EQ"; + case TOK_DATE: return "TOK_DATE"; + case TOK_CODE: return "TOK_CODE"; + case TOK_PAYEE: return "TOK_PAYEE"; + case TOK_NOTE: return "TOK_NOTE"; + case TOK_ACCOUNT: return "TOK_ACCOUNT"; + case TOK_META: return "TOK_META"; + case TOK_EXPR: return "TOK_EXPR"; + case TERM: return string("TERM(") + *value + ")"; + case END_REACHED: return "END_REACHED"; + } + assert(false); + return empty_string; + } + + string symbol() const { + switch (kind) { + case LPAREN: return "("; + case RPAREN: return ")"; + case TOK_NOT: return "not"; + case TOK_AND: return "and"; + case TOK_OR: return "or"; + case TOK_EQ: return "="; + case TOK_DATE: return "date"; + case TOK_CODE: return "code"; + case TOK_PAYEE: return "payee"; + case TOK_NOTE: return "note"; + case TOK_ACCOUNT: return "account"; + case TOK_META: return "meta"; + case TOK_EXPR: return "expr"; + + case END_REACHED: return "<EOF>"; + + case TERM: + assert(false); + return "<TERM>"; + + case UNKNOWN: + default: + assert(false); + return "<UNKNOWN>"; + } + } + + void unexpected(); + void expected(char wanted, char c = '\0'); + }; + + token_t token_cache; + + lexer_t(value_t::sequence_t::const_iterator _begin, + value_t::sequence_t::const_iterator _end) + : begin(_begin), end(_end), + consume_whitespace(false), + consume_next_arg(false) + { + TRACE_CTOR(query_t::lexer_t, ""); + assert(begin != end); + arg_i = (*begin).as_string().begin(); + arg_end = (*begin).as_string().end(); + } + lexer_t(const lexer_t& lexer) + : begin(lexer.begin), end(lexer.end), + arg_i(lexer.arg_i), arg_end(lexer.arg_end), + consume_whitespace(lexer.consume_whitespace), + consume_next_arg(lexer.consume_next_arg), + token_cache(lexer.token_cache) + { + TRACE_CTOR(query_t::lexer_t, "copy"); + } + ~lexer_t() throw() { + TRACE_DTOR(query_t::lexer_t); + } + + token_t next_token(); + void push_token(token_t tok) { + assert(token_cache.kind == token_t::UNKNOWN); + token_cache = tok; + } + token_t peek_token() { + if (token_cache.kind == token_t::UNKNOWN) + token_cache = next_token(); + return token_cache; + } + }; + +protected: + class parser_t + { + friend class query_t; + + value_t args; + lexer_t lexer; + + expr_t::ptr_op_t parse_query_term(lexer_t::token_t::kind_t tok_context); + expr_t::ptr_op_t parse_unary_expr(lexer_t::token_t::kind_t tok_context); + expr_t::ptr_op_t parse_and_expr(lexer_t::token_t::kind_t tok_context); + expr_t::ptr_op_t parse_or_expr(lexer_t::token_t::kind_t tok_context); + expr_t::ptr_op_t parse_query_expr(lexer_t::token_t::kind_t tok_context); + + public: + parser_t(const value_t& _args) + : args(_args), lexer(args.begin(), args.end()) { + TRACE_CTOR(query_t::parser_t, ""); + } + parser_t(const parser_t& parser) + : args(parser.args), lexer(parser.lexer) { + TRACE_CTOR(query_t::parser_t, "copy"); + } + ~parser_t() throw() { + TRACE_DTOR(query_t::parser_t); + } + + expr_t::ptr_op_t parse() { + return parse_query_expr(lexer_t::token_t::TOK_ACCOUNT); + } + + bool tokens_remaining() { + lexer_t::token_t tok = lexer.peek_token(); + assert(tok.kind != lexer_t::token_t::UNKNOWN); + return tok.kind != lexer_t::token_t::END_REACHED; + } + }; + + optional<parser_t> parser; + +public: + query_t() { + TRACE_CTOR(query_t, ""); + } + query_t(const query_t& other) + : predicate_t(other) { + TRACE_CTOR(query_t, "copy"); + } + query_t(const string& arg, + const keep_details_t& _what_to_keep = keep_details_t()) + : predicate_t(_what_to_keep) { + TRACE_CTOR(query_t, "string, keep_details_t"); + if (! arg.empty()) { + value_t temp(string_value(arg)); + parse_args(temp.to_sequence()); + } + } + query_t(const value_t& args, + const keep_details_t& _what_to_keep = keep_details_t()) + : predicate_t(_what_to_keep) { + TRACE_CTOR(query_t, "value_t, keep_details_t"); + if (! args.empty()) + parse_args(args); + } + virtual ~query_t() { + TRACE_DTOR(query_t); + } + + void parse_args(const value_t& args) { + if (! parser) + parser = parser_t(args); + ptr = parser->parse(); // expr_t::ptr + } + + void parse_again() { + assert(parser); + ptr = parser->parse(); // expr_t::ptr + } + + bool tokens_remaining() { + return parser && parser->tokens_remaining(); + } + + virtual string text() { + return print_to_str(); + } +}; + +} // namespace ledger + +#endif // _QUERY_H diff --git a/src/quotes.cc b/src/quotes.cc new file mode 100644 index 00000000..f892e93f --- /dev/null +++ b/src/quotes.cc @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2003-2009, 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 "amount.h" +#include "commodity.h" +#include "pool.h" +#include "quotes.h" + +namespace ledger { + +optional<price_point_t> +commodity_quote_from_script(commodity_t& commodity, + const optional<commodity_t&>& exchange_commodity) +{ + DEBUG("commodity.download", "downloading quote for symbol " << commodity.symbol()); +#if defined(DEBUG_ON) + if (exchange_commodity) + DEBUG("commodity.download", + " in terms of commodity " << exchange_commodity->symbol()); +#endif + + char buf[256]; + buf[0] = '\0'; + + string getquote_cmd("getquote \""); + getquote_cmd += commodity.symbol(); + getquote_cmd += "\" \""; + if (exchange_commodity) + getquote_cmd += exchange_commodity->symbol(); + getquote_cmd += "\""; + + DEBUG("commodity.download", "invoking command: " << getquote_cmd); + + bool success = true; + if (FILE * fp = popen(getquote_cmd.c_str(), "r")) { + if (std::feof(fp) || ! std::fgets(buf, 255, fp)) + success = false; + if (pclose(fp) != 0) + success = false; + } else { + success = false; + } + + if (success && buf[0]) { + if (char * p = std::strchr(buf, '\n')) *p = '\0'; + DEBUG("commodity.download", "downloaded quote: " << buf); + + if (optional<std::pair<commodity_t *, price_point_t> > point = + commodity_pool_t::current_pool->parse_price_directive(buf)) { + if (commodity_pool_t::current_pool->price_db) { +#if defined(__GNUG__) && __GNUG__ < 3 + ofstream database(*commodity_pool_t::current_pool->price_db, + ios::out | ios::app); +#else + ofstream database(*commodity_pool_t::current_pool->price_db, + std::ios_base::out | std::ios_base::app); +#endif + database << "P " + << format_datetime(point->second.when, FMT_WRITTEN) + << " " << commodity.symbol() + << " " << point->second.price + << std::endl; + } + return point->second; + } + } else { + DEBUG("commodity.download", + "Failed to download price for '" << commodity.symbol() << + "' (command: \"getquote " << commodity.symbol() << + " " << (exchange_commodity ? + exchange_commodity->symbol() : "''") << "\")"); + + // Don't try to download this commodity again. + commodity.add_flags(COMMODITY_NOMARKET); + } + return none; +} + +} // namespace ledger diff --git a/src/quotes.h b/src/quotes.h new file mode 100644 index 00000000..d00c5bfd --- /dev/null +++ b/src/quotes.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2003-2009, 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 extra + */ + +/** + * @file quotes.h + * @author John Wiegley + * + * @ingroup extra + */ +#ifndef _QUOTES_H +#define _QUOTES_H + +namespace ledger { + +optional<price_point_t> +commodity_quote_from_script(commodity_t& commodity, + const optional<commodity_t&>& exchange_commodity); + +} // namespace ledger + +#endif // _QUOTES_H diff --git a/src/report.cc b/src/report.cc new file mode 100644 index 00000000..2d9d7cc6 --- /dev/null +++ b/src/report.cc @@ -0,0 +1,1307 @@ +/* + * Copyright (c) 2003-2009, 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 "report.h" +#include "session.h" +#include "pool.h" +#include "format.h" +#include "query.h" +#include "output.h" +#include "iterators.h" +#include "filters.h" +#include "precmd.h" +#include "stats.h" +#include "generate.h" +#include "draft.h" +#include "xml.h" +#include "emacs.h" + +namespace ledger { + +void report_t::normalize_options(const string& verb) +{ + // Patch up some of the reporting options based on what kind of + // command it was. + +#ifdef HAVE_ISATTY + if (! HANDLED(force_color)) { + if (! HANDLED(no_color) && isatty(STDOUT_FILENO)) + HANDLER(color).on_only(string("?normalize")); + if (HANDLED(color) && ! isatty(STDOUT_FILENO)) + HANDLER(color).off(); + } + if (! HANDLED(force_pager)) { + if (HANDLED(pager_) && ! isatty(STDOUT_FILENO)) + HANDLER(pager_).off(); + } +#endif + + item_t::use_effective_date = (HANDLED(effective) && + ! HANDLED(actual_dates)); + + 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(); + + if (session.HANDLED(price_db_)) + commodity_pool_t::current_pool->price_db = session.HANDLER(price_db_).str(); + else + commodity_pool_t::current_pool->price_db = none; + + if (HANDLED(date_format_)) + set_date_format(HANDLER(date_format_).str().c_str()); + if (HANDLED(datetime_format_)) + set_datetime_format(HANDLER(datetime_format_).str().c_str()); + if (HANDLED(start_of_week_)) { + if (optional<date_time::weekdays> weekday = + string_to_day_of_week(HANDLER(start_of_week_).str())) + start_of_week = *weekday; + } + + if (verb == "print" || verb == "xact" || verb == "dump") { + HANDLER(related).on_only(string("?normalize")); + HANDLER(related_all).on_only(string("?normalize")); + } + else if (verb == "equity") { + HANDLER(equity).on_only(string("?normalize")); + } + + if (verb == "print") + HANDLER(limit_).on(string("?normalize"), "actual"); + + if (! HANDLED(empty)) + HANDLER(display_).on(string("?normalize"), "amount|(!post&total)"); + + if (verb[0] != 'b' && verb[0] != 'r') + HANDLER(base).on_only(string("?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 + // using -b or -e). Then, if no _duration_ was specified (such as monthly), + // then ignore the period since the begin/end are the only interesting + // details. + if (HANDLED(period_)) { + if (! HANDLED(sort_all_)) + HANDLER(sort_xacts_).on_only(string("?normalize")); + + date_interval_t interval(HANDLER(period_).str()); + + optional<date_t> begin = interval.begin(session.current_year); + optional<date_t> end = interval.end(session.current_year); + + if (! HANDLED(begin_) && begin) { + string predicate = "date>=[" + to_iso_extended_string(*begin) + "]"; + HANDLER(limit_).on(string("?normalize"), predicate); + } + if (! HANDLED(end_) && end) { + string predicate = "date<[" + to_iso_extended_string(*end) + "]"; + HANDLER(limit_).on(string("?normalize"), predicate); + } + + if (! interval.duration) + HANDLER(period_).off(); + } + + // If -j or -J were specified, set the appropriate format string now so as + // 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); + } + else if (HANDLED(total_data)) { + HANDLER(format_) + .on_with(string("?normalize"), HANDLER(plot_total_format_).value); + } + + // If the --exchange (-X) option was used, parse out any final price + // settings that may be there. + if (HANDLED(exchange_) && + HANDLER(exchange_).str().find('=') != string::npos) { + value_t(0L).exchange_commodities(HANDLER(exchange_).str(), true, + terminus); + } + + long cols = 0; + if (HANDLED(columns_)) + cols = HANDLER(columns_).value.to_long(); + else if (const char * columns = std::getenv("COLUMNS")) + cols = lexical_cast<long>(columns); + else + cols = 80L; + + 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() : + amount_width); + + DEBUG("auto.columns", "date_width = " << date_width); + DEBUG("auto.columns", "payee_width = " << payee_width); + DEBUG("auto.columns", "account_width = " << account_width); + 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) { + long total = (4 /* the spaces between */ + date_width + payee_width + + account_width + amount_width + total_width); + if (total > cols) { + DEBUG("auto.columns", "adjusting account down"); + account_width -= total - cols; + DEBUG("auto.columns", "account_width now = " << account_width); + } + } + + 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); + } +} + +void report_t::parse_query_args(const value_t& args, const string& whence) +{ + query_t query(args, what_to_keep()); + if (query) { + HANDLER(limit_).on(whence, query.text()); + + DEBUG("report.predicate", + "Predicate = " << HANDLER(limit_).str()); + } + + if (query.tokens_remaining()) { + query.parse_again(); + if (query) { + HANDLER(display_).on(whence, query.text()); + + DEBUG("report.predicate", + "Display predicate = " << HANDLER(display_).str()); + } + } +} + +void report_t::posts_report(post_handler_ptr handler) +{ + journal_posts_iterator walker(*session.journal.get()); + pass_down_posts(chain_post_handlers(*this, handler), walker); + session.journal->clear_xdata(); +} + +void report_t::generate_report(post_handler_ptr handler) +{ + HANDLER(limit_).on(string("#generate"), "actual"); + + generate_posts_iterator walker + (session, HANDLED(seed_) ? + static_cast<unsigned int>(HANDLER(seed_).value.to_long()) : 0, + HANDLED(head_) ? + static_cast<unsigned int>(HANDLER(head_).value.to_long()) : 50); + + pass_down_posts(chain_post_handlers(*this, handler), walker); +} + +void report_t::xact_report(post_handler_ptr handler, xact_t& xact) +{ + xact_posts_iterator walker(xact); + pass_down_posts(chain_post_handlers(*this, handler), walker); + xact.clear_xdata(); +} + +void report_t::accounts_report(acct_handler_ptr handler) +{ + journal_posts_iterator walker(*session.journal.get()); + + // The lifetime of the chain object controls the lifetime of all temporary + // objects created within it during the call to pass_down_posts, which will + // be needed later by the pass_down_accounts. + post_handler_ptr chain = + chain_post_handlers(*this, post_handler_ptr(new ignore_posts), true); + pass_down_posts(chain, walker); + + HANDLER(amount_).expr.mark_uncompiled(); + HANDLER(total_).expr.mark_uncompiled(); + HANDLER(display_amount_).expr.mark_uncompiled(); + HANDLER(display_total_).expr.mark_uncompiled(); + HANDLER(revalued_total_).expr.mark_uncompiled(); + + scoped_ptr<accounts_iterator> iter; + if (! HANDLED(sort_)) { + iter.reset(new basic_accounts_iterator(*session.journal->master)); + } else { + expr_t sort_expr(HANDLER(sort_).str()); + sort_expr.set_context(this); + iter.reset(new sorted_accounts_iterator(*session.journal->master, + sort_expr, HANDLED(flat))); + } + + if (HANDLED(display_)) + pass_down_accounts(handler, *iter.get(), + predicate_t(HANDLER(display_).str(), what_to_keep()), + *this); + else + pass_down_accounts(handler, *iter.get()); + + session.journal->clear_xdata(); +} + +void report_t::commodities_report(post_handler_ptr handler) +{ + posts_commodities_iterator walker(*session.journal.get()); + pass_down_posts(chain_post_handlers(*this, handler), walker); + session.journal->clear_xdata(); +} + +value_t report_t::fn_amount_expr(call_scope_t& scope) +{ + return HANDLER(amount_).expr.calc(scope); +} + +value_t report_t::fn_total_expr(call_scope_t& scope) +{ + return HANDLER(total_).expr.calc(scope); +} + +value_t report_t::fn_display_amount(call_scope_t& scope) +{ + return HANDLER(display_amount_).expr.calc(scope); +} + +value_t report_t::fn_display_total(call_scope_t& scope) +{ + return HANDLER(display_total_).expr.calc(scope); +} + +value_t report_t::fn_market(call_scope_t& scope) +{ + interactive_t args(scope, "a&ts"); + + value_t result; + optional<datetime_t> moment = (args.has(1) ? + args.get<datetime_t>(1) : + optional<datetime_t>()); + if (args.has(2)) + result = args.value_at(0).exchange_commodities(args.get<string>(2), + /* add_prices= */ false, + moment); + else + result = args.value_at(0).value(true, moment); + + if (! result.is_null()) + return result; + + return args.value_at(0); +} + +value_t report_t::fn_get_at(call_scope_t& scope) +{ + interactive_t args(scope, "Sl"); + + DEBUG("report.get_at", "get_at[0] = " << args.value_at(0)); + DEBUG("report.get_at", "get_at[1] = " << args.value_at(1)); + + if (args.get<long>(1) == 0) { + if (! args.value_at(0).is_sequence()) + return args.value_at(0); + } else { + if (! args.value_at(0).is_sequence()) + throw_(std::runtime_error, + _("Attempting to get argument at index %1 from %2") + << args.get<long>(1) << args.value_at(0).label()); + } + return args.get<const value_t::sequence_t&>(0)[args.get<long>(1)]; +} + +value_t report_t::fn_is_seq(call_scope_t& scope) +{ + return scope.value().is_sequence(); +} + +value_t report_t::fn_strip(call_scope_t& args) +{ + return args.value().strip_annotations(what_to_keep()); +} + +value_t report_t::fn_trim(call_scope_t& args) +{ + string temp(args.value().to_string()); + scoped_array<char> buf(new char[temp.length() + 1]); + std::strcpy(buf.get(), temp.c_str()); + + const char * p = buf.get(); + while (*p && std::isspace(*p)) + p++; + + const char * e = buf.get() + temp.length(); + while (e > p && std::isspace(*e)) + e--; + + if (e == p) { + return string_value(empty_string); + } + else if (e < p) { + assert(false); + return string_value(empty_string); + } + else { + return string_value(string(p, e - p)); + } +} + +value_t report_t::fn_scrub(call_scope_t& args) +{ + value_t temp(args.value().strip_annotations(what_to_keep())); + if (HANDLED(base)) + return temp; + else + return temp.unreduced(); +} + +value_t report_t::fn_rounded(call_scope_t& args) +{ + return args.value().rounded(); +} + +value_t report_t::fn_unrounded(call_scope_t& args) +{ + return args.value().unrounded(); +} + +value_t report_t::fn_quantity(call_scope_t& scope) +{ + interactive_t args(scope, "a"); + return args.get<amount_t>(0).number(); +} + +value_t report_t::fn_floor(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + return args.value_at(0).floored(); +} + +value_t report_t::fn_abs(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + return args.value_at(0).abs(); +} + +value_t report_t::fn_truncated(call_scope_t& scope) +{ + interactive_t args(scope, "v&ll"); + return string_value(format_t::truncate + (args.get<string>(0), + args.has(1) && args.get<int>(1) > 0 ? args.get<int>(1) : 0, + args.has(2) ? args.get<int>(2) : 0)); +} + +value_t report_t::fn_justify(call_scope_t& scope) +{ + interactive_t args(scope, "vl&lbb"); + std::ostringstream out; + args.value_at(0) + .print(out, args.get<int>(1), + args.has(2) ? args.get<int>(2) : -1, + args.has(3) ? args.get<bool>(3) : false, + args.has(4) ? args.get<bool>(4) : false); + return string_value(out.str()); +} + +value_t report_t::fn_quoted(call_scope_t& scope) +{ + interactive_t args(scope, "s"); + std::ostringstream out; + + out << '"'; + foreach (const char ch, args.get<string>(0)) { + if (ch == '"') + out << "\\\""; + else + out << ch; + } + out << '"'; + + return string_value(out.str()); +} + +value_t report_t::fn_join(call_scope_t& scope) +{ + interactive_t args(scope, "s"); + std::ostringstream out; + + foreach (const char ch, args.get<string>(0)) { + if (ch != '\n') + out << ch; + else + out << "\\n"; + } + return string_value(out.str()); +} + +value_t report_t::fn_format_date(call_scope_t& scope) +{ + interactive_t args(scope, "d&s"); + if (args.has(1)) + return string_value(format_date(args.get<date_t>(0), FMT_CUSTOM, + args.get<string>(1).c_str())); + else + return string_value(format_date(args.get<date_t>(0), FMT_PRINTED)); +} + +value_t report_t::fn_ansify_if(call_scope_t& scope) +{ + interactive_t args(scope, "v&s"); + + if (args.has(1)) { + string color = args.get<string>(1); + std::ostringstream buf; + if (color == "black") buf << "\033[30m"; + else if (color == "red") buf << "\033[31m"; + else if (color == "green") buf << "\033[32m"; + else if (color == "yellow") buf << "\033[33m"; + else if (color == "blue") buf << "\033[34m"; + else if (color == "magenta") buf << "\033[35m"; + else if (color == "cyan") buf << "\033[36m"; + else if (color == "white") buf << "\033[37m"; + else if (color == "bold") buf << "\033[1m"; + else if (color == "underline") buf << "\033[4m"; + else if (color == "blink") buf << "\033[5m"; + buf << args.value_at(0); + buf << "\033[0m"; + return string_value(buf.str()); + } else { + return args.value_at(0); + } +} + +value_t report_t::fn_percent(call_scope_t& scope) +{ + interactive_t args(scope, "aa"); + return (amount_t("100.00%") * + (args.get<amount_t>(0) / args.get<amount_t>(1)).number()); +} + +value_t report_t::fn_price(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + return args.value_at(0).price(); +} + +value_t report_t::fn_lot_date(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + if (args.value_at(0).has_annotation()) { + const annotation_t& details(args.value_at(0).annotation()); + if (details.date) + return *details.date; + } + return NULL_VALUE; +} + +value_t report_t::fn_lot_price(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + if (args.value_at(0).has_annotation()) { + const annotation_t& details(args.value_at(0).annotation()); + if (details.price) + return *details.price; + } + return NULL_VALUE; +} + +value_t report_t::fn_lot_tag(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + if (args.value_at(0).has_annotation()) { + const annotation_t& details(args.value_at(0).annotation()); + if (details.tag) + return string_value(*details.tag); + } + return NULL_VALUE; +} + +value_t report_t::fn_to_boolean(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::BOOLEAN); + return args.value_at(0); +} + +value_t report_t::fn_to_int(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::INTEGER); + return args.value_at(0); +} + +value_t report_t::fn_to_datetime(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::DATETIME); + return args.value_at(0); +} + +value_t report_t::fn_to_date(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::DATE); + return args.value_at(0); +} + +value_t report_t::fn_to_amount(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::AMOUNT); + return args.value_at(0); +} + +value_t report_t::fn_to_balance(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::BALANCE); + return args.value_at(0); +} + +value_t report_t::fn_to_string(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::STRING); + return args.value_at(0); +} + +value_t report_t::fn_to_mask(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::MASK); + return args.value_at(0); +} + +value_t report_t::fn_to_sequence(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::SEQUENCE); + return args.value_at(0); +} + +namespace { + value_t fn_black(call_scope_t&) { + return string_value("black"); + } + value_t fn_blink(call_scope_t&) { + return string_value("blink"); + } + value_t fn_blue(call_scope_t&) { + return string_value("blue"); + } + value_t fn_bold(call_scope_t&) { + return string_value("bold"); + } + value_t fn_cyan(call_scope_t&) { + return string_value("cyan"); + } + value_t fn_green(call_scope_t&) { + return string_value("green"); + } + value_t fn_magenta(call_scope_t&) { + return string_value("magenta"); + } + value_t fn_red(call_scope_t&) { + return string_value("red"); + } + value_t fn_underline(call_scope_t&) { + return string_value("underline"); + } + value_t fn_white(call_scope_t&) { + return string_value("white"); + } + value_t fn_yellow(call_scope_t&) { + return string_value("yellow"); + } + value_t fn_false(call_scope_t&) { + return false; + } + value_t fn_null(call_scope_t&) { + return NULL_VALUE; + } +} + +value_t report_t::reload_command(call_scope_t&) +{ + session.close_journal_files(); + session.read_journal_files(); + return true; +} + +value_t report_t::echo_command(call_scope_t& scope) +{ + interactive_t args(scope, "s"); + std::ostream& out(output_stream); + out << args.get<string>(0) << std::endl; + return true; +} + +option_t<report_t> * report_t::lookup_option(const char * p) +{ + switch (*p) { + case '%': + OPT_CH(percent); + break; + case 'A': + OPT_CH(average); + break; + case 'B': + OPT_CH(basis); + break; + case 'C': + OPT_CH(cleared); + break; + case 'D': + OPT_CH(daily); + break; + case 'E': + OPT_CH(empty); + break; + case 'F': + OPT_CH(format_); + break; + case 'G': + OPT_CH(gain); + break; + case 'I': + OPT_CH(price); + break; + case 'J': + OPT_CH(total_data); + break; + case 'L': + OPT_CH(actual); + break; + case 'M': + OPT_CH(monthly); + break; + case 'O': + OPT_CH(quantity); + break; + case 'P': + OPT_CH(by_payee); + break; + case 'R': + OPT_CH(real); + break; + case 'S': + OPT_CH(sort_); + break; + case 'T': + OPT_CH(total_); + break; + case 'U': + OPT_CH(uncleared); + break; + case 'V': + OPT_CH(market); + break; + case 'W': + OPT_CH(weekly); + break; + case 'X': + OPT_CH(exchange_); + break; + case 'Y': + OPT_CH(yearly); + break; + case 'a': + 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(anon); + else OPT_ALT(color, ansi); + else OPT(average); + else OPT(account_width_); + else OPT(amount_width_); + break; + case 'b': + OPT(balance_format_); + else OPT(base); + else OPT_ALT(basis, cost); + else OPT_(begin_); + else OPT(budget); + else OPT(by_payee); + break; + case 'c': + OPT(csv_format_); + else OPT(cleared); + else OPT(collapse); + else OPT(collapse_if_zero); + else OPT(color); + else OPT(columns_); + else OPT_ALT(basis, cost); + else OPT_(current); + break; + case 'd': + OPT(daily); + else OPT(date_); + else OPT(date_format_); + else OPT(datetime_format_); + 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(date_width_); + break; + case 'e': + OPT(effective); + else OPT(empty); + else OPT_(end_); + else OPT(equity); + else OPT(exact); + else OPT(exchange_); + break; + case 'f': + OPT(flat); + else OPT_ALT(forecast_while_, forecast_); + else OPT(forecast_years_); + else OPT(format_); + else OPT(force_color); + else OPT(force_pager); + else OPT_ALT(head_, first_); + break; + case 'g': + OPT(gain); + break; + case 'h': + OPT(head_); + break; + case 'i': + OPT(invert); + break; + case 'j': + OPT_CH(amount_data); + break; + case 'l': + OPT_(limit_); + else OPT(lot_dates); + else OPT(lot_prices); + else OPT(lot_tags); + else OPT(lots); + else OPT(lots_actual); + else OPT_ALT(tail_, last_); + break; + case 'm': + OPT(market); + else OPT(monthly); + break; + case 'n': + OPT_CH(collapse); + else OPT(no_color); + else OPT(no_total); + else OPT(now_); + break; + case 'o': + OPT(only_); + else OPT_(output_); + break; + case 'p': + OPT(pager_); + else OPT(payee_); + else OPT(pending); + else OPT(percent); + else OPT_(period_); + else OPT_ALT(sort_xacts_, period_sort_); + else OPT(plot_amount_format_); + else OPT(plot_total_format_); + else OPT(price); + else OPT(prices_format_); + else OPT(pricedb_format_); + else OPT(print_format_); + else OPT(payee_width_); + else OPT(prepend_format_); + break; + case 'q': + OPT(quantity); + else OPT(quarterly); + break; + case 'r': + OPT(raw); + else OPT(real); + else OPT(register_format_); + else OPT_(related); + else OPT(related_all); + else OPT(revalued); + else OPT(revalued_only); + else OPT(revalued_total_); + break; + case 's': + OPT(sort_); + else OPT(sort_all_); + else OPT(sort_xacts_); + else OPT_(subtotal); + else OPT(start_of_week_); + else OPT(seed_); + break; + case 't': + OPT_CH(amount_); + else OPT(tail_); + else OPT(total_); + else OPT(total_data); + else OPT(truncate_); + else OPT(total_width_); + break; + case 'u': + OPT(unbudgeted); + else OPT(uncleared); + else OPT(unrealized); + else OPT(unrealized_gains_); + else OPT(unrealized_losses_); + else OPT(unround); + else OPT(unsorted); + break; + case 'w': + OPT(weekly); + else OPT_(wide); + break; + case 'y': + OPT_CH(date_format_); + else OPT(yearly); + break; + } + return NULL; +} + +void report_t::define(const symbol_t::kind_t kind, const string& name, + expr_t::ptr_op_t def) +{ + session.define(kind, name, def); +} + +expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (expr_t::ptr_op_t def = session.lookup(kind, name)) + return def; + + const char * p = name.c_str(); + + switch (kind) { + case symbol_t::FUNCTION: + // Support 2.x's single-letter value expression names. + if (*(p + 1) == '\0') { + switch (*p) { + case 'd': + case 'm': + return MAKE_FUNCTOR(report_t::fn_now); + case 'P': + return MAKE_FUNCTOR(report_t::fn_market); + case 't': + return MAKE_FUNCTOR(report_t::fn_display_amount); + case 'T': + return MAKE_FUNCTOR(report_t::fn_display_total); + case 'U': + return MAKE_FUNCTOR(report_t::fn_abs); + case 'S': + return MAKE_FUNCTOR(report_t::fn_strip); + case 'i': + throw_(std::runtime_error, + _("The i value expression variable is no longer supported")); + case 'A': + throw_(std::runtime_error, + _("The A value expression variable is no longer supported")); + case 'v': + case 'V': + throw_(std::runtime_error, + _("The V and v value expression variables are no longer supported")); + case 'I': + case 'B': + throw_(std::runtime_error, + _("The I and B value expression variables are no longer supported")); + case 'g': + case 'G': + throw_(std::runtime_error, + _("The G and g value expression variables are no longer supported")); + default: + return NULL; + } + } + + switch (*p) { + case 'a': + if (is_eq(p, "amount_expr")) + return MAKE_FUNCTOR(report_t::fn_amount_expr); + else if (is_eq(p, "ansify_if")) + return MAKE_FUNCTOR(report_t::fn_ansify_if); + else if (is_eq(p, "abs")) + return MAKE_FUNCTOR(report_t::fn_abs); + break; + + case 'b': + if (is_eq(p, "black")) + return WRAP_FUNCTOR(fn_black); + else if (is_eq(p, "blink")) + return WRAP_FUNCTOR(fn_blink); + else if (is_eq(p, "blue")) + return WRAP_FUNCTOR(fn_blue); + else if (is_eq(p, "bold")) + return WRAP_FUNCTOR(fn_bold); + break; + + case 'c': + if (is_eq(p, "cyan")) + return WRAP_FUNCTOR(fn_cyan); + break; + + case 'd': + if (is_eq(p, "display_amount")) + return MAKE_FUNCTOR(report_t::fn_display_amount); + 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); + break; + + case 'f': + if (is_eq(p, "format_date")) + return MAKE_FUNCTOR(report_t::fn_format_date); + else if (is_eq(p, "floor")) + return MAKE_FUNCTOR(report_t::fn_floor); + break; + + case 'g': + if (is_eq(p, "get_at")) + return MAKE_FUNCTOR(report_t::fn_get_at); + else if (is_eq(p, "green")) + return WRAP_FUNCTOR(fn_green); + break; + + case 'i': + if (is_eq(p, "is_seq")) + return MAKE_FUNCTOR(report_t::fn_is_seq); + break; + + case 'j': + if (is_eq(p, "justify")) + return MAKE_FUNCTOR(report_t::fn_justify); + else if (is_eq(p, "join")) + return MAKE_FUNCTOR(report_t::fn_join); + break; + + case 'm': + if (is_eq(p, "market")) + return MAKE_FUNCTOR(report_t::fn_market); + else if (is_eq(p, "magenta")) + return WRAP_FUNCTOR(fn_magenta); + break; + + case 'n': + if (is_eq(p, "null")) + return WRAP_FUNCTOR(fn_null); + else if (is_eq(p, "now")) + return MAKE_FUNCTOR(report_t::fn_now); + break; + + case 'o': + if (is_eq(p, "options")) + return MAKE_FUNCTOR(report_t::fn_options); + break; + + case 'p': + if (is_eq(p, "post")) + 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); + break; + + case 'q': + if (is_eq(p, "quoted")) + return MAKE_FUNCTOR(report_t::fn_quoted); + else if (is_eq(p, "quantity")) + return MAKE_FUNCTOR(report_t::fn_quantity); + break; + + case 'r': + if (is_eq(p, "rounded")) + return MAKE_FUNCTOR(report_t::fn_rounded); + else if (is_eq(p, "red")) + return WRAP_FUNCTOR(fn_red); + break; + + case 's': + if (is_eq(p, "scrub")) + return MAKE_FUNCTOR(report_t::fn_scrub); + else if (is_eq(p, "strip")) + return MAKE_FUNCTOR(report_t::fn_strip); + break; + + case 't': + if (is_eq(p, "truncated")) + return MAKE_FUNCTOR(report_t::fn_truncated); + else if (is_eq(p, "total_expr")) + return MAKE_FUNCTOR(report_t::fn_total_expr); + else if (is_eq(p, "today")) + return MAKE_FUNCTOR(report_t::fn_today); + else if (is_eq(p, "t")) + return MAKE_FUNCTOR(report_t::fn_display_amount); + else if (is_eq(p, "trim")) + return MAKE_FUNCTOR(report_t::fn_trim); + else if (is_eq(p, "to_boolean")) + return MAKE_FUNCTOR(report_t::fn_to_boolean); + else if (is_eq(p, "to_int")) + return MAKE_FUNCTOR(report_t::fn_to_int); + else if (is_eq(p, "to_datetime")) + return MAKE_FUNCTOR(report_t::fn_to_datetime); + else if (is_eq(p, "to_date")) + return MAKE_FUNCTOR(report_t::fn_to_date); + else if (is_eq(p, "to_amount")) + return MAKE_FUNCTOR(report_t::fn_to_amount); + else if (is_eq(p, "to_balance")) + return MAKE_FUNCTOR(report_t::fn_to_balance); + else if (is_eq(p, "to_string")) + return MAKE_FUNCTOR(report_t::fn_to_string); + else if (is_eq(p, "to_mask")) + return MAKE_FUNCTOR(report_t::fn_to_mask); + else if (is_eq(p, "to_sequence")) + return MAKE_FUNCTOR(report_t::fn_to_sequence); + break; + + case 'T': + if (is_eq(p, "T")) + return MAKE_FUNCTOR(report_t::fn_display_total); + break; + + case 'u': + if (is_eq(p, "underline")) + return WRAP_FUNCTOR(fn_underline); + else if (is_eq(p, "unrounded")) + return MAKE_FUNCTOR(report_t::fn_unrounded); + break; + + case 'w': + if (is_eq(p, "white")) + return WRAP_FUNCTOR(fn_white); + break; + + case 'y': + if (is_eq(p, "yellow")) + return WRAP_FUNCTOR(fn_yellow); + break; + } + + // Check if they are trying to access an option's setting or value. + if (option_t<report_t> * handler = lookup_option(p)) + return MAKE_OPT_FUNCTOR(report_t, handler); + break; + + case symbol_t::OPTION: + if (option_t<report_t> * handler = lookup_option(p)) + return MAKE_OPT_HANDLER(report_t, handler); + break; + + case symbol_t::COMMAND: + switch (*p) { + 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_))), + *this, "#balance")); + } + else if (is_eq(p, "budget")) { + HANDLER(amount_).set_expr(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_))), + *this, "#budget")); + } + 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_))), + *this, "#csv")); + } + 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_))), + *this, "#cleared")); + } + break; + + case 'e': + if (is_eq(p, "equity")) + return WRAP_FUNCTOR + (reporter<> + (new format_posts(*this, report_format(HANDLER(print_format_))), + *this, "#equity")); + 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")); + else if (is_eq(p, "echo")) + return MAKE_FUNCTOR(report_t::echo_command); + break; + + case 'p': + if (*(p + 1) == '\0' || is_eq(p, "print")) + return WRAP_FUNCTOR + (reporter<> + (new format_posts(*this, report_format(HANDLER(print_format_)), + HANDLED(raw)), *this, "#print")); + 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_))), + *this, "#prices")); + 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_))), + *this, "#pricedb")); + 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_)), + false, maybe_format(HANDLER(prepend_format_))), + *this, "#register")); + else if (is_eq(p, "reload")) + return MAKE_FUNCTOR(report_t::reload_command); + break; + + case 's': + if (is_eq(p, "stats") || is_eq(p, "stat")) + return WRAP_FUNCTOR(report_statistics); + 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")); + break; + } + break; + + case symbol_t::PRECOMMAND: + switch (*p) { + case 'a': + if (is_eq(p, "args")) + return WRAP_FUNCTOR(args_command); + break; + case 'e': + if (is_eq(p, "eval")) + return WRAP_FUNCTOR(eval_command); + break; + case 'f': + if (is_eq(p, "format")) + 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 format_posts(*this, report_format(HANDLER(print_format_)), + false), *this, "#generate")); + case 'p': + if (is_eq(p, "parse")) + return WRAP_FUNCTOR(parse_command); + else if (is_eq(p, "period")) + return WRAP_FUNCTOR(period_command); + break; + case 't': + if (is_eq(p, "template")) + return WRAP_FUNCTOR(template_command); + break; + } + break; + + default: + break; + } + + return NULL; +} + +} // namespace ledger diff --git a/src/report.h b/src/report.h new file mode 100644 index 00000000..94d39215 --- /dev/null +++ b/src/report.h @@ -0,0 +1,958 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file report.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _REPORT_H +#define _REPORT_H + +#include "interactive.h" +#include "expr.h" +#include "query.h" +#include "chain.h" +#include "stream.h" +#include "option.h" +#include "commodity.h" +#include "annotate.h" +#include "session.h" +#include "format.h" + +namespace ledger { + +class session_t; +class xact_t; + +// These are the elements of any report: +// +// 1. Formatting string used for outputting the underlying ReportedType. +// +// 2. Handler object for the ReportedType. This is constructed using #1, or +// else #1 is ignored completely. This handler object is also constructed +// with the output stream that will be used during formatting. +// +// --- The details of #1 and #2 together represent the ItemHandler. +// +// 3. Mode of the report. Currently there are four modes: +// +// a. Posting or commodity iteration. In this mode, all the journal's +// xacts, the postings of a specific xact, or all the journal's +// commodities are walked. In the first two cases, it's the underlying +// postings which are passed to #2; in the second case, each +// commodity is passed to #2. +// +// b. Account iteration. This employs step 'a', but add a prologue and +// epilogue to it. In the prologue it "sums" all account totals and +// subtotals; in the epilogue it calls yet another handler whose job is +// reporting (the handler used in 'a' is only for calculation). +// +// There is one variation on 'b' in which a "totals" line is also +// displayed. +// +// c. Write journal. In this mode, a single function is called that output +// the journal object as a textual file. #2 is used to print out each +// posting in the journal. +// +// d. Dump binary file. This is just like 'c', except that it dumps out a +// binary file and #2 is completely ignored. +// +// 4. For 'a' and 'b' in #3, there is a different iteration function called, +// depending on whether we're iterating: +// +// a. The postings of an xact: walk_postings. +// b. The xacts of a journal: walk_xacts. +// c. The commodities of a journal: walk_commodities. +// +// 5. Finally, for the 'a' and 'b' reporting modes, there is a variant which +// says that the formatter should be "flushed" after the entities are +// iterated. This does not happen for the commodities iteration, however. + +class report_t : public scope_t +{ + report_t(); + +public: + session_t& session; + output_stream_t output_stream; + +#define BUDGET_NO_BUDGET 0x00 +#define BUDGET_BUDGETED 0x01 +#define BUDGET_UNBUDGETED 0x02 +#define BUDGET_WRAP_VALUES 0x04 + + datetime_t terminus; + uint_least8_t budget_flags; + + explicit report_t(session_t& _session) + : session(_session), terminus(CURRENT_TIME()), + budget_flags(BUDGET_NO_BUDGET) {} + + virtual ~report_t() { + output_stream.close(); + } + + void normalize_options(const string& verb); + void parse_query_args(const value_t& args, const string& whence); + + void posts_report(post_handler_ptr handler); + void generate_report(post_handler_ptr handler); + void xact_report(post_handler_ptr handler, xact_t& xact); + void accounts_report(acct_handler_ptr handler); + void commodities_report(post_handler_ptr handler); + + value_t fn_amount_expr(call_scope_t& scope); + value_t fn_total_expr(call_scope_t& scope); + value_t fn_display_amount(call_scope_t& scope); + value_t fn_display_total(call_scope_t& scope); + value_t fn_market(call_scope_t& scope); + value_t fn_get_at(call_scope_t& scope); + value_t fn_is_seq(call_scope_t& scope); + value_t fn_strip(call_scope_t& scope); + value_t fn_trim(call_scope_t& scope); + value_t fn_scrub(call_scope_t& scope); + value_t fn_quantity(call_scope_t& scope); + value_t fn_rounded(call_scope_t& scope); + value_t fn_unrounded(call_scope_t& scope); + value_t fn_truncated(call_scope_t& scope); + value_t fn_floor(call_scope_t& scope); + value_t fn_abs(call_scope_t& scope); + value_t fn_justify(call_scope_t& scope); + 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_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_lot_date(call_scope_t& scope); + value_t fn_lot_price(call_scope_t& scope); + value_t fn_lot_tag(call_scope_t& scope); + value_t fn_to_boolean(call_scope_t& scope); + value_t fn_to_int(call_scope_t& scope); + value_t fn_to_datetime(call_scope_t& scope); + value_t fn_to_date(call_scope_t& scope); + value_t fn_to_amount(call_scope_t& scope); + value_t fn_to_balance(call_scope_t& scope); + value_t fn_to_string(call_scope_t& scope); + value_t fn_to_mask(call_scope_t& scope); + value_t fn_to_sequence(call_scope_t& scope); + + value_t fn_now(call_scope_t&) { + return terminus; + } + value_t fn_today(call_scope_t&) { + return terminus.date(); + } + + value_t fn_options(call_scope_t&) { + return value_t(static_cast<scope_t *>(this)); + } + + string report_format(option_t<report_t>& option) { + if (HANDLED(format_)) + return HANDLER(format_).str(); + return option.str(); + } + + optional<string> maybe_format(option_t<report_t>& option) { + if (option) + return option.str(); + return none; + } + + value_t reload_command(call_scope_t&); + value_t echo_command(call_scope_t& scope); + + keep_details_t what_to_keep() { + bool lots = HANDLED(lots) || HANDLED(lots_actual); + return keep_details_t(lots || HANDLED(lot_prices), + lots || HANDLED(lot_dates), + lots || HANDLED(lot_tags), + HANDLED(lots_actual)); + } + + void report_options(std::ostream& out) + { + 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(average).report(out); + HANDLER(balance_format_).report(out); + HANDLER(base).report(out); + HANDLER(basis).report(out); + HANDLER(begin_).report(out); + HANDLER(budget).report(out); + HANDLER(budget_format_).report(out); + HANDLER(by_payee).report(out); + HANDLER(cleared).report(out); + HANDLER(cleared_format_).report(out); + HANDLER(color).report(out); + HANDLER(collapse).report(out); + HANDLER(collapse_if_zero).report(out); + HANDLER(columns_).report(out); + HANDLER(csv_format_).report(out); + HANDLER(current).report(out); + HANDLER(daily).report(out); + HANDLER(date_).report(out); + HANDLER(date_format_).report(out); + HANDLER(datetime_format_).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); + HANDLER(exact).report(out); + HANDLER(exchange_).report(out); + HANDLER(flat).report(out); + HANDLER(force_color).report(out); + HANDLER(force_pager).report(out); + HANDLER(forecast_while_).report(out); + HANDLER(forecast_years_).report(out); + HANDLER(format_).report(out); + HANDLER(gain).report(out); + HANDLER(head_).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(lots).report(out); + HANDLER(lots_actual).report(out); + HANDLER(market).report(out); + HANDLER(monthly).report(out); + HANDLER(no_total).report(out); + HANDLER(now_).report(out); + HANDLER(only_).report(out); + HANDLER(output_).report(out); + HANDLER(pager_).report(out); + HANDLER(payee_).report(out); + HANDLER(pending).report(out); + HANDLER(percent).report(out); + HANDLER(period_).report(out); + HANDLER(plot_amount_format_).report(out); + HANDLER(plot_total_format_).report(out); + HANDLER(prepend_format_).report(out); + HANDLER(price).report(out); + HANDLER(prices_format_).report(out); + HANDLER(pricedb_format_).report(out); + HANDLER(print_format_).report(out); + HANDLER(quantity).report(out); + HANDLER(quarterly).report(out); + HANDLER(raw).report(out); + HANDLER(real).report(out); + HANDLER(register_format_).report(out); + HANDLER(related).report(out); + HANDLER(related_all).report(out); + HANDLER(revalued).report(out); + HANDLER(revalued_only).report(out); + HANDLER(revalued_total_).report(out); + HANDLER(seed_).report(out); + HANDLER(sort_).report(out); + HANDLER(sort_all_).report(out); + HANDLER(sort_xacts_).report(out); + HANDLER(start_of_week_).report(out); + HANDLER(subtotal).report(out); + HANDLER(tail_).report(out); + HANDLER(total_).report(out); + HANDLER(total_data).report(out); + HANDLER(truncate_).report(out); + HANDLER(unbudgeted).report(out); + HANDLER(uncleared).report(out); + HANDLER(unrealized).report(out); + HANDLER(unrealized_gains_).report(out); + HANDLER(unrealized_losses_).report(out); + HANDLER(unround).report(out); + HANDLER(unsorted).report(out); + HANDLER(weekly).report(out); + HANDLER(wide).report(out); + HANDLER(yearly).report(out); + HANDLER(date_width_).report(out); + HANDLER(payee_width_).report(out); + HANDLER(account_width_).report(out); + HANDLER(amount_width_).report(out); + HANDLER(total_width_).report(out); + } + + option_t<report_t> * lookup_option(const char * p); + + virtual void define(const symbol_t::kind_t kind, const string& name, + expr_t::ptr_op_t def); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + /** + * Option handlers + */ + + OPTION__(report_t, abbrev_len_, + CTOR(report_t, abbrev_len_) { on_with(none, 2L); }); + OPTION(report_t, account_); + + OPTION_(report_t, actual, DO() { // -L + parent->HANDLER(limit_).on(string("--actual"), "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[0].to_string(), args[1].to_string()); + }); + + OPTION(report_t, amount_data); // -j + OPTION(report_t, anon); + + OPTION_(report_t, average, DO() { // -A + parent->HANDLER(display_total_) + .set_expr(string("--average"), "total_expr/count"); + }); + + OPTION__(report_t, balance_format_, CTOR(report_t, balance_format_) { + on(none, + "%(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"); + }); + + 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)"); + }); + + OPTION_(report_t, begin_, DO_(args) { // -b + date_interval_t interval(args[1].to_string()); + optional<date_t> begin = interval.begin(parent->session.current_year); + if (! begin) + throw_(std::invalid_argument, + _("Could not determine beginning of period '%1'") + << args[1].to_string()); + + string predicate = "date>=[" + to_iso_extended_string(*begin) + "]"; + parent->HANDLER(limit_).on(string("--begin"), predicate); + }); + + 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) ? " + " scrub((100% * get_at(total_expr, 0)) / " + " - get_at(total_expr, 1)) : 0), " + " 5, -1, true, false)," + " magenta if (color and get_at(total_expr, 1) and " + " (abs(quantity(get_at(total_expr, 0)) / " + " quantity(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%/" + "------------ ------------ ------------ -----\n"); + }); + + OPTION(report_t, by_payee); // -P + + OPTION_(report_t, cleared, DO() { // -C + parent->HANDLER(limit_).on(string("--cleared"), "cleared"); + }); + + OPTION__(report_t, cleared_format_, CTOR(report_t, cleared_format_) { + on(none, + "%(justify(scrub(get_at(total_expr, 0)), 16, -1, true, color))" + " %(justify(scrub(get_at(total_expr, 1)), 16, -1, 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%/" + "---------------- ---------------- ---------\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"); + }); + + OPTION_(report_t, collapse_if_zero, DO() { + parent->HANDLER(collapse).on_only(string("--collapse-if-zero")); + }); + + OPTION(report_t, columns_); + + OPTION__(report_t, csv_format_, CTOR(report_t, csv_format_) { + on(none, + "%(quoted(date))," + "%(quoted(payee))," + "%(quoted(account))," + "%(quoted(scrub(display_amount)))," + "%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\")))," + "%(quoted(code))," + "%(quoted(join(note | xact.note)))\n"); + }); + + OPTION_(report_t, current, DO() { // -c + parent->HANDLER(limit_).on(string("--current"), "date<=today"); + }); + + OPTION_(report_t, daily, DO() { // -D + parent->HANDLER(period_).on(string("--daily"), "daily"); + }); + + OPTION(report_t, date_); + OPTION(report_t, date_format_); + OPTION(report_t, datetime_format_); + + OPTION_(report_t, depth_, DO_(scope) { + interactive_t args(scope, "sl"); + parent->HANDLER(display_).on(string("--depth"), + string("depth<=") + args.get<string>(1)); + }); + + OPTION_(report_t, deviation, DO() { + parent->HANDLER(display_total_) + .set_expr(string("--deviation"), "amount_expr-total_expr/count"); + }); + + 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_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[0].to_string(), args[1].to_string()); + }); + + 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[0].to_string(), args[1].to_string()); + }); + + OPTION(report_t, dow); + OPTION(report_t, effective); + OPTION(report_t, empty); // -E + + OPTION_(report_t, end_, DO_(args) { // -e + date_interval_t interval(args[1].to_string()); + // 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(parent->session.current_year); + if (! end) + throw_(std::invalid_argument, + _("Could not determine end of period '%1'") + << args[1].to_string()); + + string predicate = "date<[" + to_iso_extended_string(*end) + "]"; + parent->HANDLER(limit_).on(string("--end"), predicate); + + parent->terminus = datetime_t(*end); + }); + + OPTION(report_t, equity); + OPTION(report_t, exact); + + OPTION_(report_t, exchange_, DO_(args) { // -X + on_with(args[0].as_string(), 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, flat); + OPTION(report_t, force_color); + OPTION(report_t, force_pager); + OPTION(report_t, forecast_while_); + OPTION(report_t, forecast_years_); + 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)"); + // 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), date, exchange)" + " - get_at(amount_expr, 1))"); + parent->HANDLER(revalued_total_) + .set_expr(string("--gain"), + "(market(get_at(total_expr, 0), 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), date, exchange)" + " - get_at(total_expr, 1)"); + }); + + OPTION(report_t, head_); + + OPTION_(report_t, invert, DO() { + parent->HANDLER(amount_).set_expr(string("--invert"), "-amount"); + }); + + 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, lot_dates); + OPTION(report_t, lot_prices); + OPTION(report_t, lot_tags); + 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, date, exchange)"); + parent->HANDLER(display_total_) + .set_expr(string("--market"), "market(total_expr, date, exchange)"); + }); + + OPTION_(report_t, monthly, DO() { // -M + parent->HANDLER(period_).on(string("--monthly"), "monthly"); + }); + + OPTION_(report_t, no_color, DO() { + parent->HANDLER(color).off(); + }); + + OPTION(report_t, no_total); + + OPTION_(report_t, now_, DO_(args) { + date_interval_t interval(args[1].to_string()); + optional<date_t> begin = interval.begin(parent->session.current_year); + if (! begin) + throw_(std::invalid_argument, + _("Could not determine beginning of period '%1'") + << args[1].to_string()); + ledger::epoch = parent->terminus = datetime_t(*begin); + parent->session.current_year = ledger::epoch->date().year(); + }); + + 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() + ")")); + }); + + OPTION(report_t, output_); // -o + +#ifdef HAVE_ISATTY + OPTION__ + (report_t, pager_, + CTOR(report_t, pager_) { + if (! std::getenv("PAGER") && isatty(STDOUT_FILENO)) { + bool have_less = false; + if (exists(path("/opt/local/bin/less")) || + exists(path("/usr/local/bin/less")) || + exists(path("/usr/bin/less"))) + have_less = true; + + if (have_less) { + on(none, "less"); + 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); + }); +#endif // HAVE_ISATTY + + OPTION(report_t, payee_); + + OPTION_(report_t, pending, DO() { // -C + parent->HANDLER(limit_).on(string("--pending"), "pending"); + }); + + OPTION_(report_t, percent, DO() { // -% + parent->HANDLER(total_) + .set_expr(string("--percent"), + "is_account&parent&parent.total&percent(total, parent.total)"); + }); + + 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, 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, prepend_format_); + + 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)"); + }); + + OPTION__(report_t, prices_format_, CTOR(report_t, prices_format_) { + on(none, + "%(date) %-8(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) %(account) %(scrub(display_amount))\n"); + }); + + OPTION__(report_t, print_format_, CTOR(report_t, print_format_) { + on(none, + "%(xact.date)" + "%(!effective & xact.effective_date ?" + " \"=\" + xact.effective_date : \"\")" + "%(xact.cleared ? \" *\" : (xact.pending ? \" !\" : \"\"))" + "%(code ? \" (\" + code + \")\" :" + " \"\") %(payee)%(xact.comment)\n" + " %(xact.uncleared ?" + " (cleared ? \"* \" : (pending ? \"! \" : \"\")) : \"\")" + "%(calculated ? account : justify(account, 34, -1, false))" + "%(calculated ? \"\" : \" \" + justify(scrub(amount), 12, -1, true))" + "%(has_cost & !cost_calculated ?" + " \" @ \" + justify(scrub(abs(cost / amount)), 0) : \"\")" + "%(comment)\n%/" + " %$7%$8%$9%$A%$B\n%/\n"); + }); + + 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"); + }); + + OPTION_(report_t, quarterly, DO() { + parent->HANDLER(period_).on(string("--quarterly"), "quarterly"); + }); + + OPTION(report_t, raw); + + OPTION_(report_t, real, DO() { // -R + parent->HANDLER(limit_).on(string("--real"), "real"); + }); + + OPTION__(report_t, register_format_, CTOR(report_t, register_format_) { + on(none, + "%(ansify_if(justify(format_date(date), date_width), green " + " if color & date > today))" + " %(ansify_if(justify(truncated(payee, payee_width), payee_width), " + " bold if color & !cleared & actual))" + " %(ansify_if(justify(truncated(account, account_width, abbrev_len), " + " account_width), blue if color))" + " %(justify(scrub(display_amount), amount_width, " + " 3 + date_width + payee_width + account_width + amount_width, " + " true, color))" + " %(justify(scrub(display_total), total_width, " + " 4 + date_width + payee_width + account_width + amount_width " + " + total_width, true, color))\n%/" + "%(justify(\" \", 2 + date_width + payee_width))%$3 %$4 %$5\n"); + }); + + OPTION(report_t, related); // -r + + OPTION_(report_t, related_all, DO() { + parent->HANDLER(related).on_only(string("--related-all")); + }); + + OPTION(report_t, revalued); + OPTION(report_t, revalued_only); + + OPTION__ + (report_t, revalued_total_, + expr_t expr; + CTOR(report_t, revalued_total_) {} + void set_expr(const optional<string>& whence, const string& str) { + expr = str; + on(whence, str); + } + DO_(args) { + set_expr(args[0].to_string(), args[1].to_string()); + }); + + OPTION(report_t, seed_); + + OPTION_(report_t, sort_, DO_(args) { // -S + on_with(args[0].as_string(), args[1]); + parent->HANDLER(sort_xacts_).off(); + parent->HANDLER(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_xacts_, DO_(args) { + parent->HANDLER(sort_).on_with(string("--sort-xacts"), args[1]); + parent->HANDLER(sort_all_).off(); + }); + + OPTION(report_t, start_of_week_); + OPTION(report_t, subtotal); // -s + OPTION(report_t, tail_); + + 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[0].to_string(), args[1].to_string()); + }); + + OPTION(report_t, total_data); // -J + + OPTION_(report_t, truncate_, DO_(args) { + string style(args[1].to_string()); + if (style == "leading") + format_t::default_style = format_t::TRUNCATE_LEADING; + else if (style == "middle") + format_t::default_style = format_t::TRUNCATE_MIDDLE; + else if (style == "trailing") + format_t::default_style = format_t::TRUNCATE_TRAILING; + else + throw_(std::invalid_argument, + _("Unrecognized truncation style: '%1'") << style); + format_t::default_style_changed = true; + }); + + OPTION_(report_t, unbudgeted, DO() { + parent->budget_flags |= BUDGET_UNBUDGETED; + }); + + OPTION_(report_t, uncleared, DO() { // -U + parent->HANDLER(limit_).on(string("--uncleared"), "uncleared|pending"); + }); + + OPTION(report_t, unrealized); + + OPTION(report_t, unrealized_gains_); + 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)"); + }); + + OPTION(report_t, unsorted); + + OPTION_(report_t, weekly, DO() { // -W + parent->HANDLER(period_).on(string("--weekly"), "weekly"); + }); + + OPTION_(report_t, wide, DO() { // -w + parent->HANDLER(columns_).on_with(string("--wide"), 132L); + }); + + OPTION_(report_t, yearly, DO() { // -Y + parent->HANDLER(period_).on(string("--yearly"), "yearly"); + }); + + OPTION__(report_t, date_width_, + bool specified; + CTOR(report_t, date_width_) { specified = false; } + DO_(args) { value = args[1].to_long(); specified = true; }); + OPTION__(report_t, payee_width_, + bool specified; + CTOR(report_t, payee_width_) { specified = false; } + DO_(args) { value = args[1].to_long(); specified = true; }); + OPTION__(report_t, account_width_, + bool specified; + CTOR(report_t, account_width_) { specified = false; } + DO_(args) { value = args[1].to_long(); specified = true; }); + OPTION__(report_t, amount_width_, + bool specified; + CTOR(report_t, amount_width_) { specified = false; } + DO_(args) { value = args[1].to_long(); specified = true; }); + OPTION__(report_t, total_width_, + bool specified; + CTOR(report_t, total_width_) { specified = false; } + DO_(args) { value = args[1].to_long(); specified = true; }); +}; + + +template <class Type = post_t, + class handler_ptr = post_handler_ptr, + void (report_t::*report_method)(handler_ptr) = + &report_t::posts_report> +class reporter +{ + shared_ptr<item_handler<Type> > handler; + + report_t& report; + string whence; + +public: + reporter(item_handler<Type> * _handler, + report_t& _report, const string& _whence) + : handler(_handler), report(_report), whence(_whence) {} + + value_t operator()(call_scope_t& args) + { + if (args.size() > 0) + report.parse_query_args(args.value(), whence); + + (report.*report_method)(handler_ptr(handler)); + + return true; + } +}; + +} // namespace ledger + +#endif // _REPORT_H diff --git a/src/scope.cc b/src/scope.cc new file mode 100644 index 00000000..64736ca3 --- /dev/null +++ b/src/scope.cc @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2003-2009, 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 "scope.h" + +namespace ledger { + +scope_t * scope_t::default_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); + + if (! symbols) + symbols = symbol_map(); + + std::pair<symbol_map::iterator, bool> result + = symbols->insert(symbol_map::value_type(symbol_t(kind, name, def), def)); + if (! result.second) { + symbol_map::iterator i = symbols->find(symbol_t(kind, name)); + assert(i != symbols->end()); + symbols->erase(i); + + 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); + } +} + +expr_t::ptr_op_t symbol_scope_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (symbols) { + symbol_map::const_iterator i = symbols->find(symbol_t(kind, name)); + if (i != symbols->end()) + return (*i).second; + } + return child_scope_t::lookup(kind, name); +} + +} // namespace ledger diff --git a/src/scope.h b/src/scope.h new file mode 100644 index 00000000..93f80230 --- /dev/null +++ b/src/scope.h @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2003-2009, 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 scope.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _SCOPE_H +#define _SCOPE_H + +#include "op.h" + +namespace ledger { + +struct symbol_t +{ + enum kind_t { + UNKNOWN, + FUNCTION, + OPTION, + PRECOMMAND, + COMMAND, + DIRECTIVE, + FORMAT + }; + + kind_t kind; + string name; + expr_t::ptr_op_t definition; + + symbol_t() : kind(UNKNOWN), name(""), definition(NULL) { + TRACE_CTOR(symbol_t, ""); + } + symbol_t(kind_t _kind, string _name, expr_t::ptr_op_t _definition = NULL) + : kind(_kind), name(_name), definition(_definition) { + TRACE_CTOR(symbol_t, "symbol_t::kind_t, string"); + } + symbol_t(const symbol_t& sym) + : kind(sym.kind), name(sym.name), + definition(sym.definition) { + TRACE_CTOR(symbol_t, "copy"); + } + ~symbol_t() throw() { + TRACE_DTOR(symbol_t); + } + + bool operator<(const symbol_t& sym) const { + return kind < sym.kind || name < sym.name; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & kind; + ar & name; + ar & definition; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class scope_t +{ +public: + static scope_t * default_scope; + + explicit scope_t() { + TRACE_CTOR(scope_t, ""); + } + virtual ~scope_t() { + TRACE_DTOR(scope_t); + } + + 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) = 0; + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive&, const unsigned int /* version */) {} +#endif // HAVE_BOOST_SERIALIZATION +}; + +class child_scope_t : public noncopyable, public scope_t +{ +public: + scope_t * parent; + + explicit child_scope_t() : parent(NULL) { + TRACE_CTOR(child_scope_t, ""); + } + explicit child_scope_t(scope_t& _parent) + : parent(&_parent) { + TRACE_CTOR(child_scope_t, "scope_t&"); + } + virtual ~child_scope_t() { + TRACE_DTOR(child_scope_t); + } + + virtual void define(const symbol_t::kind_t kind, + const string& name, expr_t::ptr_op_t def) { + if (parent) + parent->define(kind, name, def); + } + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name) { + if (parent) + return parent->lookup(kind, name); + return NULL; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<scope_t>(*this); + ar & parent; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class symbol_scope_t : public child_scope_t +{ + typedef std::map<symbol_t, expr_t::ptr_op_t> symbol_map; + + optional<symbol_map> symbols; + +public: + explicit symbol_scope_t() { + TRACE_CTOR(symbol_scope_t, ""); + } + explicit symbol_scope_t(scope_t& _parent) : child_scope_t(_parent) { + TRACE_CTOR(symbol_scope_t, "scope_t&"); + } + virtual ~symbol_scope_t() { + TRACE_DTOR(symbol_scope_t); + } + + virtual void define(const symbol_t::kind_t kind, const string& name, + expr_t::ptr_op_t def); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<child_scope_t>(*this); + ar & symbols; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class call_scope_t : public child_scope_t +{ + value_t args; + +public: + explicit call_scope_t(scope_t& _parent) : child_scope_t(_parent) { + TRACE_CTOR(call_scope_t, "scope_t&"); + } + virtual ~call_scope_t() { + TRACE_DTOR(call_scope_t); + } + + void set_args(const value_t& _args) { + args = _args; + } + value_t& value() { + return args; + } + + value_t& operator[](const std::size_t index) { + return args[index]; + } + const value_t& operator[](const std::size_t index) const { + return args[index]; + } + + void push_front(const value_t& val) { + args.push_front(val); + } + void push_back(const value_t& val) { + args.push_back(val); + } + void pop_back() { + args.pop_back(); + } + + value_t::sequence_t::const_iterator begin() const { + return args.begin(); + } + value_t::sequence_t::const_iterator end() const { + return args.end(); + } + + std::size_t size() const { + return args.size(); + } + bool empty() const { + return args.size() == 0; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + explicit call_scope_t() { + TRACE_CTOR(call_scope_t, ""); + } + + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<child_scope_t>(*this); + ar & args; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class bind_scope_t : public child_scope_t +{ + bind_scope_t(); + +public: + scope_t& grandchild; + + explicit bind_scope_t(scope_t& _parent, + scope_t& _grandchild) + : child_scope_t(_parent), grandchild(_grandchild) { + TRACE_CTOR(bind_scope_t, "scope_t&, scope_t&"); + } + virtual ~bind_scope_t() { + TRACE_DTOR(bind_scope_t); + } + + virtual void define(const symbol_t::kind_t kind, const string& name, + expr_t::ptr_op_t def) { + parent->define(kind, name, def); + grandchild.define(kind, name, def); + } + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name) { + if (expr_t::ptr_op_t def = grandchild.lookup(kind, name)) + return def; + return child_scope_t::lookup(kind, name); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<child_scope_t>(*this); + ar & grandchild; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +template <typename T> +T * search_scope(scope_t * ptr) +{ + 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)) + return sought; + return search_scope<T>(scope->parent); + } + else if (child_scope_t * scope = dynamic_cast<child_scope_t *>(ptr)) { + return search_scope<T>(scope->parent); + } + return NULL; +} + +template <typename T> +inline T& find_scope(child_scope_t& scope, bool skip_this = true) +{ + if (T * sought = search_scope<T>(skip_this ? scope.parent : &scope)) + return *sought; + + throw_(std::runtime_error, _("Could not find scope")); + return reinterpret_cast<T&>(scope); // never executed +} + +} // namespace ledger + +#endif // _SCOPE_H diff --git a/src/session.cc b/src/session.cc new file mode 100644 index 00000000..1882e554 --- /dev/null +++ b/src/session.cc @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2003-2009, 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 "session.h" +#include "xact.h" +#include "account.h" +#include "journal.h" +#include "iterators.h" +#include "filters.h" +#include "archive.h" + +namespace ledger { + +void set_session_context(session_t * session) +{ + if (session) { + times_initialize(); + amount_t::initialize(); + + amount_t::parse_conversion("1.0m", "60s"); + amount_t::parse_conversion("1.0h", "60m"); + + value_t::initialize(); + } + else if (! session) { + value_t::shutdown(); + amount_t::shutdown(); + times_shutdown(); + } +} + +session_t::session_t() + : flush_on_next_data_file(false), + current_year(CURRENT_DATE().year()), + 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()); +} + +std::size_t session_t::read_data(const string& master_account) +{ + bool populated_data_files = false; + + if (HANDLER(file_).data_files.empty()) { + path file; + if (const char * home_var = std::getenv("HOME")) + file = path(home_var) / ".ledger"; + + if (! file.empty() && exists(file)) + HANDLER(file_).data_files.push_back(file); + else + throw_(parse_error, "No journal file was specified (please use -f)"); + + populated_data_files = true; + } + + std::size_t xact_count = 0; + + account_t * acct = journal->master; + if (! master_account.empty()) + 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 defined(HAVE_BOOST_SERIALIZATION) + optional<archive_t> cache; + if (HANDLED(cache_) && master_account.empty()) + cache = archive_t(HANDLED(cache_).str()); + + if (! (cache && + cache->should_load(HANDLER(file_).data_files) && + cache->load(*journal.get()))) { +#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")); + } + } + + foreach (const path& pathname, HANDLER(file_).data_files) { + if (pathname == "-") { + // To avoid problems with stdin and pipes, etc., we read the entire + // file in beforehand into a memory buffer, and then parcel it out + // from there. + std::ostringstream buffer; + + while (std::cin.good() && ! std::cin.eof()) { + char line[8192]; + std::cin.read(line, 8192); + std::streamsize count = std::cin.gcount(); + buffer.write(line, count); + } + 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()); + } else { + xact_count += journal->read(pathname, acct); + } + } + + assert(xact_count == journal->xacts.size()); + +#if defined(HAVE_BOOST_SERIALIZATION) + if (cache && cache->should_save(*journal.get())) + cache->save(*journal.get()); + } +#endif // HAVE_BOOST_SERIALIZATION + + if (populated_data_files) + HANDLER(file_).data_files.clear(); + + VERIFY(journal->valid()); + + return journal->xacts.size(); +} + +void session_t::read_journal_files() +{ + INFO_START(journal, "Read journal file"); + + string master_account; + if (HANDLED(master_account_)) + master_account = HANDLER(master_account_).str(); + + std::size_t count = read_data(master_account); + if (count == 0) + throw_(parse_error, + _("Failed to locate any transactions; did you specify a valid file with -f?")); + + INFO_FINISH(journal); + + INFO("Found " << count << " transactions"); +} + +void session_t::close_journal_files() +{ + journal.reset(); + amount_t::shutdown(); + + journal.reset(new journal_t); + amount_t::initialize(); +} + +option_t<session_t> * session_t::lookup_option(const char * p) +{ + switch (*p) { + case 'Q': + OPT_CH(download); // -Q + break; + case 'Z': + OPT_CH(price_exp_); + break; + case 'c': + OPT(cache_); + break; + case 'd': + OPT(download); // -Q + break; + case 'e': + OPT(european); + break; + case 'f': + OPT_(file_); // -f + break; + case 'i': + OPT(input_date_format_); + break; + case 'l': + OPT_ALT(price_exp_, leeway_); + break; + case 'm': + OPT(master_account_); + break; + case 'p': + OPT(price_db_); + else OPT(price_exp_); + break; + case 's': + OPT(strict); + break; + } + return NULL; +} + +expr_t::ptr_op_t session_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + switch (kind) { + case symbol_t::FUNCTION: + // Check if they are trying to access an option's setting or value. + if (option_t<session_t> * handler = lookup_option(name.c_str())) + return MAKE_OPT_FUNCTOR(session_t, handler); + break; + + case symbol_t::OPTION: + if (option_t<session_t> * handler = lookup_option(name.c_str())) + return MAKE_OPT_HANDLER(session_t, handler); + break; + + default: + break; + } + + return symbol_scope_t::lookup(kind, name); +} + +} // namespace ledger diff --git a/src/session.h b/src/session.h new file mode 100644 index 00000000..5c4612a0 --- /dev/null +++ b/src/session.h @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup report Reporting + */ + +/** + * @file session.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _SESSION_H +#define _SESSION_H + +#include "interactive.h" +#include "account.h" +#include "journal.h" +#include "option.h" +#include "commodity.h" + +namespace ledger { + +class commodity_pool_t; +class xact_t; + +class session_t : public symbol_scope_t +{ + friend void set_session_context(session_t * session); + +public: + bool flush_on_next_data_file; + date_t::year_type current_year; + std::auto_ptr<journal_t> journal; + + explicit session_t(); + virtual ~session_t() { + TRACE_DTOR(session_t); + } + + void set_flush_on_next_data_file(const bool truth) { + flush_on_next_data_file = truth; + } + + std::size_t read_data(const string& master_account = ""); + + void read_journal_files(); + void close_journal_files(); + + void report_options(std::ostream& out) + { + HANDLER(cache_).report(out); + HANDLER(download).report(out); + HANDLER(european).report(out); + HANDLER(file_).report(out); + HANDLER(input_date_format_).report(out); + HANDLER(master_account_).report(out); + HANDLER(price_db_).report(out); + HANDLER(price_exp_).report(out); + HANDLER(strict).report(out); + } + + option_t<session_t> * lookup_option(const char * p); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + + /** + * Option handlers + */ + + OPTION(session_t, cache_); + OPTION(session_t, download); // -Q + + OPTION_(session_t, european, DO() { + commodity_t::european_by_default = true; + }); + + OPTION__ + (session_t, price_exp_, // -Z + CTOR(session_t, price_exp_) { value = 24L * 3600L; } + DO_(args) { + value = args[1].to_long() * 60L; + }); + + OPTION__ + (session_t, file_, // -f + std::list<path> data_files; + CTOR(session_t, file_) {} + DO_(args) { + assert(args.size() == 2); + if (parent->flush_on_next_data_file) { + data_files.clear(); + parent->flush_on_next_data_file = false; + } + data_files.push_back(args[1].as_string()); + }); + + 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[1].as_string().c_str()); + }); + + OPTION(session_t, master_account_); + OPTION(session_t, price_db_); + OPTION(session_t, strict); +}; + +/** + * Set the current session context, transferring all static globals to point + * at the data structures related to this session. Although Ledger itself is + * not thread-safe, by locking, switching session context, then unlocking + * after an operation is done, multiple threads can sequentially make use of + * the library. Thus, a session_t maintains all of the information relating + * to a single usage of the Ledger library. + */ +void set_session_context(session_t * session); + +} // namespace ledger + +#endif // _SESSION_H diff --git a/src/stats.cc b/src/stats.cc new file mode 100644 index 00000000..b89a5949 --- /dev/null +++ b/src/stats.cc @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2003-2009, 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 "draft.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "report.h" +#include "session.h" + +namespace ledger { + +value_t report_statistics(call_scope_t& args) +{ + report_t& report(find_scope<report_t>(args)); + std::ostream& out(report.output_stream); + + const account_t::xdata_t::details_t& + statistics(report.session.journal->master->family_details(true)); + + if (! is_valid(statistics.earliest_post) && + ! is_valid(statistics.latest_post)) + return NULL_VALUE; + + assert(is_valid(statistics.earliest_post)); + assert(is_valid(statistics.latest_post)); + + { + straccstream accum; + out << ACCUM(accum << _("Time period: %1 to %2 (%3 days)") + << format_date(statistics.earliest_post) + << format_date(statistics.latest_post) + << (statistics.latest_post - + statistics.earliest_post).days()) + << std::endl << std::endl; + } + + out << _(" Files these postings came from:") << std::endl; + + foreach (const path& pathname, statistics.filenames) + if (! pathname.empty()) + out << " " << pathname.string() << std::endl; + out << std::endl; + + out << _(" Unique payees: "); + out.width(6); + out << statistics.payees_referenced.size() << std::endl; + + out << _(" Unique accounts: "); + out.width(6); + out << statistics.accounts_referenced.size() << std::endl; + + out << std::endl; + + out << _(" Number of postings: "); + out.width(6); + out << statistics.posts_count; + + out << " ("; + out.precision(2); + out << (double((statistics.latest_post - statistics.earliest_post).days()) / + double(statistics.posts_count)) << _(" per day)") << std::endl; + + out << _(" Uncleared postings: "); + out.width(6); + out << (statistics.posts_count - + statistics.posts_cleared_count) << std::endl; + + out << std::endl; + + out << _(" Days since last post: "); + out.width(6); + out << (CURRENT_DATE() - statistics.latest_post).days() + << std::endl; + + out << _(" Posts in last 7 days: "); + out.width(6); + out << statistics.posts_last_7_count << std::endl; + out << _(" Posts in last 30 days: "); + out.width(6); + out << statistics.posts_last_30_count << std::endl; + out << _(" Posts seen this month: "); + out.width(6); + out << statistics.posts_this_month_count << std::endl; + + out.flush(); + + return NULL_VALUE; +} + +} // namespace ledger diff --git a/src/stats.h b/src/stats.h new file mode 100644 index 00000000..d2d10de6 --- /dev/null +++ b/src/stats.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2003-2009, 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 stats + */ + +/** + * @file stats.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _STATS_H +#define _STATS_H + +#include "value.h" + +namespace ledger { + +class call_scope_t; + +value_t report_statistics(call_scope_t& scope); + +} // namespace ledger + +#endif // _STATS_H diff --git a/src/stream.cc b/src/stream.cc new file mode 100644 index 00000000..e39b74e0 --- /dev/null +++ b/src/stream.cc @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2003-2009, 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 "stream.h" + +namespace ledger { + +namespace { + /** + * @brief Forks a child process so that Ledger may handle running a + * pager + * + * In order for the pager option to work, Ledger has to run the pager + * itself, which requires Ledger to fork a new process in order to run + * the pager. This function does the necessary fork. After the fork, + * two processes exist. One of them is exec'd to create the pager; + * the other is still Ledger. + * + * This function returns only for the process that is still Ledger. + * + * @param out Pointer to a pointer to the output stream. This The + * pointer to the output stream is changed so that the stream is + * connected to the stdin of the pager. The caller is responsible for + * cleaning this up. + * + * @param pager_path Path to the pager command. + * + * @return The file descriptor of the pipe to the pager. The caller + * is responsible for cleaning this up. + * + * @exception std::logic_error Some problem was encountered, such as + * failure to create a pipe or failure to fork a child process. + */ + int do_fork(std::ostream ** os, const path& pager_path) + { + int pfd[2]; + + int status = pipe(pfd); + if (status == -1) + throw std::logic_error(_("Failed to create pipe")); + + status = fork(); + if (status < 0) { + throw std::logic_error(_("Failed to fork child process")); + } + else if (status == 0) { // child + // Duplicate pipe's reading end into stdin + status = dup2(pfd[0], STDIN_FILENO); + if (status == -1) + perror("dup2"); + + // Close unuseful file descriptors: the pipe's writing and reading + // ends (the latter is not needed anymore, after the duplication). + close(pfd[1]); + close(pfd[0]); + + // Find command name: its the substring starting right of the + // rightmost '/' character in the pager pathname. See manpage for + // strrchr. +#if BOOST_VERSION >= 103700 + path basename(pager_path.filename()); +#else + path basename(pager_path.leaf()); +#endif + execlp(pager_path.string().c_str(), basename.string().c_str(), NULL); + + // We should never, ever reach here + perror((std::string("execlp: ") + pager_path.string()).c_str()); + exit(1); + } + else { // parent + close(pfd[0]); + typedef iostreams::stream<iostreams::file_descriptor_sink> fdstream; + *os = new fdstream(pfd[1]); + } + return pfd[1]; + } +} + +void output_stream_t::initialize(const optional<path>& output_file, + const optional<path>& pager_path) +{ + if (output_file && *output_file != "-") + os = new ofstream(*output_file); + else if (pager_path) + pipe_to_pager_fd = do_fork(&os, *pager_path); + else + os = &std::cout; +} + +void output_stream_t::close() +{ + if (os != &std::cout) { + checked_delete(os); + os = &std::cout; + } + + if (pipe_to_pager_fd != -1) { + ::close(pipe_to_pager_fd); + pipe_to_pager_fd = -1; + + int status; + wait(&status); + if (! WIFEXITED(status) || WEXITSTATUS(status) != 0) + throw std::logic_error(_("Error in the pager")); + } +} + +} // namespace ledger diff --git a/src/stream.h b/src/stream.h new file mode 100644 index 00000000..e87da67a --- /dev/null +++ b/src/stream.h @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file stream.h + * @author John Wiegley, Omari Norman + * + * @ingroup util + * + * @brief A utility class for abstracting an output stream. + * + * Because Ledger might send output to a file, the console, or a pager + * child process, different cleanup is needed for each scenario. This + * file abstracts those various needs. + */ +#ifndef _STREAM_H +#define _STREAM_H + +#include "utils.h" + +namespace ledger { + +/** + * @brief An output stream + * + * A stream to output in Ledger may be going to one of three places: + * to stdout, to a file, or to a pager. Construct an output_stream_t and + * the stream will automatically be cleaned up upon destruction. + * + * This class suffers from "else-if-heimer's disease," see Marshall + * Cline's "C++ FAQ Lite". Arguably this should be three different + * classes, but that introduces additional unneeded complications. + */ +class output_stream_t +{ + output_stream_t& operator=(const output_stream_t&); + +private: + int pipe_to_pager_fd; + +public: + /** + * A pointer to the ostream. Don't delete this; the output_stream_t + * class takes care of this. + */ + std::ostream * os; + + /** + * Construct a new output_stream_t. + */ + output_stream_t() : pipe_to_pager_fd(-1), os(&std::cout) { + TRACE_CTOR(output_stream_t, ""); + } + + /** + * When copy-constructed, make the copy just be a new output stream. This + * allows large classes to rely on their default copy-constructor without + * worrying about pointer copying within output_stream_t. + */ + output_stream_t(const output_stream_t&) + : pipe_to_pager_fd(-1), os(&std::cout) { + TRACE_CTOR(output_stream_t, "copy"); + } + + /** + * Destroys an output_stream_t. This deletes the dynamically + * allocated ostream, if necessary. It also closes output file + * descriptor, if necessary. + */ + ~output_stream_t() { + TRACE_DTOR(output_stream_t); + close(); + } + + /** + * Initialize the output stream object. + * + * @param output_file File to which to send output. If both this + * and pager are set, output_file takes priority. + * + * @param pager_path Path to a pager. To not use a pager, leave this + * empty. + */ + void initialize(const optional<path>& output_file = none, + const optional<path>& pager_path = none); + + /** + * Convertor to a standard ostream. This is used so that we can + * stream directly to an object of type output_stream_t. + */ + operator std::ostream&() { + return *os; + } + + /** + * Flushing function. A simple proxy for ostream's flush. + */ + void flush() { + os->flush(); + } + + /** + * Close the output stream, waiting on the pager process if necessary. + */ + void close(); +}; + +} // namespace ledger + +#endif // _STREAM_H diff --git a/src/system.hh.in b/src/system.hh.in new file mode 100644 index 00000000..341ce129 --- /dev/null +++ b/src/system.hh.in @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + * + * @file system.hh + * @author John Wiegley + * + * @brief All system headers needed by Ledger. + * + * These are collected here so that a pre-compiled header can be made. + * None of these header files (with the exception of acconf.h, when + * configure is re-run) are expected to change. + */ +#ifndef _SYSTEM_HH +#define _SYSTEM_HH + +#include "config.h" + +#if defined(__GNUG__) && __GNUG__ < 3 +#define _XOPEN_SOURCE +#endif + +#include <algorithm> +#include <exception> +#include <typeinfo> +#include <locale> +#include <stdexcept> +#include <iostream> +#include <streambuf> +#include <iomanip> +#include <fstream> +#include <sstream> +#include <iterator> +#include <list> +#include <map> +#include <memory> +#include <new> +#include <set> +#include <stack> +#include <string> +#include <vector> + +#if defined(__GNUG__) && __GNUG__ < 3 + +namespace std { + inline ostream & right (ostream & i) { + i.setf(i.right, i.adjustfield); + return i; + } + inline ostream & left (ostream & i) { + i.setf(i.left, i.adjustfield); + return i; + } +} + +typedef std::streamoff istream_pos_type; +typedef std::streamoff ostream_pos_type; + +#else // ! (defined(__GNUG__) && __GNUG__ < 3) + +typedef std::istream::pos_type istream_pos_type; +typedef std::ostream::pos_type ostream_pos_type; + +#endif + +#include <cassert> +#include <cctype> +#include <cstdarg> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <csignal> + +#if defined __FreeBSD__ && __FreeBSD__ <= 4 +// FreeBSD has a broken isspace macro, so don't use it +#undef isspace(c) +#endif + +#include <sys/stat.h> +#if defined(WIN32) +#include <io.h> +#else +#include <unistd.h> +#endif +#if defined(HAVE_GETPWUID) || defined(HAVE_GETPWNAM) +#include <pwd.h> +#endif + +#if defined(HAVE_UNIX_PIPES) +#include <sys/types.h> +#include <sys/wait.h> +#endif +#if defined(HAVE_GETTEXT) +#include "gettext.h" +#define _(str) gettext(str) +#else +#define _(str) str +#endif + +#include <gmp.h> +#include <mpfr.h> +#include "sha1.h" +#include "utf8.h" + +#if defined(HAVE_LIBEDIT) +#include <editline/readline.h> +#endif + +#include <boost/algorithm/string/classification.hpp> +#include <boost/algorithm/string/predicate.hpp> +#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> +#include <boost/foreach.hpp> +#include <boost/function.hpp> +#include <boost/intrusive_ptr.hpp> +#include <boost/iostreams/stream.hpp> +#include <boost/iostreams/write.hpp> +#include <boost/iostreams/device/file_descriptor.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/variant.hpp> +#include <boost/version.hpp> + +#if defined(HAVE_BOOST_SERIALIZATION) + +#include <boost/archive/binary_iarchive.hpp> +#include <boost/archive/binary_oarchive.hpp> + +#include <boost/serialization/base_object.hpp> +#include <boost/serialization/binary_object.hpp> +#include <boost/serialization/optional.hpp> +#include <boost/serialization/shared_ptr.hpp> +#include <boost/serialization/variant.hpp> +#include <boost/serialization/utility.hpp> +#include <boost/serialization/export.hpp> +#include <boost/serialization/level.hpp> +#include <boost/serialization/string.hpp> +#include <boost/serialization/vector.hpp> +#include <boost/serialization/deque.hpp> +#include <boost/serialization/list.hpp> +#include <boost/serialization/map.hpp> + +#include <boost/date_time/posix_time/time_serialize.hpp> +#include <boost/date_time/gregorian/greg_serialize.hpp> + +namespace boost { +namespace serialization { + +template <class Archive> +void serialize(Archive& ar, boost::filesystem::path& p, const unsigned int) +{ + std::string s; + if (Archive::is_saving::value) + s = p.string(); + + ar & s; + + if (Archive::is_loading::value) + p = s; +} + +template <class Archive, class T> +void serialize(Archive& ar, boost::intrusive_ptr<T>& ptr, const unsigned int) +{ + if (Archive::is_saving::value) { + T * p = ptr.get(); + ar & p; + } + else if (Archive::is_loading::value) { + T * p; + ar & p; + ptr.reset(p); + } +} + +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)); +} + +} // namespace serialization +} // namespace boost + +#endif // HAVE_BOOST_SERIALIZATION + +#if defined(HAVE_BOOST_PYTHON) + +#include <boost/python.hpp> + +#include <boost/python/detail/wrap_python.hpp> +#include <datetime.h> +#include <unicodeobject.h> + +#include <boost/python/module_init.hpp> +#include <boost/python/suite/indexing/vector_indexing_suite.hpp> + +#endif // HAVE_BOOST_PYTHON + +#endif // _SYSTEM_HH diff --git a/src/temps.cc b/src/temps.cc new file mode 100644 index 00000000..fd099e9a --- /dev/null +++ b/src/temps.cc @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2003-2009, 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 "xact.h" +#include "post.h" +#include "account.h" +#include "temps.h" + +namespace ledger { + +temporaries_t::~temporaries_t() +{ + if (post_temps) { + foreach (post_t& post, *post_temps) { + if (! post.xact->has_flags(ITEM_TEMP)) + post.xact->remove_post(&post); + + if (post.account && ! post.account->has_flags(ACCOUNT_TEMP)) + post.account->remove_post(&post); + } + } + + if (acct_temps) { + foreach (account_t& acct, *acct_temps) { + if (acct.parent && ! acct.parent->has_flags(ACCOUNT_TEMP)) + acct.parent->remove_account(&acct); + } + } +} + +xact_t& temporaries_t::copy_xact(xact_t& origin) +{ + if (! xact_temps) + xact_temps = std::list<xact_t>(); + + xact_temps->push_back(origin); + xact_t& temp(xact_temps->back()); + + temp.add_flags(ITEM_TEMP); + return temp; +} + +xact_t& temporaries_t::create_xact() +{ + if (! xact_temps) + xact_temps = std::list<xact_t>(); + + xact_temps->push_back(xact_t()); + xact_t& temp(xact_temps->back()); + + temp.add_flags(ITEM_TEMP); + return temp; +} + +post_t& temporaries_t::copy_post(post_t& origin, xact_t& xact, + account_t * account) +{ + if (! post_temps) + post_temps = std::list<post_t>(); + + post_temps->push_back(origin); + post_t& temp(post_temps->back()); + + if (account) + temp.account = account; + temp.add_flags(ITEM_TEMP); + + temp.account->add_post(&temp); + xact.add_post(&temp); + + return temp; +} + +post_t& temporaries_t::create_post(xact_t& xact, account_t * account) +{ + if (! post_temps) + post_temps = std::list<post_t>(); + + post_temps->push_back(post_t(account)); + post_t& temp(post_temps->back()); + + temp.account = account; + temp.add_flags(ITEM_TEMP); + + temp.account->add_post(&temp); + xact.add_post(&temp); + + return temp; +} + +account_t& temporaries_t::create_account(const string& name, + account_t * parent) +{ + if (! acct_temps) + acct_temps = std::list<account_t>(); + + acct_temps->push_back(account_t(parent, name)); + account_t& temp(acct_temps->back()); + + if (parent) + parent->add_account(&temp); + + temp.add_flags(ACCOUNT_TEMP); + return temp; +} + +} // namespace ledger diff --git a/src/temps.h b/src/temps.h new file mode 100644 index 00000000..4243079c --- /dev/null +++ b/src/temps.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file temps.h + * @author John Wiegley + * + * @ingroup report + */ +#ifndef _TEMPS_H +#define _TEMPS_H + +namespace ledger { + +class temporaries_t +{ + optional<std::list<xact_t> > xact_temps; + optional<std::list<post_t> > post_temps; + optional<std::list<account_t> > acct_temps; + +public: + ~temporaries_t(); + + xact_t& copy_xact(xact_t& origin); + xact_t& create_xact(); + xact_t& last_xact() { + return xact_temps->back(); + } + 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& last_post() { + return post_temps->back(); + } + account_t& create_account(const string& name = "", + account_t * parent = NULL); + account_t& last_account() { + return acct_temps->back(); + } +}; + +} // namespace ledger + +#endif // _TEMPS_H diff --git a/src/textual.cc b/src/textual.cc new file mode 100644 index 00000000..cf670cae --- /dev/null +++ b/src/textual.cc @@ -0,0 +1,1396 @@ +/* + * Copyright (c) 2003-2009, 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 "journal.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "option.h" +#include "query.h" +#include "pstream.h" +#include "pool.h" +#include "session.h" + +#define TIMELOG_SUPPORT 1 +#if defined(TIMELOG_SUPPORT) +#include "timelog.h" +#endif + +namespace ledger { + +namespace { + class instance_t : public noncopyable, public scope_t + { + static const std::size_t MAX_LINE = 1024; + + public: + typedef std::pair<commodity_t *, amount_t> fixed_rate_t; + typedef variant<account_t *, string, fixed_rate_t> state_t; + + std::list<state_t>& state_stack; + +#if defined(TIMELOG_SUPPORT) + time_log_t& timelog; +#endif + instance_t * parent; + std::istream& in; + scope_t& scope; + journal_t& journal; + account_t * master; + const path * original_file; + accounts_map account_aliases; + bool strict; + path pathname; + char linebuf[MAX_LINE + 1]; + std::size_t linenum; + istream_pos_type line_beg_pos; + istream_pos_type curr_pos; + std::size_t count; + std::size_t errors; + + optional<date_t::year_type> current_year; + + instance_t(std::list<state_t>& _state_stack, +#if defined(TIMELOG_SUPPORT) + time_log_t& _timelog, +#endif + std::istream& _in, + scope_t& _scope, + journal_t& _journal, + account_t * _master = NULL, + const path * _original_file = NULL, + bool _strict = false, + instance_t * _parent = NULL); + + ~instance_t(); + + 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); + } + + account_t * top_account() { + foreach (state_t& state, state_stack) + if (state.type() == typeid(account_t *)) + return boost::get<account_t *>(state); + return NULL; + } + + void parse(); + std::streamsize read_line(char *& line); + bool peek_whitespace_line() { + return (in.good() && ! in.eof() && + (in.peek() == ' ' || in.peek() == '\t')); + } + + void read_next_directive(); + +#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); + void default_account_directive(char * line); + void price_conversion_directive(char * line); + void price_xact_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 tag_directive(char * line); + void define_directive(char * line); + bool general_directive(char * line); + + post_t * parse_post(char * line, + std::streamsize len, + account_t * account, + xact_t * xact, + bool honor_strict = true, + bool defer_expr = false); + + bool parse_posts(account_t * account, + xact_base_t& xact, + const bool defer_expr = false); + + xact_t * parse_xact(char * line, + std::streamsize len, + account_t * account); + + virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind, + const string& name); + }; + + void parse_amount_expr(scope_t& scope, + std::istream& in, + amount_t& amount, + optional<expr_t> * amount_expr, + post_t * post, + const parse_flags_t& flags = PARSE_DEFAULT, + const bool defer_expr = false) + { + expr_t expr(in, flags.plus_flags(PARSE_PARTIAL)); + + DEBUG("textual.parse", "Parsed an amount expression"); + +#if defined(DEBUG_ENABLED) + DEBUG_IF("textual.parse") { + if (_debug_stream) { + ledger::dump_value_expr(*_debug_stream, expr); + *_debug_stream << std::endl; + } + } +#endif + + if (expr) { + bind_scope_t bound_scope(scope, *post); + if (defer_expr) { + assert(amount_expr); + *amount_expr = expr; + (*amount_expr)->compile(bound_scope); + } else { + value_t result(expr.calc(bound_scope)); + if (result.is_long()) { + amount = result.to_amount(); + } else { + if (! result.is_amount()) + throw_(amount_error, + _("Amount expressions must result in a simple amount")); + amount = result.as_amount(); + } + DEBUG("textual.parse", "The posting amount is " << amount); + } + } + } +} + +instance_t::instance_t(std::list<state_t>& _state_stack, +#if defined(TIMELOG_SUPPORT) + time_log_t& _timelog, +#endif + std::istream& _in, + scope_t& _scope, + journal_t& _journal, + account_t * _master, + const path * _original_file, + bool _strict, + instance_t * _parent) + : state_stack(_state_stack), +#if defined(TIMELOG_SUPPORT) + timelog(_timelog), +#endif + parent(_parent), in(_in), scope(_scope), + journal(_journal), master(_master), + original_file(_original_file), strict(_strict) +{ + TRACE_CTOR(instance_t, "..."); + + if (! master) + master = journal.master; + state_stack.push_front(master); + + if (_original_file) + pathname = *_original_file; + else + pathname = "/dev/stdin"; +} + +instance_t::~instance_t() +{ + TRACE_DTOR(instance_t); + + assert(! state_stack.empty()); + state_stack.pop_front(); +} + +void instance_t::parse() +{ + INFO("Parsing file '" << pathname.string() << "'"); + + TRACE_START(instance_parse, 1, + "Done parsing file '" << pathname.string() << "'"); + + if (! in.good() || in.eof()) + return; + + linenum = 0; + errors = 0; + count = 0; + curr_pos = in.tellg(); + + while (in.good() && ! in.eof()) { + try { + read_next_directive(); + } + catch (const std::exception& err) { + string current_context = error_context(); + + if (parent) { + std::list<instance_t *> instances; + + for (instance_t * instance = parent; + instance; + instance = instance->parent) + instances.push_front(instance); + + foreach (instance_t * instance, instances) + add_error_context(_("In file included from %1") + << file_context(instance->pathname, + instance->linenum)); + } + add_error_context(_("While parsing file %1") + << file_context(pathname, linenum)); + + if (caught_signal != NONE_CAUGHT) + throw; + + string context = error_context(); + if (! context.empty()) + std::cerr << context << std::endl; + + if (! current_context.empty()) + std::cerr << current_context << std::endl; + + std::cerr << _("Error: ") << err.what() << std::endl; + errors++; + } + } + + TRACE_STOP(instance_parse, 1); +} + +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; + + check_for_signal(); + + in.getline(linebuf, MAX_LINE); + std::streamsize len = in.gcount(); + + if (len > 0) { + if (linenum == 0 && utf8::is_bom(linebuf)) + line = &linebuf[3]; + else + line = linebuf; + + if (line[len - 1] == '\r') // strip Windows CRLF down to LF + line[--len] = '\0'; + + linenum++; + + curr_pos = line_beg_pos; + curr_pos += len; + + return len - 1; // LF is being silently dropped + } + return 0; +} + +void instance_t::read_next_directive() +{ + char * line; + std::streamsize len = read_line(line); + + if (len == 0 || line == NULL) + return; + + switch (line[0]) { + case '\0': + assert(false); // shouldn't ever reach here + break; + + case ' ': + case '\t': { + break; + } + + case ';': // comments + case '#': + case '*': + case '|': + break; + + case '-': // option setting + option_directive(line); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + xact_directive(line, len); + break; + case '=': // automated xact + automated_xact_directive(line); + break; + case '~': // period xact + period_xact_directive(line); + break; + + case '@': + case '!': + line++; + // fall through... + default: // some other directive + if (! general_directive(line)) { + switch (line[0]) { +#if defined(TIMELOG_SUPPORT) + case 'i': + clock_in_directive(line, false); + break; + case 'I': + clock_in_directive(line, true); + break; + + case 'o': + clock_out_directive(line, false); + break; + case 'O': + clock_out_directive(line, true); + break; + + case 'h': + case 'b': + break; +#endif // TIMELOG_SUPPORT + + case 'A': // a default account for unbalanced posts + default_account_directive(line); + break; + case 'C': // a set of conversions + price_conversion_directive(line); + break; + case 'D': // a default commodity for "xact" + default_commodity_directive(line); + break; + case 'N': // don't download prices + nomarket_directive(line); + break; + case 'P': // a pricing xact + price_xact_directive(line); + break; + case 'Y': // set the current year + year_directive(line); + break; + } + } + break; + } +} + +#if defined(TIMELOG_SUPPORT) + +void instance_t::clock_in_directive(char * line, + bool /*capitalized*/) +{ + string datetime(line, 2, 19); + + char * p = skip_ws(line + 22); + char * n = next_element(p, true); + char * end = n ? next_element(n, true) : NULL; + + if (end && *end == ';') + end = skip_ws(end + 1); + else + 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; + + time_xact_t event(position, parse_datetime(datetime, current_year), + p ? top_account()->find_account(p) : NULL, + n ? n : "", + end ? end : ""); + + timelog.clock_in(event); +} + +void instance_t::clock_out_directive(char * line, + bool /*capitalized*/) +{ + string datetime(line, 2, 19); + + char * p = skip_ws(line + 22); + char * n = next_element(p, true); + char * end = n ? next_element(n, true) : NULL; + + if (end && *end == ';') + end = skip_ws(end + 1); + else + 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; + + time_xact_t event(position, parse_datetime(datetime, current_year), + p ? top_account()->find_account(p) : NULL, + n ? n : "", + end ? end : ""); + + timelog.clock_out(event); + count++; +} + +#endif // TIMELOG_SUPPORT + +void instance_t::default_commodity_directive(char * line) +{ + amount_t amt(skip_ws(line + 1)); + VERIFY(amt.valid()); + commodity_pool_t::current_pool->default_commodity = &amt.commodity(); + amt.commodity().add_flags(COMMODITY_KNOWN); +} + +void instance_t::default_account_directive(char * line) +{ + journal.bucket = top_account()->find_account(skip_ws(line + 1)); + journal.bucket->add_flags(ACCOUNT_KNOWN); +} + +void instance_t::price_conversion_directive(char * line) +{ + if (char * p = std::strchr(line + 1, '=')) { + *p++ = '\0'; + amount_t::parse_conversion(line + 1, p); + } +} + +void instance_t::price_xact_directive(char * line) +{ + optional<std::pair<commodity_t *, price_point_t> > point = + commodity_pool_t::current_pool->parse_price_directive(skip_ws(line + 1)); + if (! point) + throw parse_error(_("Pricing entry failed to parse")); +} + +void instance_t::nomarket_directive(char * line) +{ + char * p = skip_ws(line + 1); + string symbol; + commodity_t::parse_symbol(p, symbol); + + if (commodity_t * commodity = + commodity_pool_t::current_pool->find_or_create(symbol)) + commodity->add_flags(COMMODITY_NOMARKET | COMMODITY_KNOWN); +} + +void instance_t::year_directive(char * line) +{ + current_year = lexical_cast<unsigned short>(skip_ws(line + 1)); +} + +void instance_t::option_directive(char * line) +{ + char * p = next_element(line); + if (! p) { + p = std::strchr(line, '='); + if (p) + *p++ = '\0'; + } + + if (! process_option(pathname.string(), line + 2, 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; + std::size_t lnum = linenum; + + bool reveal_context = true; + + try { + + std::auto_ptr<auto_xact_t> ae + (new auto_xact_t(query_t(string(skip_ws(line + 1)), + keep_details_t(true, true, true)))); + + reveal_context = false; + + if (parse_posts(top_account(), *ae.get(), true)) { + reveal_context = true; + + journal.auto_xacts.push_back(ae.get()); + + ae->journal = &journal; + ae->pos = position_t(); + ae->pos->pathname = pathname; + ae->pos->beg_pos = pos; + ae->pos->beg_line = lnum; + ae->pos->end_pos = curr_pos; + ae->pos->end_line = linenum; + + ae.release(); + } + + } + catch (const std::exception& err) { + if (reveal_context) { + add_error_context(_("While parsing periodic transaction:")); + add_error_context(source_context(pathname, pos, curr_pos, "> ")); + } + throw; + } +} + +void instance_t::period_xact_directive(char * line) +{ + istream_pos_type pos = line_beg_pos; + std::size_t lnum = linenum; + + bool reveal_context = true; + + try { + + std::auto_ptr<period_xact_t> pe(new period_xact_t(skip_ws(line + 1))); + + reveal_context = false; + + if (parse_posts(top_account(), *pe.get())) { + reveal_context = true; + pe->journal = &journal; + + if (pe->finalize()) { + journal.extend_xact(pe.get()); + journal.period_xacts.push_back(pe.get()); + + pe->pos = position_t(); + pe->pos->pathname = pathname; + pe->pos->beg_pos = pos; + pe->pos->beg_line = lnum; + pe->pos->end_pos = curr_pos; + pe->pos->end_line = linenum; + + pe.release(); + } else { + pe->journal = NULL; + throw parse_error(_("Period transaction failed to balance")); + } + } + + } + catch (const std::exception& err) { + if (reveal_context) { + add_error_context(_("While parsing periodic transaction:")); + add_error_context(source_context(pathname, pos, curr_pos, "> ")); + } + throw; + } +} + +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, top_account())) { + std::auto_ptr<xact_t> manager(xact); + + if (journal.add_xact(xact)) { + manager.release(); // it's owned by the journal now + count++; + } + // It's perfectly valid for the journal to reject the xact, which it will + // do if the xact has no substantive effect (for example, a checking + // xact, all of whose postings have null amounts). + } else { + throw parse_error(_("Failed to parse transaction")); + } + + TRACE_STOP(xacts, 1); +} + +void instance_t::include_directive(char * line) +{ + path filename; + + if (line[0] != '/' && line[0] != '\\' && line[0] != '~') { + string::size_type pos = pathname.string().rfind('/'); + if (pos == string::npos) + pos = pathname.string().rfind('\\'); + if (pos != string::npos) + filename = path(string(pathname.string(), 0, pos + 1)) / line; + } else { + filename = line; + } + + filename = resolve_path(filename); + + DEBUG("textual.include", "Line " << linenum << ": " << + "Including path '" << filename << "'"); + + if (! exists(filename)) + throw_(std::runtime_error, + _("File to include was not found: '%1'") << filename); + + ifstream stream(filename); + + instance_t instance(state_stack, +#if defined(TIMELOG_SUPPORT) + timelog, +#endif + stream, scope, journal, master, + &filename, strict, this); + instance.parse(); + + errors += instance.errors; + count += instance.count; +} + +void instance_t::master_account_directive(char * line) +{ + if (account_t * acct = top_account()->find_account(line)) + state_stack.push_front(acct); + else + assert(! "Failed to create account"); +} + +void instance_t::end_directive(char * kind) +{ + string name(kind); + + if ((name.empty() || name == "account") && ! front_is_account()) + throw_(std::runtime_error, + _("'end account' directive does not match open directive")); + else if (name == "tag" && ! front_is_string()) + throw_(std::runtime_error, + _("'end tag' directive does not match open directive")); + else if (name == "fixed" && ! front_is_fixed_rate()) + throw_(std::runtime_error, + _("'end fixed' directive does not match open directive")); + + if (state_stack.size() <= 1) + throw_(std::runtime_error, + _("'end' found, but no enclosing tag or account directive")); + else + state_stack.pop_front(); +} + +void instance_t::alias_directive(char * line) +{ + char * b = skip_ws(line); + if (char * e = std::strchr(b, '=')) { + 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 = top_account()->find_account(e); + std::pair<accounts_map::iterator, bool> result + = account_aliases.insert(accounts_map::value_type(b, acct)); + assert(result.second); + } +} + +void instance_t::fixed_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)) { + state_stack.push_front(fixed_rate_t(price_point->first, + price_point->second.price)); + } else { + throw_(std::runtime_error, _("Error in fixed directive")); + } +} + +void instance_t::tag_directive(char * line) +{ + string tag(trim_ws(line)); + + if (tag.find(':') == string::npos) + tag = string(":") + tag + ":"; + + state_stack.push_front(tag); +} + +void instance_t::define_directive(char * line) +{ + expr_t def(skip_ws(line)); + def.compile(scope); // causes definitions to be established +} + +bool instance_t::general_directive(char * line) +{ + char buf[8192]; + + std::strcpy(buf, line); + + char * p = buf; + char * arg = next_element(buf); + + if (*p == '@' || *p == '!') + p++; + + switch (*p) { + case 'a': + if (std::strcmp(p, "account") == 0) { + master_account_directive(arg); + return true; + } + else if (std::strcmp(p, "alias") == 0) { + alias_directive(arg); + return true; + } + break; + + case 'b': + if (std::strcmp(p, "bucket") == 0) { + default_account_directive(arg); + return true; + } + break; + + case 'd': + if (std::strcmp(p, "def") == 0 || std::strcmp(p, "define") == 0) { + define_directive(arg); + return true; + } + break; + + case 'e': + if (std::strcmp(p, "end") == 0) { + end_directive(arg); + return true; + } + break; + + case 'f': + if (std::strcmp(p, "fixed") == 0) { + fixed_directive(arg); + return true; + } + break; + + case 'i': + if (std::strcmp(p, "include") == 0) { + include_directive(arg); + return true; + } + break; + + case 't': + if (std::strcmp(p, "tag") == 0) { + tag_directive(arg); + return true; + } + break; + + case 'y': + if (std::strcmp(p, "year") == 0) { + year_directive(arg); + return true; + } + break; + } + + if (expr_t::ptr_op_t op = lookup(symbol_t::DIRECTIVE, p)) { + call_scope_t args(*this); + args.push_back(string_value(p)); + op->as_function()(args); + return true; + } + + return false; +} + +post_t * instance_t::parse_post(char * line, + std::streamsize len, + account_t * account, + xact_t * xact, + bool honor_strict, + bool defer_expr) +{ + TRACE_START(post_details, 1, "Time spent parsing postings:"); + + std::auto_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; + + char buf[MAX_LINE + 1]; + std::strcpy(buf, line); + std::size_t beg = 0; + + try { + + // Parse the state flag + + assert(line); + assert(*line); + + char * p = skip_ws(line); + + switch (*p) { + case '*': + post->set_state(item_t::CLEARED); + p = skip_ws(p + 1); + DEBUG("textual.parse", "line " << linenum << ": " + << "Parsed the CLEARED flag"); + break; + + case '!': + post->set_state(item_t::PENDING); + p = skip_ws(p + 1); + DEBUG("textual.parse", "line " << linenum << ": " + << "Parsed the PENDING flag"); + break; + } + + if (xact && + ((xact->_state == item_t::CLEARED && post->_state != item_t::CLEARED) || + (xact->_state == item_t::PENDING && post->_state == item_t::UNCLEARED))) + post->set_state(xact->_state); + + // Parse the account name + + if (! *p) + throw parse_error(_("Posting has no account")); + + char * next = next_element(p, true); + char * e = p + std::strlen(p); + + while (e > p && std::isspace(*(e - 1))) + e--; + + if ((*p == '[' && *(e - 1) == ']') || (*p == '(' && *(e - 1) == ')')) { + post->add_flags(POST_VIRTUAL); + DEBUG("textual.parse", "line " << linenum << ": " + << "Parsed a virtual account name"); + + if (*p == '[') { + post->add_flags(POST_MUST_BALANCE); + DEBUG("textual.parse", "line " << linenum << ": " + << "Posting must balance"); + } + p++; e--; + } + + string name(p, e - p); + DEBUG("textual.parse", "line " << 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 (honor_strict && strict && ! post->account->has_flags(ACCOUNT_KNOWN)) { + if (post->_state == item_t::UNCLEARED) + warning_(_("\"%1\", line %2: Unknown account '%3'") + << pathname << linenum << post->account->fullname()); + post->account->add_flags(ACCOUNT_KNOWN); + } + + // Parse the optional amount + + bool saw_amount = false; + + if (next && *next && (*next != ';' && *next != '=')) { + saw_amount = true; + + beg = next - line; + ptristream stream(next, len - beg); + + if (*next != '(') // indicates a value expression + post->amount.parse(stream, PARSE_NO_REDUCE); + else + parse_amount_expr(scope, stream, post->amount, &post->amount_expr, + post.get(), PARSE_NO_REDUCE | PARSE_SINGLE | + PARSE_NO_ASSIGN, defer_expr); + + if (! post->amount.is_null() && post->amount.has_commodity()) { + if (honor_strict && strict && + ! post->amount.commodity().has_flags(COMMODITY_KNOWN)) { + if (post->_state == item_t::UNCLEARED) + warning_(_("\"%1\", line %2: Unknown commodity '%3'") + << pathname << linenum << post->amount.commodity()); + post->amount.commodity().add_flags(COMMODITY_KNOWN); + } + + if (! post->amount.has_annotation()) { + foreach (state_t& state, 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; + } + } + } + } + } + + DEBUG("textual.parse", "line " << linenum << ": " + << "post amount = " << post->amount); + + if (stream.eof()) { + next = NULL; + } else { + next = skip_ws(next + static_cast<std::ptrdiff_t>(stream.tellg())); + + // Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST) + + if (*next == '@') { + DEBUG("textual.parse", "line " << linenum << ": " + << "Found a price indicator"); + + bool per_unit = true; + + if (*++next == '@') { + per_unit = false; + DEBUG("textual.parse", "line " << linenum << ": " + << "And it's for a total price"); + } + + beg = ++next - line; + + p = skip_ws(next); + if (*p) { + post->cost = amount_t(); + + beg = p - line; + ptristream cstream(p, len - beg); + + if (*p != '(') // indicates a value expression + post->cost->parse(cstream, PARSE_NO_MIGRATE); + else + parse_amount_expr(scope, cstream, *post->cost, NULL, post.get(), + PARSE_NO_MIGRATE | PARSE_SINGLE | PARSE_NO_ASSIGN, + defer_expr); + + if (post->cost->sign() < 0) + throw parse_error(_("A posting's cost may not be negative")); + + post->cost->in_place_unround(); + + if (per_unit) { + // For the sole case where the cost might be uncommoditized, + // guarantee that the commodity of the cost after multiplication + // is the same as it was before. + commodity_t& cost_commodity(post->cost->commodity()); + *post->cost *= post->amount; + post->cost->set_commodity(cost_commodity); + } + + DEBUG("textual.parse", "line " << linenum << ": " + << "Total cost is " << *post->cost); + DEBUG("textual.parse", "line " << linenum << ": " + << "Annotated amount is " << post->amount); + + if (cstream.eof()) + next = NULL; + else + next = skip_ws(p + static_cast<std::ptrdiff_t>(cstream.tellg())); + } else { + throw parse_error(_("Expected a cost amount")); + } + } + } + } + + // Parse the optional balance assignment + + if (xact && next && *next == '=') { + DEBUG("textual.parse", "line " << linenum << ": " + << "Found a balance assignment indicator"); + + beg = ++next - line; + + p = skip_ws(next); + if (*p) { + post->assigned_amount = amount_t(); + + beg = p - line; + ptristream stream(p, len - beg); + + if (*p != '(') // indicates a value expression + post->assigned_amount->parse(stream, PARSE_NO_MIGRATE); + else + parse_amount_expr(scope, stream, *post->assigned_amount, NULL, + post.get(), PARSE_SINGLE | PARSE_NO_MIGRATE, + defer_expr); + + if (post->assigned_amount->is_null()) { + if (post->amount.is_null()) + throw parse_error(_("Balance assignment must evaluate to a constant")); + else + throw parse_error(_("Balance assertion must evaluate to a constant")); + } + + DEBUG("textual.parse", "line " << linenum << ": " + << "POST assign: parsed amt = " << *post->assigned_amount); + + amount_t& amt(*post->assigned_amount); + value_t account_total + (post->account->amount(false).strip_annotations(keep_details_t())); + + DEBUG("post.assign", + "line " << linenum << ": " "account balance = " << account_total); + DEBUG("post.assign", + "line " << linenum << ": " "post amount = " << amt); + + amount_t diff = amt; + + switch (account_total.type()) { + case value_t::AMOUNT: + diff -= account_total.as_amount(); + break; + + case value_t::BALANCE: + if (optional<amount_t> comm_bal = + account_total.as_balance().commodity_amount(amt.commodity())) + diff -= *comm_bal; + break; + + default: + break; + } + + DEBUG("post.assign", + "line " << linenum << ": " << "diff = " << diff); + DEBUG("textual.parse", + "line " << linenum << ": " << "POST assign: diff = " << diff); + + if (! diff.is_zero()) { + if (! post->amount.is_null()) { + diff -= post->amount; + if (! diff.is_zero()) + throw_(parse_error, _("Balance assertion off by %1") << diff); + } else { + post->amount = diff; + DEBUG("textual.parse", "line " << linenum << ": " + << "Overwrite null posting"); + } + } + + if (stream.eof()) + next = NULL; + else + next = skip_ws(p + static_cast<std::ptrdiff_t>(stream.tellg())); + } else { + throw parse_error(_("Expected an balance assignment/assertion amount")); + } + } + + // Parse the optional note + + if (next && *next == ';') { + post->append_note(++next, current_year); + next = line + len; + DEBUG("textual.parse", "line " << linenum << ": " + << "Parsed a posting note"); + } + + // There should be nothing more to read + + if (next && *next) + throw_(parse_error, + _("Unexpected char '%1' (Note: inline math requires parentheses)") + << *next); + + post->pos->end_pos = curr_pos; + post->pos->end_line = linenum; + + if (! state_stack.empty()) { + foreach (const state_t& state, state_stack) + if (state.type() == typeid(string)) + post->parse_tags(boost::get<string>(state).c_str()); + } + + TRACE_STOP(post_details, 1); + + return post.release(); + + } + catch (const std::exception& err) { + add_error_context(_("While parsing posting:")); + add_error_context(line_context(buf, beg, len)); + throw; + } +} + +bool instance_t::parse_posts(account_t * account, + xact_base_t& xact, + const bool defer_expr) +{ + TRACE_START(xact_posts, 1, "Time spent parsing postings:"); + + bool added = false; + + 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, /* honor_strict= */ false, + defer_expr)) { + xact.add_post(post); + added = true; + } + } + + TRACE_STOP(xact_posts, 1); + + return added; +} + +xact_t * instance_t::parse_xact(char * line, + std::streamsize len, + account_t * account) +{ + TRACE_START(xact_text, 1, "Time spent parsing transaction text:"); + + std::auto_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; + + bool reveal_context = true; + + try { + + // Parse the date + + char * next = next_element(line); + + if (char * p = std::strchr(line, '=')) { + *p++ = '\0'; + xact->_date_eff = parse_date(p, current_year); + } + xact->_date = parse_date(line, current_year); + + // Parse the optional cleared flag: * + + if (next) { + switch (*next) { + case '*': + xact->_state = item_t::CLEARED; + next = skip_ws(++next); + break; + case '!': + xact->_state = item_t::PENDING; + next = skip_ws(++next); + break; + } + } + + // Parse the optional code: (TEXT) + + if (next && *next == '(') { + if (char * p = std::strchr(next++, ')')) { + *p++ = '\0'; + xact->code = next; + next = skip_ws(p); + } + } + + // Parse the description text + + if (next && *next) { + char * p = next_element(next, true); + xact->payee = next; + next = p; + } else { + xact->payee = _("<Unspecified payee>"); + } + + // Parse the xact note + + if (next && *next == ';') + xact->append_note(++next, current_year); + + TRACE_STOP(xact_text, 1); + + // Parse all of the posts associated with this xact + + TRACE_START(xact_details, 1, "Time spent parsing transaction details:"); + + post_t * last_post = NULL; + + while (peek_whitespace_line()) { + len = read_line(line); + + char * p = skip_ws(line); + if (! *p) + break; + + if (*p == ';') { + item_t * item; + if (last_post) + item = last_post; + else + item = xact.get(); + + // This is a trailing note, and possibly a metadata info tag + item->append_note(p + 1, current_year); + item->pos->end_pos = curr_pos; + item->pos->end_line++; + } else { + reveal_context = false; + + if (post_t * post = + parse_post(p, len - (p - line), account, xact.get())) { + xact->add_post(post); + last_post = post; + } + } + } + + if (xact->_state == item_t::UNCLEARED) { + item_t::state_t result = item_t::CLEARED; + + foreach (post_t * post, xact->posts) { + if (post->_state == item_t::UNCLEARED) { + result = item_t::UNCLEARED; + break; + } + else if (post->_state == item_t::PENDING) { + result = item_t::PENDING; + } + } + } + + xact->pos->end_pos = curr_pos; + xact->pos->end_line = linenum; + + if (! state_stack.empty()) { + foreach (const state_t& state, state_stack) + if (state.type() == typeid(string)) + xact->parse_tags(boost::get<string>(state).c_str()); + } + + TRACE_STOP(xact_details, 1); + + return xact.release(); + + } + catch (const std::exception& err) { + if (reveal_context) { + add_error_context(_("While parsing transaction:")); + add_error_context(source_context(xact->pos->pathname, + xact->pos->beg_pos, curr_pos, "> ")); + } + throw; + } +} + +expr_t::ptr_op_t instance_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + return scope.lookup(kind, name); +} + +std::size_t journal_t::parse(std::istream& in, + scope_t& scope, + account_t * master, + const path * original_file, + bool strict) +{ + TRACE_START(parsing_total, 1, "Total time spent parsing text:"); + + std::list<instance_t::state_t> state_stack; +#if defined(TIMELOG_SUPPORT) + time_log_t timelog(*this); +#endif + + instance_t parsing_instance(state_stack, +#if defined(TIMELOG_SUPPORT) + timelog, +#endif + in, scope, *this, master, + original_file, strict); + parsing_instance.parse(); + + TRACE_STOP(parsing_total, 1); + + // These tracers were started in textual.cc + TRACE_FINISH(xact_text, 1); + TRACE_FINISH(xact_details, 1); + TRACE_FINISH(xact_posts, 1); + TRACE_FINISH(xacts, 1); + TRACE_FINISH(instance_parse, 1); // report per-instance timers + TRACE_FINISH(parsing_total, 1); + + if (parsing_instance.errors > 0) + throw static_cast<int>(parsing_instance.errors); + + return parsing_instance.count; +} + +} // namespace ledger diff --git a/src/timelog.cc b/src/timelog.cc new file mode 100644 index 00000000..25bf8e94 --- /dev/null +++ b/src/timelog.cc @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2003-2009, 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 "timelog.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "journal.h" + +namespace ledger { + +namespace { + void clock_out_from_timelog(std::list<time_xact_t>& time_xacts, + time_xact_t out_event, + journal_t& journal) + { + time_xact_t event; + + if (time_xacts.size() == 1) { + event = time_xacts.back(); + time_xacts.clear(); + } + else if (time_xacts.empty()) { + throw parse_error(_("Timelog check-out event without a check-in")); + } + else if (! out_event.account) { + throw parse_error + (_("When multiple check-ins are active, checking out requires an account")); + } + else { + bool found = false; + + for (std::list<time_xact_t>::iterator i = time_xacts.begin(); + i != time_xacts.end(); + i++) + if (out_event.account == (*i).account) { + event = *i; + found = true; + time_xacts.erase(i); + break; + } + + if (! found) + throw parse_error + (_("Timelog check-out event does not match any current check-ins")); + } + + if (out_event.checkin < event.checkin) + throw parse_error + (_("Timelog check-out date less than corresponding check-in")); + + if (! out_event.desc.empty() && event.desc.empty()) { + event.desc = out_event.desc; + out_event.desc = empty_string; + } + + 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()); + + 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(); + } +} // unnamed namespace + +time_log_t::~time_log_t() +{ + TRACE_DTOR(time_log_t); + + if (! time_xacts.empty()) { + std::list<account_t *> accounts; + + foreach (time_xact_t& time_xact, time_xacts) + accounts.push_back(time_xact.account); + + foreach (account_t * account, accounts) + clock_out_from_timelog(time_xacts, + time_xact_t(none, CURRENT_TIME(), account), + journal); + + assert(time_xacts.empty()); + } +} + +void time_log_t::clock_in(time_xact_t event) +{ + if (! time_xacts.empty()) { + foreach (time_xact_t& time_xact, time_xacts) { + if (event.account == time_xact.account) + throw parse_error(_("Cannot double check-in to the same account")); + } + } + + time_xacts.push_back(event); +} + +void 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); +} + +} // namespace ledger diff --git a/src/timelog.h b/src/timelog.h new file mode 100644 index 00000000..83256dfa --- /dev/null +++ b/src/timelog.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2003-2009, 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 timelog.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _TIMELOG_H +#define _TIMELOG_H + +#include "utils.h" +#include "times.h" +#include "item.h" + +namespace ledger { + +class account_t; +class journal_t; + +class time_xact_t +{ +public: + datetime_t checkin; + account_t * account; + string desc; + string note; + position_t position; + + time_xact_t() : account(NULL) { + TRACE_CTOR(time_xact_t, ""); + } + time_xact_t(const optional<position_t>& _position, + const datetime_t& _checkin, + account_t * _account = NULL, + const string& _desc = "", + const string& _note = "") + : checkin(_checkin), account(_account), desc(_desc), note(_note), + position(_position ? *_position : position_t()) { + TRACE_CTOR(time_xact_t, + "position_t, datetime_t, account_t *, string, string"); + } + time_xact_t(const time_xact_t& xact) + : checkin(xact.checkin), account(xact.account), + desc(xact.desc), note(xact.note), position(xact.position) { + TRACE_CTOR(time_xact_t, "copy"); + } + ~time_xact_t() throw() { + TRACE_DTOR(time_xact_t); + } +}; + +class time_log_t +{ + std::list<time_xact_t> time_xacts; + journal_t& journal; + +public: + time_log_t(journal_t& _journal) : journal(_journal) { + TRACE_CTOR(time_log_t, "journal_t&"); + } + ~time_log_t(); + + void clock_in(time_xact_t event); + void clock_out(time_xact_t event); +}; + +} // namespace ledger + +#endif // _TIMELOG_H diff --git a/src/times.cc b/src/times.cc new file mode 100644 index 00000000..d4317509 --- /dev/null +++ b/src/times.cc @@ -0,0 +1,1587 @@ +/* + * Copyright (c) 2003-2009, 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 "times.h" + +namespace ledger { + +optional<datetime_t> epoch; + +date_time::weekdays start_of_week = gregorian::Sunday; + +//#define USE_BOOST_FACETS 1 + +namespace { + template <typename T, typename InputFacetType, typename OutputFacetType> + class temporal_io_t : public noncopyable + { + const char * fmt_str; +#if defined(USE_BOOST_FACETS) + std::istringstream input_stream; + std::ostringstream output_stream; + InputFacetType * input_facet; + OutputFacetType * output_facet; + std::string temp_string; +#endif // USE_BOOST_FACETS + + public: + date_traits_t traits; + bool input; + + temporal_io_t(const char * _fmt_str, bool _input) + : fmt_str(_fmt_str), + traits(icontains(fmt_str, "%y"), + icontains(fmt_str, "%m") || icontains(fmt_str, "%b"), + icontains(fmt_str, "%d")), + input(_input) { +#if defined(USE_BOOST_FACETS) + if (input) { + input_facet = new InputFacetType(fmt_str); + input_stream.imbue(std::locale(std::locale::classic(), input_facet)); + } else { + output_facet = new OutputFacetType(fmt_str); + output_stream.imbue(std::locale(std::locale::classic(), output_facet)); + } +#endif // USE_BOOST_FACETS + } + + void set_format(const char * fmt) { + fmt_str = fmt; + traits = date_traits_t(icontains(fmt_str, "%y"), + icontains(fmt_str, "%m") || + icontains(fmt_str, "%b"), + icontains(fmt_str, "%d")); +#if defined(USE_BOOST_FACETS) + if (input) + input_facet->format(fmt_str); + else + output_facet->format(fmt_str); +#endif // USE_BOOST_FACETS + } + + T parse(const char * str) { + } + + std::string format(const T& when) { +#if defined(USE_BOOST_FACETS) + output_stream.str(temp_string); + output_stream.seekp(std::ios_base::beg); + output_stream.clear(); + output_stream << when; + return output_stream.str(); +#else // USE_BOOST_FACETS + std::tm data(to_tm(when)); + char buf[128]; + std::strftime(buf, 127, fmt_str, &data); + return buf; +#endif // USE_BOOST_FACETS + } + }; + + template <> + datetime_t temporal_io_t<datetime_t, posix_time::time_input_facet, + posix_time::time_facet> + ::parse(const char * str) + { +#if defined(USE_BOOST_FACETS) + input_stream.seekg(std::ios_base::beg); + input_stream.clear(); + input_stream.str(str); + + datetime_t when; + input_stream >> when; +#if defined(DEBUG_ON) + if (when.is_not_a_date_time()) + DEBUG("times.parse", "Failed to parse date/time '" << str + << "' using pattern '" << fmt_str << "'"); +#endif + + if (! when.is_not_a_date_time() && + input_stream.good() && ! input_stream.eof() && + input_stream.peek() != EOF) { + DEBUG("times.parse", "This string has leftovers: '" << str << "'"); + return datetime_t(); + } + return when; +#else // USE_BOOST_FACETS + std::tm data; + std::memset(&data, 0, sizeof(std::tm)); + if (strptime(str, fmt_str, &data)) + return posix_time::ptime_from_tm(data); + else + return datetime_t(); +#endif // USE_BOOST_FACETS + } + + template <> + date_t temporal_io_t<date_t, gregorian::date_input_facet, + gregorian::date_facet> + ::parse(const char * str) + { +#if defined(USE_BOOST_FACETS) + input_stream.seekg(std::ios_base::beg); + input_stream.clear(); + input_stream.str(str); + + date_t when; + input_stream >> when; +#if defined(DEBUG_ON) + if (when.is_not_a_date()) + DEBUG("times.parse", "Failed to parse date '" << str + << "' using pattern '" << fmt_str << "'"); +#endif + + if (! when.is_not_a_date() && + input_stream.good() && ! input_stream.eof() && + input_stream.peek() != EOF) { + DEBUG("times.parse", "This string has leftovers: '" << str << "'"); + return date_t(); + } + return when; +#else // USE_BOOST_FACETS + std::tm data; + std::memset(&data, 0, sizeof(std::tm)); + data.tm_mday = 1; // some formats have no day + if (strptime(str, fmt_str, &data)) + return gregorian::date_from_tm(data); + else + return date_t(); +#endif // USE_BOOST_FACETS + } + + typedef temporal_io_t<datetime_t, posix_time::time_input_facet, + posix_time::time_facet> datetime_io_t; + typedef temporal_io_t<date_t, gregorian::date_input_facet, + gregorian::date_facet> date_io_t; + + shared_ptr<datetime_io_t> input_datetime_io; + shared_ptr<date_io_t> input_date_io; + shared_ptr<datetime_io_t> written_datetime_io; + shared_ptr<date_io_t> written_date_io; + shared_ptr<datetime_io_t> printed_datetime_io; + shared_ptr<date_io_t> printed_date_io; + + std::vector<shared_ptr<date_io_t> > readers; + + date_t parse_date_mask_routine(const char * date_str, date_io_t& io, + optional_year year, + date_traits_t * traits = NULL) + { + date_t when; + + if (std::strchr(date_str, '/')) { + when = io.parse(date_str); + } else { + char buf[128]; + VERIFY(std::strlen(date_str) < 127); + std::strcpy(buf, date_str); + + for (char * p = buf; *p; p++) + if (*p == '.' || *p == '-') + *p = '/'; + + when = io.parse(buf); + } + + if (! when.is_not_a_date()) { + DEBUG("times.parse", "Parsed date string: " << date_str); + DEBUG("times.parse", "Parsed result is: " << when); + + if (traits) + *traits = io.traits; + + if (! io.traits.has_year) { + when = date_t(year ? *year : CURRENT_DATE().year(), + when.month(), when.day()); + + if (when.month() > CURRENT_DATE().month()) + when -= gregorian::years(1); + } + } + return when; + } + + date_t parse_date_mask(const char * date_str, optional_year year, + date_traits_t * traits = NULL) + { + if (input_date_io.get()) { + date_t when = parse_date_mask_routine(date_str, *input_date_io.get(), + year, traits); + if (! when.is_not_a_date()) + return when; + } + + foreach (shared_ptr<date_io_t>& reader, readers) { + date_t when = parse_date_mask_routine(date_str, *reader.get(), + year, traits); + if (! when.is_not_a_date()) + return when; + } + + throw_(date_error, _("Invalid date: %1") << date_str); + return date_t(); + } +} + +optional<date_time::weekdays> string_to_day_of_week(const std::string& str) +{ + if (str == _("sun") || str == _("sunday") || str == "0") + return gregorian::Sunday; + else if (str == _("mon") || str == _("monday") || str == "1") + return gregorian::Monday; + else if (str == _("tue") || str == _("tuesday") || str == "2") + return gregorian::Tuesday; + else if (str == _("wed") || str == _("wednesday") || str == "3") + return gregorian::Wednesday; + else if (str == _("thu") || str == _("thursday") || str == "4") + return gregorian::Thursday; + else if (str == _("fri") || str == _("friday") || str == "5") + return gregorian::Friday; + else if (str == _("sat") || str == _("saturday") || str == "6") + return gregorian::Saturday; + else + return none; +} + +optional<date_time::months_of_year> +string_to_month_of_year(const std::string& str) +{ + if (str == _("jan") || str == _("january") || str == "0") + return gregorian::Jan; + else if (str == _("feb") || str == _("february") || str == "1") + return gregorian::Feb; + else if (str == _("mar") || str == _("march") || str == "2") + return gregorian::Mar; + else if (str == _("apr") || str == _("april") || str == "3") + return gregorian::Apr; + else if (str == _("may") || str == _("may") || str == "4") + return gregorian::May; + else if (str == _("jun") || str == _("june") || str == "5") + return gregorian::Jun; + else if (str == _("jul") || str == _("july") || str == "6") + return gregorian::Jul; + else if (str == _("aug") || str == _("august") || str == "7") + return gregorian::Aug; + else if (str == _("sep") || str == _("september") || str == "8") + return gregorian::Sep; + else if (str == _("oct") || str == _("october") || str == "9") + return gregorian::Oct; + else if (str == _("nov") || str == _("november") || str == "10") + return gregorian::Nov; + else if (str == _("dec") || str == _("december") || str == "11") + return gregorian::Dec; + else + return none; +} + +datetime_t parse_datetime(const char * str, optional_year) +{ + datetime_t when = input_datetime_io->parse(str); + if (when.is_not_a_date_time()) + throw_(date_error, _("Invalid date/time: %1") << str); + return when; +} + +date_t parse_date(const char * str, optional_year current_year) +{ + return parse_date_mask(str, current_year); +} + +date_t date_specifier_t::begin(const optional_year& current_year) const +{ + assert(year || current_year); + + year_type the_year = year ? *year : static_cast<year_type>(*current_year); + month_type the_month = month ? *month : date_t::month_type(1); + day_type the_day = day ? *day : date_t::day_type(1); + + if (day) + assert(! wday); + else if (wday) + assert(! day); + + // jww (2009-11-16): Handle wday. If a month is set, find the most recent + // wday in that month; if the year is set, then in that year. + + return gregorian::date(static_cast<date_t::year_type>(the_year), + static_cast<date_t::month_type>(the_month), + static_cast<date_t::day_type>(the_day)); +} + +date_t date_specifier_t::end(const optional_year& current_year) const +{ + if (day || wday) + return begin(current_year) + gregorian::days(1); + else if (month) + return begin(current_year) + gregorian::months(1); + else if (year) + return begin(current_year) + gregorian::years(1); + else { + assert(false); + return date_t(); + } +} + +std::ostream& operator<<(std::ostream& out, + const date_duration_t& duration) +{ + if (duration.quantum == date_duration_t::DAYS) + out << duration.length << " day(s)"; + else if (duration.quantum == date_duration_t::WEEKS) + out << duration.length << " week(s)"; + else if (duration.quantum == date_duration_t::MONTHS) + out << duration.length << " month(s)"; + else if (duration.quantum == date_duration_t::QUARTERS) + out << duration.length << " quarter(s)"; + else { + assert(duration.quantum == date_duration_t::YEARS); + out << duration.length << " year(s)"; + } + return out; +} + +class date_parser_t +{ + friend void show_period_tokens(std::ostream& out, const string& arg); + + class lexer_t + { + friend class date_parser_t; + + string::const_iterator begin; + string::const_iterator end; + + public: + struct token_t + { + enum kind_t { + UNKNOWN, + + TOK_DATE, + TOK_INT, + TOK_SLASH, + TOK_DASH, + TOK_DOT, + + TOK_A_YEAR, + TOK_A_MONTH, + TOK_A_WDAY, + + TOK_SINCE, + TOK_UNTIL, + TOK_IN, + TOK_THIS, + TOK_NEXT, + TOK_LAST, + TOK_EVERY, + + TOK_TODAY, + TOK_TOMORROW, + TOK_YESTERDAY, + + TOK_YEAR, + TOK_QUARTER, + TOK_MONTH, + TOK_WEEK, + TOK_DAY, + + TOK_YEARLY, + TOK_QUARTERLY, + TOK_BIMONTHLY, + TOK_MONTHLY, + TOK_BIWEEKLY, + TOK_WEEKLY, + TOK_DAILY, + + TOK_YEARS, + TOK_QUARTERS, + TOK_MONTHS, + TOK_WEEKS, + TOK_DAYS, + + END_REACHED + + } kind; + + typedef variant<unsigned short, + string, + date_specifier_t::year_type, + date_time::months_of_year, + date_time::weekdays, + date_specifier_t> content_t; + + optional<content_t> value; + + explicit token_t(kind_t _kind = UNKNOWN, + const optional<content_t>& _value = + content_t(empty_string)) + : kind(_kind), value(_value) { + TRACE_CTOR(date_parser_t::lexer_t::token_t, ""); + } + token_t(const token_t& tok) + : kind(tok.kind), value(tok.value) { + TRACE_CTOR(date_parser_t::lexer_t::token_t, "copy"); + } + ~token_t() throw() { + TRACE_DTOR(date_parser_t::lexer_t::token_t); + } + + token_t& operator=(const token_t& tok) { + if (this != &tok) { + kind = tok.kind; + value = tok.value; + } + return *this; + } + + operator bool() const { + return kind != END_REACHED; + } + + string to_string() const { + std::ostringstream out; + + switch (kind) { + case UNKNOWN: + out << boost::get<string>(*value); + break; + case TOK_DATE: + return boost::get<date_specifier_t>(*value).to_string(); + case TOK_INT: + out << boost::get<unsigned short>(*value); + break; + case TOK_SLASH: return "/"; + case TOK_DASH: return "-"; + case TOK_DOT: return "."; + case TOK_A_YEAR: + out << boost::get<date_specifier_t::year_type>(*value); + break; + case TOK_A_MONTH: + out << date_specifier_t::month_type + (boost::get<date_time::months_of_year>(*value)); + break; + case TOK_A_WDAY: + out << date_specifier_t::day_of_week_type + (boost::get<date_time::weekdays>(*value)); + break; + case TOK_SINCE: return "since"; + case TOK_UNTIL: return "until"; + case TOK_IN: return "in"; + case TOK_THIS: return "this"; + case TOK_NEXT: return "next"; + case TOK_LAST: return "last"; + case TOK_EVERY: return "every"; + case TOK_TODAY: return "today"; + case TOK_TOMORROW: return "tomorrow"; + case TOK_YESTERDAY: return "yesterday"; + case TOK_YEAR: return "year"; + case TOK_QUARTER: return "quarter"; + case TOK_MONTH: return "month"; + case TOK_WEEK: return "week"; + case TOK_DAY: return "day"; + case TOK_YEARLY: return "yearly"; + case TOK_QUARTERLY: return "quarterly"; + case TOK_BIMONTHLY: return "bimonthly"; + case TOK_MONTHLY: return "monthly"; + case TOK_BIWEEKLY: return "biweekly"; + case TOK_WEEKLY: return "weekly"; + case TOK_DAILY: return "daily"; + case TOK_YEARS: return "years"; + case TOK_QUARTERS: return "quarters"; + case TOK_MONTHS: return "months"; + case TOK_WEEKS: return "weeks"; + case TOK_DAYS: return "days"; + case END_REACHED: return "<EOF>"; + default: + assert(false); + return empty_string; + } + + return out.str(); + } + + void dump(std::ostream& out) const { + switch (kind) { + case UNKNOWN: out << "UNKNOWN"; break; + case TOK_DATE: out << "TOK_DATE"; break; + case TOK_INT: out << "TOK_INT"; break; + case TOK_SLASH: out << "TOK_SLASH"; break; + case TOK_DASH: out << "TOK_DASH"; break; + case TOK_DOT: out << "TOK_DOT"; break; + case TOK_A_YEAR: out << "TOK_A_YEAR"; break; + case TOK_A_MONTH: out << "TOK_A_MONTH"; break; + case TOK_A_WDAY: out << "TOK_A_WDAY"; break; + case TOK_SINCE: out << "TOK_SINCE"; break; + case TOK_UNTIL: out << "TOK_UNTIL"; break; + case TOK_IN: out << "TOK_IN"; break; + case TOK_THIS: out << "TOK_THIS"; break; + case TOK_NEXT: out << "TOK_NEXT"; break; + case TOK_LAST: out << "TOK_LAST"; break; + case TOK_EVERY: out << "TOK_EVERY"; break; + case TOK_TODAY: out << "TOK_TODAY"; break; + case TOK_TOMORROW: out << "TOK_TOMORROW"; break; + case TOK_YESTERDAY: out << "TOK_YESTERDAY"; break; + case TOK_YEAR: out << "TOK_YEAR"; break; + case TOK_QUARTER: out << "TOK_QUARTER"; break; + case TOK_MONTH: out << "TOK_MONTH"; break; + case TOK_WEEK: out << "TOK_WEEK"; break; + case TOK_DAY: out << "TOK_DAY"; break; + case TOK_YEARLY: out << "TOK_YEARLY"; break; + case TOK_QUARTERLY: out << "TOK_QUARTERLY"; break; + case TOK_BIMONTHLY: out << "TOK_BIMONTHLY"; break; + case TOK_MONTHLY: out << "TOK_MONTHLY"; break; + case TOK_BIWEEKLY: out << "TOK_BIWEEKLY"; break; + case TOK_WEEKLY: out << "TOK_WEEKLY"; break; + case TOK_DAILY: out << "TOK_DAILY"; break; + case TOK_YEARS: out << "TOK_YEARS"; break; + case TOK_QUARTERS: out << "TOK_QUARTERS"; break; + case TOK_MONTHS: out << "TOK_MONTHS"; break; + case TOK_WEEKS: out << "TOK_WEEKS"; break; + case TOK_DAYS: out << "TOK_DAYS"; break; + case END_REACHED: out << "END_REACHED"; break; + default: + assert(false); + break; + } + } + + void unexpected(); + static void expected(char wanted, char c = '\0'); + }; + + token_t token_cache; + + lexer_t(string::const_iterator _begin, + string::const_iterator _end) + : begin(_begin), end(_end) + { + TRACE_CTOR(date_parser_t::lexer_t, ""); + } + lexer_t(const lexer_t& lexer) + : begin(lexer.begin), end(lexer.end), + token_cache(lexer.token_cache) + { + TRACE_CTOR(date_parser_t::lexer_t, "copy"); + } + ~lexer_t() throw() { + TRACE_DTOR(date_parser_t::lexer_t); + } + + token_t next_token(); + void push_token(token_t tok) { + assert(token_cache.kind == token_t::UNKNOWN); + token_cache = tok; + } + token_t peek_token() { + if (token_cache.kind == token_t::UNKNOWN) + token_cache = next_token(); + return token_cache; + } + }; + + string arg; + lexer_t lexer; + +public: + date_parser_t(const string& _arg) + : arg(_arg), lexer(arg.begin(), arg.end()) { + TRACE_CTOR(date_parser_t, ""); + } + date_parser_t(const date_parser_t& parser) + : arg(parser.arg), lexer(parser.lexer) { + TRACE_CTOR(date_parser_t, "copy"); + } + ~date_parser_t() throw() { + TRACE_DTOR(date_parser_t); + } + + date_interval_t parse(); + +private: + void determine_when(lexer_t::token_t& tok, date_specifier_t& specifier); +}; + +void date_parser_t::determine_when(date_parser_t::lexer_t::token_t& tok, + date_specifier_t& specifier) +{ + switch (tok.kind) { + case lexer_t::token_t::TOK_DATE: + specifier = boost::get<date_specifier_t>(*tok.value); + break; + + case lexer_t::token_t::TOK_INT: + specifier.day = + date_specifier_t::day_type(boost::get<unsigned short>(*tok.value)); + break; + case lexer_t::token_t::TOK_A_YEAR: + specifier.year = boost::get<date_specifier_t::year_type>(*tok.value); + break; + case lexer_t::token_t::TOK_A_MONTH: + specifier.month = + date_specifier_t::month_type + (boost::get<date_time::months_of_year>(*tok.value)); + break; + case lexer_t::token_t::TOK_A_WDAY: + specifier.wday = + date_specifier_t::day_of_week_type + (boost::get<date_time::weekdays>(*tok.value)); + break; + + default: + tok.unexpected(); + break; + } +} + +date_interval_t date_parser_t::parse() +{ + optional<date_specifier_t> since_specifier; + optional<date_specifier_t> until_specifier; + optional<date_specifier_t> inclusion_specifier; + + date_interval_t period; + date_t today = CURRENT_DATE(); + bool end_inclusive = false; + + for (lexer_t::token_t tok = lexer.next_token(); + tok.kind != lexer_t::token_t::END_REACHED; + tok = lexer.next_token()) { + switch (tok.kind) { +#if 0 + case lexer_t::token_t::TOK_INT: + // jww (2009-11-18): NYI + assert(! "Need to allow for expressions like \"4 months ago\""); + tok.unexpected(); + break; +#endif + + case lexer_t::token_t::TOK_DATE: + if (! inclusion_specifier) + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + break; + + case lexer_t::token_t::TOK_INT: + if (! inclusion_specifier) + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + break; + + case lexer_t::token_t::TOK_A_YEAR: + if (! inclusion_specifier) + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + break; + + case lexer_t::token_t::TOK_A_MONTH: + if (! inclusion_specifier) + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + break; + + case lexer_t::token_t::TOK_A_WDAY: + if (! inclusion_specifier) + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + break; + + case lexer_t::token_t::TOK_DASH: + if (inclusion_specifier) { + since_specifier = inclusion_specifier; + until_specifier = date_specifier_t(); + inclusion_specifier = none; + + tok = lexer.next_token(); + determine_when(tok, *until_specifier); + + // The dash operator is special: it has an _inclusive_ end. + end_inclusive = true; + } else { + tok.unexpected(); + } + break; + + case lexer_t::token_t::TOK_SINCE: + if (since_specifier) { + tok.unexpected(); + } else { + since_specifier = date_specifier_t(); + tok = lexer.next_token(); + determine_when(tok, *since_specifier); + } + break; + + case lexer_t::token_t::TOK_UNTIL: + if (until_specifier) { + tok.unexpected(); + } else { + until_specifier = date_specifier_t(); + tok = lexer.next_token(); + determine_when(tok, *until_specifier); + } + break; + + case lexer_t::token_t::TOK_IN: + if (inclusion_specifier) { + tok.unexpected(); + } else { + inclusion_specifier = date_specifier_t(); + tok = lexer.next_token(); + determine_when(tok, *inclusion_specifier); + } + break; + + case lexer_t::token_t::TOK_THIS: + case lexer_t::token_t::TOK_NEXT: + case lexer_t::token_t::TOK_LAST: { + int8_t adjust = 0; + if (tok.kind == lexer_t::token_t::TOK_NEXT) + adjust = 1; + else if (tok.kind == lexer_t::token_t::TOK_LAST) + adjust = -1; + + tok = lexer.next_token(); + switch (tok.kind) { + case lexer_t::token_t::TOK_INT: + // jww (2009-11-18): Allow things like "last 5 weeks" + assert(! "Need to allow for expressions like \"last 5 weeks\""); + tok.unexpected(); + break; + + case lexer_t::token_t::TOK_A_MONTH: { + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + + date_t temp(today.year(), *inclusion_specifier->month, 1); + temp += gregorian::years(adjust); + inclusion_specifier = + date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()), + temp.month()); + break; + } + + case lexer_t::token_t::TOK_A_WDAY: { + inclusion_specifier = date_specifier_t(); + determine_when(tok, *inclusion_specifier); + + date_t temp = + date_duration_t::find_nearest(today, date_duration_t::WEEKS); + while (temp.day_of_week() != inclusion_specifier->wday) + temp += gregorian::days(1); + temp += gregorian::days(7 * adjust); + inclusion_specifier = date_specifier_t(temp); + break; + } + + case lexer_t::token_t::TOK_YEAR: { + date_t temp(today); + temp += gregorian::years(adjust); + inclusion_specifier = + date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year())); + break; + } + + case lexer_t::token_t::TOK_QUARTER: { + date_t temp = + date_duration_t::find_nearest(today, date_duration_t::QUARTERS); + temp += gregorian::months(3 * adjust); + inclusion_specifier = + date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()), + temp.month()); +#if 0 + period.duration = date_duration_t(date_duration_t::QUARTERS, 1); +#endif + break; + } + + case lexer_t::token_t::TOK_MONTH: { + date_t temp(today); + temp += gregorian::months(adjust); + inclusion_specifier = + date_specifier_t(static_cast<date_specifier_t::year_type>(temp.year()), + temp.month()); + break; + } + + case lexer_t::token_t::TOK_WEEK: { + date_t temp = + date_duration_t::find_nearest(today, date_duration_t::WEEKS); + temp += gregorian::days(7 * adjust); + inclusion_specifier = date_specifier_t(today); +#if 0 + period.duration = date_duration_t(date_duration_t::WEEKS, 1); +#endif + break; + } + + case lexer_t::token_t::TOK_DAY: { + date_t temp(today); + temp += gregorian::days(adjust); + inclusion_specifier = date_specifier_t(temp); + break; + } + + default: + tok.unexpected(); + break; + } + break; + } + + case lexer_t::token_t::TOK_TODAY: + inclusion_specifier = date_specifier_t(today); + break; + case lexer_t::token_t::TOK_TOMORROW: + inclusion_specifier = date_specifier_t(today + gregorian::days(1)); + break; + case lexer_t::token_t::TOK_YESTERDAY: + inclusion_specifier = date_specifier_t(today - gregorian::days(1)); + break; + + case lexer_t::token_t::TOK_EVERY: + tok = lexer.next_token(); + if (tok == lexer_t::token_t::TOK_INT) { + int quantity = boost::get<unsigned short>(*tok.value); + tok = lexer.next_token(); + switch (tok.kind) { + case lexer_t::token_t::TOK_YEARS: + period.duration = date_duration_t(date_duration_t::YEARS, quantity); + break; + case lexer_t::token_t::TOK_QUARTERS: + period.duration = date_duration_t(date_duration_t::QUARTERS, quantity); + break; + case lexer_t::token_t::TOK_MONTHS: + period.duration = date_duration_t(date_duration_t::MONTHS, quantity); + break; + case lexer_t::token_t::TOK_WEEKS: + period.duration = date_duration_t(date_duration_t::WEEKS, quantity); + break; + case lexer_t::token_t::TOK_DAYS: + period.duration = date_duration_t(date_duration_t::DAYS, quantity); + break; + default: + tok.unexpected(); + break; + } + } else { + switch (tok.kind) { + case lexer_t::token_t::TOK_YEAR: + period.duration = date_duration_t(date_duration_t::YEARS, 1); + break; + case lexer_t::token_t::TOK_QUARTER: + period.duration = date_duration_t(date_duration_t::QUARTERS, 1); + break; + case lexer_t::token_t::TOK_MONTH: + period.duration = date_duration_t(date_duration_t::MONTHS, 1); + break; + case lexer_t::token_t::TOK_WEEK: + period.duration = date_duration_t(date_duration_t::WEEKS, 1); + break; + case lexer_t::token_t::TOK_DAY: + period.duration = date_duration_t(date_duration_t::DAYS, 1); + break; + default: + tok.unexpected(); + break; + } + } + break; + + case lexer_t::token_t::TOK_YEARLY: + period.duration = date_duration_t(date_duration_t::YEARS, 1); + break; + case lexer_t::token_t::TOK_QUARTERLY: + period.duration = date_duration_t(date_duration_t::QUARTERS, 1); + break; + case lexer_t::token_t::TOK_BIMONTHLY: + period.duration = date_duration_t(date_duration_t::MONTHS, 2); + break; + case lexer_t::token_t::TOK_MONTHLY: + period.duration = date_duration_t(date_duration_t::MONTHS, 1); + break; + case lexer_t::token_t::TOK_BIWEEKLY: + period.duration = date_duration_t(date_duration_t::WEEKS, 2); + break; + case lexer_t::token_t::TOK_WEEKLY: + period.duration = date_duration_t(date_duration_t::WEEKS, 1); + break; + case lexer_t::token_t::TOK_DAILY: + period.duration = date_duration_t(date_duration_t::DAYS, 1); + break; + + default: + tok.unexpected(); + break; + } + } + +#if 0 + if (! period.duration && inclusion_specifier) + period.duration = inclusion_specifier->implied_duration(); +#endif + + if (since_specifier || until_specifier) { + date_range_t range(since_specifier, until_specifier); + range.end_inclusive = end_inclusive; + + period.range = date_specifier_or_range_t(range); + } + else if (inclusion_specifier) { + period.range = date_specifier_or_range_t(*inclusion_specifier); + } + else { + /* otherwise, it's something like "monthly", with no date reference */ + } + + return period; +} + +void date_interval_t::parse(const string& str) +{ + date_parser_t parser(str); + *this = parser.parse(); +} + +void date_interval_t::resolve_end() +{ + if (start && ! end_of_duration) { + end_of_duration = duration->add(*start); + DEBUG("times.interval", + "stabilize: end_of_duration = " << *end_of_duration); + } + + if (finish && *end_of_duration > *finish) { + end_of_duration = finish; + DEBUG("times.interval", + "stabilize: end_of_duration reset to end: " << *end_of_duration); + } + + if (start && ! next) { + next = end_of_duration; + DEBUG("times.interval", "stabilize: next set to: " << *next); + } +} + +date_t date_duration_t::find_nearest(const date_t& date, skip_quantum_t skip) +{ + date_t result; + + switch (skip) { + case date_duration_t::YEARS: + result = date_t(date.year(), gregorian::Jan, 1); + break; + case date_duration_t::QUARTERS: + result = date_t(date.year(), date.month(), 1); + while (result.month() != gregorian::Jan && + result.month() != gregorian::Apr && + result.month() != gregorian::Jul && + result.month() != gregorian::Oct) + result -= gregorian::months(1); + break; + case date_duration_t::MONTHS: + result = date_t(date.year(), date.month(), 1); + break; + case date_duration_t::WEEKS: + result = date; + while (result.day_of_week() != start_of_week) + result -= gregorian::days(1); + break; + case date_duration_t::DAYS: + result = date; + break; + default: + assert(false); + break; + } + return result; +} + +void date_interval_t::stabilize(const optional<date_t>& date) +{ +#if defined(DEBUG_ON) + if (date) + DEBUG("times.interval", "stabilize: with date = " << *date); +#endif + + if (date && ! aligned) { + DEBUG("times.interval", "stabilize: date passed, but not aligned"); + if (duration) { + DEBUG("times.interval", + "stabilize: aligning with a duration: " << *duration); + + // The interval object has not been seeded with a start date yet, so + // find the nearest period before on on date which fits, if possible. + // + // Find an efficient starting point for the upcoming while loop. We + // want a date early enough that the range will be correct, but late + // enough that we don't spend hundreds of thousands of loops skipping + // through time. + optional<date_t> initial_start = start ? start : begin(date->year()); + optional<date_t> initial_finish = finish ? finish : end(date->year()); + +#if defined(DEBUG_ON) + if (initial_start) + DEBUG("times.interval", + "stabilize: initial_start = " << *initial_start); + if (initial_finish) + DEBUG("times.interval", + "stabilize: initial_finish = " << *initial_finish); +#endif + + date_t when = start ? *start : *date; + + if (duration->quantum == date_duration_t::MONTHS || + duration->quantum == date_duration_t::QUARTERS || + duration->quantum == date_duration_t::YEARS) { + DEBUG("times.interval", + "stabilize: monthly, quarterly or yearly duration"); + start = date_duration_t::find_nearest(when, duration->quantum); + } else { + DEBUG("times.interval", "stabilize: daily or weekly duration"); + start = date_duration_t::find_nearest(when - gregorian::days(400), + duration->quantum); + } + + DEBUG("times.interval", + "stabilize: beginning start date = " << *start); + + while (*start < *date) { + date_interval_t next_interval(*this); + ++next_interval; + + if (next_interval.start && *next_interval.start < *date) { + *this = next_interval; + } else { + end_of_duration = none; + next = none; + break; + } + } + + DEBUG("times.interval", "stabilize: proposed start date = " << *start); + + if (initial_start && (! start || *start < *initial_start)) { + // Using the discovered start, find the end of the period + resolve_end(); + + start = initial_start; + DEBUG("times.interval", "stabilize: start reset to initial start"); + } + if (initial_finish && (! finish || *finish > *initial_finish)) { + finish = initial_finish; + DEBUG("times.interval", "stabilize: finish reset to initial finish"); + } + +#if defined(DEBUG_ON) + if (start) + DEBUG("times.interval", "stabilize: final start = " << *start); + if (finish) + DEBUG("times.interval", "stabilize: final finish = " << *finish); +#endif + } + else if (range) { + if (date) { + start = range->begin(date->year()); + finish = range->end(date->year()); + } else { + start = range->begin(); + finish = range->end(); + } + } + aligned = true; + } + + // If there is no duration, then if we've reached here the date falls + // between start and finish. + if (! duration) { + DEBUG("times.interval", "stabilize: there was no duration given"); + + if (! start && ! finish) + throw_(date_error, + _("Invalid date interval: neither start, nor finish, nor duration")); + } else { + resolve_end(); + } +} + +bool date_interval_t::find_period(const date_t& date) +{ + stabilize(date); + + if (finish && date > *finish) { + DEBUG("times.interval", + "false: date [" << date << "] > finish [" << *finish << "]"); + return false; + } + + if (! start) { + throw_(std::runtime_error, _("Date interval is improperly initialized")); + } + else if (date < *start) { + DEBUG("times.interval", + "false: date [" << date << "] < start [" << *start << "]"); + return false; + } + + if (end_of_duration) { + if (date < *end_of_duration) { + DEBUG("times.interval", + "true: date [" << date << "] < end_of_duration [" + << *end_of_duration << "]"); + return true; + } + } else { + DEBUG("times.interval", "false: there is no end_of_duration"); + return false; + } + + // If we've reached here, it means the date does not fall into the current + // interval, so we must seek another interval that does match -- unless we + // pass by date in so doing, which means we shouldn't alter the current + // period of the interval at all. + + date_t scan = *start; + date_t end_of_scan = *end_of_duration; + + DEBUG("times.interval", "date = " << date); + DEBUG("times.interval", "scan = " << scan); + DEBUG("times.interval", "end_of_scan = " << end_of_scan); + + while (date >= scan && (! finish || scan < *finish)) { + if (date < end_of_scan) { + start = scan; + end_of_duration = end_of_scan; + next = none; + + DEBUG("times.interval", "true: start = " << *start); + DEBUG("times.interval", "true: end_of_duration = " << *end_of_duration); + + return true; + } + + scan = duration->add(scan); + end_of_scan = duration->add(scan); + } + + return false; +} + +date_interval_t& date_interval_t::operator++() +{ + if (! start) + throw_(date_error, _("Cannot increment an unstarted date interval")); + + stabilize(); + + if (! duration) + throw_(date_error, + _("Cannot increment a date interval without a duration")); + + assert(next); + + if (finish && *next >= *finish) { + start = none; + } else { + start = *next; + end_of_duration = duration->add(*start); + } + next = none; + + resolve_end(); + + return *this; +} + +void date_interval_t::dump(std::ostream& out, optional_year current_year) +{ + out << _("--- Before stabilization ---") << std::endl; + + if (range) + out << _(" range: ") << range->to_string() << std::endl; + if (start) + out << _(" start: ") << format_date(*start) << std::endl; + if (finish) + out << _(" finish: ") << format_date(*finish) << std::endl; + + if (duration) + out << _("duration: ") << duration->to_string() << std::endl; + + stabilize(begin(current_year)); + + out << std::endl + << _("--- After stabilization ---") << std::endl; + + if (range) + out << _(" range: ") << range->to_string() << std::endl; + if (start) + out << _(" start: ") << format_date(*start) << std::endl; + if (finish) + out << _(" finish: ") << format_date(*finish) << std::endl; + + if (duration) + out << _("duration: ") << duration->to_string() << std::endl; + + out << std::endl + << _("--- Sample dates in range (max. 20) ---") << std::endl; + + date_t last_date; + + for (int i = 0; i < 20 && *this; ++i, ++*this) { + out << std::right; + out.width(2); + + if (! last_date.is_not_a_date() && last_date == *start) + break; + + out << (i + 1) << ": " << format_date(*start); + if (duration) + out << " -- " << format_date(*inclusive_end()); + out << std::endl; + + if (! duration) + break; + + last_date = *start; + } +} + +date_parser_t::lexer_t::token_t date_parser_t::lexer_t::next_token() +{ + if (token_cache.kind != token_t::UNKNOWN) { + token_t tok = token_cache; + token_cache = token_t(); + return tok; + } + + while (begin != end && std::isspace(*begin)) + begin++; + + if (begin == end) + return token_t(token_t::END_REACHED); + + switch (*begin) { + case '/': ++begin; return token_t(token_t::TOK_SLASH); + case '-': ++begin; return token_t(token_t::TOK_DASH); + case '.': ++begin; return token_t(token_t::TOK_DOT); + default: break; + } + + string::const_iterator start = begin; + + // If the first character is a digit, try parsing the whole argument as a + // date using the typical date formats. This allows not only dates like + // "2009/08/01", but also dates that fit the user's --input-date-format, + // assuming their format fits in one argument and begins with a digit. + if (std::isdigit(*begin)) { + try { + string::const_iterator i = begin; + for (i = begin; i != end && ! std::isspace(*i); i++) {} + assert(i != begin); + + string possible_date(start, i); + date_traits_t traits; + + date_t when = parse_date_mask(possible_date.c_str(), none, &traits); + if (! when.is_not_a_date()) { + begin = i; + return token_t(token_t::TOK_DATE, + token_t::content_t(date_specifier_t(when, traits))); + } + } + catch (...) {} + } + + start = begin; + + string term; + bool alnum = std::isalnum(*begin); + for (; (begin != end && ! std::isspace(*begin) && + ((alnum && static_cast<bool>(std::isalnum(*begin))) || + (! alnum && ! static_cast<bool>(std::isalnum(*begin))))); begin++) + term.push_back(*begin); + + if (! term.empty()) { + if (std::isdigit(term[0])) { + if (term.length() == 4) + return token_t(token_t::TOK_A_YEAR, + token_t::content_t + (lexical_cast<date_specifier_t::year_type>(term))); + else + return token_t(token_t::TOK_INT, + token_t::content_t(lexical_cast<unsigned short>(term))); + } + else if (std::isalpha(term[0])) { + to_lower(term); + + if (optional<date_time::months_of_year> month = + string_to_month_of_year(term)) { + return token_t(token_t::TOK_A_MONTH, token_t::content_t(*month)); + } + else if (optional<date_time::weekdays> wday = + string_to_day_of_week(term)) { + return token_t(token_t::TOK_A_WDAY, token_t::content_t(*wday)); + } + else if (term == _("from") || term == _("since")) + return token_t(token_t::TOK_SINCE); + else if (term == _("to") || term == _("until")) + return token_t(token_t::TOK_UNTIL); + else if (term == _("in")) + return token_t(token_t::TOK_IN); + else if (term == _("this")) + return token_t(token_t::TOK_THIS); + else if (term == _("next")) + return token_t(token_t::TOK_NEXT); + else if (term == _("last")) + return token_t(token_t::TOK_LAST); + else if (term == _("every")) + return token_t(token_t::TOK_EVERY); + else if (term == _("today")) + return token_t(token_t::TOK_TODAY); + else if (term == _("tomorrow")) + return token_t(token_t::TOK_TOMORROW); + else if (term == _("yesterday")) + return token_t(token_t::TOK_YESTERDAY); + else if (term == _("year")) + return token_t(token_t::TOK_YEAR); + else if (term == _("quarter")) + return token_t(token_t::TOK_QUARTER); + else if (term == _("month")) + return token_t(token_t::TOK_MONTH); + else if (term == _("week")) + return token_t(token_t::TOK_WEEK); + else if (term == _("day")) + return token_t(token_t::TOK_DAY); + else if (term == _("yearly")) + return token_t(token_t::TOK_YEARLY); + else if (term == _("quarterly")) + return token_t(token_t::TOK_QUARTERLY); + else if (term == _("bimonthly")) + return token_t(token_t::TOK_BIMONTHLY); + else if (term == _("monthly")) + return token_t(token_t::TOK_MONTHLY); + else if (term == _("biweekly")) + return token_t(token_t::TOK_BIWEEKLY); + else if (term == _("weekly")) + return token_t(token_t::TOK_WEEKLY); + else if (term == _("daily")) + return token_t(token_t::TOK_DAILY); + else if (term == _("years")) + return token_t(token_t::TOK_YEARS); + else if (term == _("quarters")) + return token_t(token_t::TOK_QUARTERS); + else if (term == _("months")) + return token_t(token_t::TOK_MONTHS); + else if (term == _("weeks")) + return token_t(token_t::TOK_WEEKS); + else if (term == _("days")) + return token_t(token_t::TOK_DAYS); + } + else { + token_t::expected('\0', term[0]); + begin = ++start; + } + } else { + token_t::expected('\0', *begin); + } + + return token_t(token_t::UNKNOWN, token_t::content_t(term)); +} + +void date_parser_t::lexer_t::token_t::unexpected() +{ + switch (kind) { + case END_REACHED: + kind = UNKNOWN; + throw_(date_error, _("Unexpected end of expression")); + default: { + string desc = to_string(); + kind = UNKNOWN; + throw_(date_error, _("Unexpected date period token '%1'") << desc); + } + } +} + +void date_parser_t::lexer_t::token_t::expected(char wanted, char c) +{ + if (c == '\0' || c == -1) { + if (wanted == '\0' || wanted == -1) + throw_(date_error, _("Unexpected end")); + else + throw_(date_error, _("Missing '%1'") << wanted); + } else { + if (wanted == '\0' || wanted == -1) + throw_(date_error, _("Invalid char '%1'") << c); + else + throw_(date_error, _("Invalid char '%1' (wanted '%2')") << c << wanted); + } +} + +namespace { + typedef std::map<std::string, datetime_io_t *> datetime_io_map; + typedef std::map<std::string, date_io_t *> date_io_map; + + datetime_io_map temp_datetime_io; + date_io_map temp_date_io; +} + +std::string format_datetime(const datetime_t& when, + const format_type_t format_type, + const optional<const char *>& format) +{ + if (format_type == FMT_WRITTEN) { + return written_datetime_io->format(when); + } + else if (format_type == FMT_CUSTOM || format) { + datetime_io_map::iterator i = temp_datetime_io.find(*format); + if (i != temp_datetime_io.end()) { + return (*i).second->format(when); + } else { + datetime_io_t * formatter = new datetime_io_t(*format, false); + temp_datetime_io.insert(datetime_io_map::value_type(*format, formatter)); + return formatter->format(when); + } + } + else if (format_type == FMT_PRINTED) { + return printed_datetime_io->format(when); + } + else { + assert(false); + return empty_string; + } +} + +std::string format_date(const date_t& when, + const format_type_t format_type, + const optional<const char *>& format) +{ + if (format_type == FMT_WRITTEN) { + return written_date_io->format(when); + } + else if (format_type == FMT_CUSTOM || format) { + date_io_map::iterator i = temp_date_io.find(*format); + if (i != temp_date_io.end()) { + return (*i).second->format(when); + } else { + date_io_t * formatter = new date_io_t(*format, false); + temp_date_io.insert(date_io_map::value_type(*format, formatter)); + return formatter->format(when); + } + } + else if (format_type == FMT_PRINTED) { + return printed_date_io->format(when); + } + else { + assert(false); + return empty_string; + } +} + +namespace { + bool is_initialized = false; +} + +void set_datetime_format(const char * format) +{ + printed_datetime_io->set_format(format); +} + +void set_date_format(const char * format) +{ + printed_date_io->set_format(format); +} + +void set_input_date_format(const char * format) +{ + input_date_io.reset(new date_io_t(format, true)); +} + +void times_initialize() +{ + if (! is_initialized) { + input_datetime_io.reset(new datetime_io_t("%Y/%m/%d %H:%M:%S", true)); + + written_datetime_io.reset(new datetime_io_t("%Y/%m/%d %H:%M:%S", false)); + written_date_io.reset(new date_io_t("%Y/%m/%d", false)); + + printed_datetime_io.reset(new datetime_io_t("%y-%b-%d %H:%M:%S", false)); + printed_date_io.reset(new date_io_t("%y-%b-%d", false)); + + readers.push_back(shared_ptr<date_io_t>(new date_io_t("%m/%d", true))); + readers.push_back(shared_ptr<date_io_t>(new date_io_t("%Y/%m/%d", true))); + readers.push_back(shared_ptr<date_io_t>(new date_io_t("%Y/%m", true))); + readers.push_back(shared_ptr<date_io_t>(new date_io_t("%y/%m/%d", true))); + + is_initialized = true; + } +} + +void times_shutdown() +{ + if (is_initialized) { + input_datetime_io.reset(); + input_date_io.reset(); + written_datetime_io.reset(); + written_date_io.reset(); + printed_datetime_io.reset(); + printed_date_io.reset(); + + readers.clear(); + + foreach (datetime_io_map::value_type& pair, temp_datetime_io) + checked_delete(pair.second); + temp_datetime_io.clear(); + + foreach (date_io_map::value_type& pair, temp_date_io) + checked_delete(pair.second); + temp_date_io.clear(); + + is_initialized = false; + } +} + +void show_period_tokens(std::ostream& out, const string& arg) +{ + date_parser_t::lexer_t lexer(arg.begin(), arg.end()); + + out << _("--- Period expression tokens ---") << std::endl; + + date_parser_t::lexer_t::token_t token; + do { + token = lexer.next_token(); + token.dump(out); + out << ": " << token.to_string() << std::endl; + } + while (token.kind != date_parser_t::lexer_t::token_t::END_REACHED); +} + +} // namespace ledger diff --git a/src/times.h b/src/times.h new file mode 100644 index 00000000..826937bb --- /dev/null +++ b/src/times.h @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup util + */ + +/** + * @file times.h + * @author John Wiegley + * + * @ingroup util + * + * @brief datetime_t and date_t objects + */ +#ifndef _TIMES_H +#define _TIMES_H + +#include "utils.h" + +namespace ledger { + +DECLARE_EXCEPTION(datetime_error, std::runtime_error); +DECLARE_EXCEPTION(date_error, std::runtime_error); + +typedef boost::posix_time::ptime datetime_t; +typedef datetime_t::time_duration_type time_duration_t; + +inline bool is_valid(const datetime_t& moment) { + return ! moment.is_not_a_date_time(); +} + +typedef boost::gregorian::date date_t; +typedef boost::gregorian::date_iterator date_iterator_t; + +inline bool is_valid(const date_t& moment) { + return ! moment.is_not_a_date(); +} + +extern optional<datetime_t> epoch; + +#ifdef BOOST_DATE_TIME_HAS_HIGH_PRECISION_CLOCK +#define TRUE_CURRENT_TIME() (boost::posix_time::microsec_clock::universal_time()) +#define CURRENT_TIME() (epoch ? *epoch : TRUE_CURRENT_TIME()) +#else +#define TRUE_CURRENT_TIME() (boost::posix_time::second_clock::universal_time()) +#define CURRENT_TIME() (epoch ? *epoch : TRUE_CURRENT_TIME()) +#endif +#define CURRENT_DATE() \ + (epoch ? epoch->date() : boost::gregorian::day_clock::universal_day()) + +extern date_time::weekdays start_of_week; + +optional<date_time::weekdays> +string_to_day_of_week(const std::string& str); +optional<date_time::months_of_year> +string_to_month_of_year(const std::string& str); + +typedef optional<date_t::year_type> optional_year; + +datetime_t parse_datetime(const char * str, optional_year current_year = none); + +inline datetime_t parse_datetime(const std::string& str, + optional_year current_year = none) { + return parse_datetime(str.c_str(), current_year); +} + +date_t parse_date(const char * str, optional_year current_year = none); + +inline date_t parse_date(const std::string& str, + optional_year current_year = none) { + return parse_date(str.c_str(), current_year); +} + +enum format_type_t { + FMT_WRITTEN, FMT_PRINTED, FMT_CUSTOM +}; + +std::string format_datetime(const datetime_t& when, + const format_type_t format_type = FMT_PRINTED, + const optional<const char *>& format = none); +void set_datetime_format(const char * format); + +std::string format_date(const date_t& when, + const format_type_t format_type = FMT_PRINTED, + const optional<const char *>& format = none); +void set_date_format(const char * format); +void set_input_date_format(const char * format); + +inline void to_xml(std::ostream& out, const datetime_t& when, + bool wrap = true) +{ + if (wrap) { + push_xml x(out, "datetime"); + out << format_datetime(when, FMT_WRITTEN); + } else { + out << format_datetime(when, FMT_WRITTEN); + } +} + +inline void to_xml(std::ostream& out, const date_t& when, + bool wrap = true) +{ + if (wrap) { + push_xml x(out, "date"); + out << format_date(when, FMT_WRITTEN); + } else { + out << format_date(when, FMT_WRITTEN); + } +} + +struct date_traits_t +{ + bool has_year; + bool has_month; + bool has_day; + + date_traits_t(bool _has_year = false, + bool _has_month = false, + bool _has_day = false) + : has_year(_has_year), has_month(_has_month), has_day(_has_day) { + TRACE_CTOR(date_traits_t, "bool, bool, bool"); + } + date_traits_t(const date_traits_t& traits) + : has_year(traits.has_year), + has_month(traits.has_month), + has_day(traits.has_day) { + TRACE_CTOR(date_traits_t, "copy"); + } + ~date_traits_t() throw() { + TRACE_DTOR(date_traits_t); + } + + date_traits_t& operator=(const date_traits_t& traits) { + has_year = traits.has_year; + has_month = traits.has_month; + has_day = traits.has_day; + return *this; + } + + bool operator==(const date_traits_t& traits) const { + return (has_year == traits.has_year && + has_month == traits.has_month && + has_day == traits.has_day); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & has_year; + ar & has_month; + ar & has_day; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +struct date_duration_t +{ + enum skip_quantum_t { + DAYS, WEEKS, MONTHS, QUARTERS, YEARS + } quantum; + int length; + + date_duration_t() : quantum(DAYS), length(0) { + TRACE_CTOR(date_duration_t, ""); + } + date_duration_t(skip_quantum_t _quantum, int _length) + : quantum(_quantum), length(_length) { + TRACE_CTOR(date_duration_t, "skip_quantum_t, int"); + } + date_duration_t(const date_duration_t& dur) + : quantum(dur.quantum), length(dur.length) { + TRACE_CTOR(date_duration_t, "copy"); + } + ~date_duration_t() throw() { + TRACE_DTOR(date_duration_t); + } + + date_t add(const date_t& date) const { + switch (quantum) { + case DAYS: + return date + gregorian::days(length); + case WEEKS: + return date + gregorian::weeks(length); + case MONTHS: + return date + gregorian::months(length); + case QUARTERS: + return date + gregorian::months(length * 3); + case YEARS: + return date + gregorian::years(length); + default: + assert(false); return date_t(); + } + } + + date_t subtract(const date_t& date) const { + switch (quantum) { + case DAYS: + return date - gregorian::days(length); + case WEEKS: + return date - gregorian::weeks(length); + case MONTHS: + return date - gregorian::months(length); + case QUARTERS: + return date - gregorian::months(length * 3); + case YEARS: + return date - gregorian::years(length); + default: + assert(false); return date_t(); + } + } + + string to_string() const { + std::ostringstream out; + + out << length << ' '; + + switch (quantum) { + case DAYS: out << "day"; break; + case WEEKS: out << "week"; break; + case MONTHS: out << "month"; break; + case QUARTERS: out << "quarter"; break; + case YEARS: out << "year"; break; + default: + assert(false); + break; + } + + if (length > 1) + out << 's'; + + return out.str(); + } + + static date_t find_nearest(const date_t& date, skip_quantum_t skip); + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & quantum; + ar & length; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class date_specifier_t +{ + friend class date_parser_t; + +#if 0 + typedef date_t::year_type year_type; +#else + typedef unsigned short year_type; +#endif + typedef date_t::month_type month_type; + typedef date_t::day_type day_type; + typedef date_t::day_of_week_type day_of_week_type; + + optional<year_type> year; + optional<month_type> month; + optional<day_type> day; + optional<day_of_week_type> wday; + +public: + date_specifier_t(const optional<year_type>& _year = none, + const optional<month_type>& _month = none, + const optional<day_type>& _day = none, + const optional<day_of_week_type>& _wday = none) + : year(_year), month(_month), day(_day), wday(_wday) { + TRACE_CTOR(date_specifier_t, + "year_type, month_type, day_type, day_of_week_type"); + } + 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(); + } + date_specifier_t(const date_specifier_t& other) + : year(other.year), month(other.month), + day(other.day), wday(other.wday) { + TRACE_CTOR(date_specifier_t, "copy"); + } + ~date_specifier_t() throw() { + TRACE_DTOR(date_specifier_t); + } + + date_t begin(const optional_year& current_year = none) const; + date_t end(const optional_year& current_year = none) const; + + bool is_within(const date_t& date, + const optional_year& current_year = none) const { + return date >= begin(current_year) && date < end(current_year); + } + + optional<date_duration_t> implied_duration() const { + if (day || wday) + return date_duration_t(date_duration_t::DAYS, 1); + else if (month) + return date_duration_t(date_duration_t::MONTHS, 1); + else if (year) + return date_duration_t(date_duration_t::YEARS, 1); + else + return none; + } + + string to_string() const { + std::ostringstream out; + + if (year) + out << " year " << *year; + if (month) + out << " month " << *month; + if (day) + out << " day " << *day; + if (wday) + out << " wday " << *wday; + + return out.str(); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & year; + ar & month; + ar & day; + ar & wday; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class date_range_t +{ + friend class date_parser_t; + + optional<date_specifier_t> range_begin; + optional<date_specifier_t> range_end; + + bool end_inclusive; + +public: + date_range_t(const optional<date_specifier_t>& _range_begin = none, + const optional<date_specifier_t>& _range_end = none) + : range_begin(_range_begin), range_end(_range_end), + end_inclusive(false) { + TRACE_CTOR(date_range_t, "date_specifier_t, date_specifier_t"); + } + date_range_t(const date_range_t& other) + : range_begin(other.range_begin), range_end(other.range_end), + end_inclusive(other.end_inclusive) { + TRACE_CTOR(date_range_t, "date_range_t"); + } + ~date_range_t() throw() { + TRACE_DTOR(date_range_t); + } + + optional<date_t> begin(const optional_year& current_year = none) const { + if (range_begin) + return range_begin->begin(current_year); + else + return none; + } + optional<date_t> end(const optional_year& current_year = none) const { + if (range_end) { + if (end_inclusive) + return range_end->end(current_year); + else + return range_end->begin(current_year); + } else { + return none; + } + } + + bool is_within(const date_t& date, + const optional_year& current_year = none) const { + optional<date_t> b = begin(current_year); + optional<date_t> e = end(current_year); + bool after_begin = b ? date >= *b : true; + bool before_end = e ? date < *e : true; + return after_begin && before_end; + } + + string to_string() const { + std::ostringstream out; + + if (range_begin) + out << "from" << range_begin->to_string(); + if (range_end) + out << " to" << range_end->to_string(); + + return out.str(); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & range_begin; + ar & range_end; + ar & end_inclusive; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class date_specifier_or_range_t +{ + typedef variant<int, date_specifier_t, date_range_t> value_type; + + value_type specifier_or_range; + +public: + date_specifier_or_range_t() { + TRACE_CTOR(date_specifier_or_range_t, ""); + } + date_specifier_or_range_t(const date_specifier_or_range_t& other) + : specifier_or_range(other.specifier_or_range) { + TRACE_CTOR(date_specifier_or_range_t, "copy"); + } + date_specifier_or_range_t(const date_specifier_t& specifier) + : specifier_or_range(specifier) { + TRACE_CTOR(date_specifier_or_range_t, "date_specifier_t"); + } + date_specifier_or_range_t(const date_range_t& range) + : specifier_or_range(range) { + TRACE_CTOR(date_specifier_or_range_t, "date_range_t"); + } + ~date_specifier_or_range_t() throw() { + TRACE_DTOR(date_specifier_or_range_t); + } + + optional<date_t> begin(const optional_year& current_year = none) const { + if (specifier_or_range.type() == typeid(date_specifier_t)) + return boost::get<date_specifier_t>(specifier_or_range).begin(current_year); + else if (specifier_or_range.type() == typeid(date_range_t)) + return boost::get<date_range_t>(specifier_or_range).begin(current_year); + else + return none; + } + optional<date_t> end(const optional_year& current_year = none) const { + if (specifier_or_range.type() == typeid(date_specifier_t)) + return boost::get<date_specifier_t>(specifier_or_range).end(current_year); + else if (specifier_or_range.type() == typeid(date_range_t)) + return boost::get<date_range_t>(specifier_or_range).end(current_year); + else + return none; + } + + + string to_string() const { + std::ostringstream out; + + if (specifier_or_range.type() == typeid(date_specifier_t)) + out << "in" << boost::get<date_specifier_t>(specifier_or_range).to_string(); + else if (specifier_or_range.type() == typeid(date_range_t)) + out << boost::get<date_range_t>(specifier_or_range).to_string(); + + return out.str(); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & specifier_or_range; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class date_interval_t : public equality_comparable<date_interval_t> +{ +public: + static date_t add_duration(const date_t& date, + const date_duration_t& duration); + static date_t subtract_duration(const date_t& date, + const date_duration_t& duration); + + optional<date_specifier_or_range_t> range; + + optional<date_t> start; // the real start, after adjustment + optional<date_t> finish; // the real end, likewise + bool aligned; + optional<date_t> next; + optional<date_duration_t> duration; + optional<date_t> end_of_duration; + + explicit date_interval_t() : aligned(false) { + TRACE_CTOR(date_interval_t, ""); + } + date_interval_t(const string& str) : aligned(false) { + TRACE_CTOR(date_interval_t, "const string&"); + parse(str); + } + date_interval_t(const date_interval_t& other) + : range(other.range), + start(other.start), + finish(other.finish), + aligned(other.aligned), + next(other.next), + duration(other.duration), + end_of_duration(other.end_of_duration) { + TRACE_CTOR(date_interval_t, "copy"); + } + ~date_interval_t() throw() { + TRACE_DTOR(date_interval_t); + } + + bool operator==(const date_interval_t& other) const { + return (start == other.start && + (! start || *start == *other.start)); + } + + operator bool() const { + return is_valid(); + } + + optional<date_t> begin(const optional_year& current_year = none) const { + return start ? start : (range ? range->begin(current_year) : none); + } + optional<date_t> end(const optional_year& current_year = none) const { + return finish ? finish : (range ? range->end(current_year) : none); + } + + void parse(const string& str); + + void resolve_end(); + void stabilize(const optional<date_t>& date = none); + + bool is_valid() const { + 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); + + optional<date_t> inclusive_end() const { + if (end_of_duration) + return *end_of_duration - gregorian::days(1); + else + return none; + } + + date_interval_t& operator++(); + + void dump(std::ostream& out, optional_year current_year = 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 & range; + ar & start; + ar & finish; + ar & aligned; + ar & next; + ar & duration; + ar & end_of_duration; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +void times_initialize(); +void times_shutdown(); + +void show_period_tokens(std::ostream& out, const string& arg); + +std::ostream& operator<<(std::ostream& out, const date_duration_t& duration); + +} // namespace ledger + +#endif // _TIMES_H diff --git a/src/token.cc b/src/token.cc new file mode 100644 index 00000000..81c54a82 --- /dev/null +++ b/src/token.cc @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2003-2009, 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 "token.h" +#include "parser.h" + +namespace ledger { + +int expr_t::token_t::parse_reserved_word(std::istream& in) +{ + char c = static_cast<char>(in.peek()); + + if (c == 'a' || c == 'd' || c == 'e' || c == 'f' || + c == 'i' || c == 'o' || c == 'n' || c == 't') { + length = 0; + + char buf[6]; + READ_INTO_(in, buf, 5, c, length, std::isalpha(c)); + + switch (buf[0]) { + case 'a': + if (std::strcmp(buf, "and") == 0) { + symbol[0] = '&'; + symbol[1] = '\0'; + kind = KW_AND; + return 1; + } + break; + + case 'd': + if (std::strcmp(buf, "div") == 0) { + symbol[0] = '/'; + symbol[1] = '/'; + symbol[2] = '\0'; + kind = KW_DIV; + return 1; + } + break; + + case 'e': + if (std::strcmp(buf, "else") == 0) { + symbol[0] = 'L'; + symbol[1] = 'S'; + symbol[2] = '\0'; + kind = KW_ELSE; + return 1; + } + break; + + case 'f': + if (std::strcmp(buf, "false") == 0) { + kind = VALUE; + value = false; + return 1; + } + break; + + case 'i': + if (std::strcmp(buf, "if") == 0) { + symbol[0] = 'i'; + symbol[1] = 'f'; + symbol[2] = '\0'; + kind = KW_IF; + return 1; + } + break; + + case 'o': + if (std::strcmp(buf, "or") == 0) { + symbol[0] = '|'; + symbol[1] = '\0'; + kind = KW_OR; + return 1; + } + break; + + case 'n': + if (std::strcmp(buf, "not") == 0) { + symbol[0] = '!'; + symbol[1] = '\0'; + kind = EXCLAM; + return 1; + } + break; + + case 't': + if (std::strcmp(buf, "true") == 0) { + kind = VALUE; + value = true; + return 1; + } + break; + } + + return 0; + } + return -1; +} + +void expr_t::token_t::parse_ident(std::istream& in) +{ + kind = IDENT; + length = 0; + + char c, buf[256]; + READ_INTO_(in, buf, 255, c, length, std::isalnum(c) || c == '_'); + + value.set_string(buf); +} + +void expr_t::token_t::next(std::istream& in, const parse_flags_t& pflags) +{ + if (in.eof()) { + kind = TOK_EOF; + return; + } + if (! in.good()) + throw_(parse_error, _("Input stream no longer valid")); + + char c = peek_next_nonws(in); + + if (in.eof()) { + kind = TOK_EOF; + return; + } + if (! in.good()) + throw_(parse_error, _("Input stream no longer valid")); + + symbol[0] = c; + symbol[1] = '\0'; + + length = 1; + + switch (c) { + case '&': + in.get(c); + c = static_cast<char>(in.peek()); + if (c == '&') { + in.get(c); + kind = KW_AND; + length = 2; + break; + } + kind = KW_AND; + break; + case '|': + in.get(c); + c = static_cast<char>(in.peek()); + if (c == '|') { + in.get(c); + kind = KW_OR; + length = 2; + break; + } + kind = KW_OR; + break; + + case '(': + in.get(c); + kind = LPAREN; + break; + case ')': + in.get(c); + kind = RPAREN; + break; + + case '[': { + in.get(c); + + char buf[256]; + READ_INTO_(in, buf, 255, c, length, c != ']'); + if (c != ']') + expected(']', c); + + in.get(c); + length++; + + date_interval_t timespan(buf); + optional<date_t> begin = timespan.begin(); + if (! begin) + throw_(parse_error, + _("Date specifier does not refer to a starting date")); + kind = VALUE; + value = *begin; + break; + } + + case '\'': + case '"': { + char delim; + in.get(delim); + char buf[4096]; + READ_INTO_(in, buf, 4095, c, length, c != delim); + if (c != delim) + expected(delim, c); + in.get(c); + length++; + kind = VALUE; + value.set_string(buf); + break; + } + + case '{': { + in.get(c); + amount_t temp; + temp.parse(in, PARSE_NO_MIGRATE); + in.get(c); + if (c != '}') + expected('}', c); + length++; + kind = VALUE; + value = temp; + break; + } + + case '!': + in.get(c); + c = static_cast<char>(in.peek()); + if (c == '=') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = NEQUAL; + length = 2; + break; + } + else if (c == '~') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = NMATCH; + length = 2; + break; + } + kind = EXCLAM; + break; + + case '-': + in.get(c); + kind = MINUS; + break; + case '+': + in.get(c); + kind = PLUS; + break; + + case '*': + in.get(c); + kind = STAR; + break; + + case '?': + in.get(c); + kind = QUERY; + break; + case ':': + in.get(c); + c = static_cast<char>(in.peek()); + if (c == '=') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = DEFINE; + length = 2; + break; + } + kind = COLON; + break; + + case '/': { + in.get(c); + if (pflags.has_flags(PARSE_OP_CONTEXT)) { // operator context + kind = SLASH; + } else { // terminal context + // Read in the regexp + char buf[256]; + READ_INTO_(in, buf, 255, c, length, c != '/'); + if (c != '/') + expected('/', c); + in.get(c); + length++; + + kind = VALUE; + value.set_mask(buf); + } + break; + } + + case '=': + in.get(c); + c = static_cast<char>(in.peek()); + if (c == '~') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = MATCH; + length = 2; + break; + } + else if (c == '=') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = EQUAL; + length = 2; + break; + } + kind = EQUAL; + break; + + case '<': + in.get(c); + if (static_cast<char>(in.peek()) == '=') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = LESSEQ; + length = 2; + break; + } + kind = LESS; + break; + + case '>': + in.get(c); + if (static_cast<char>(in.peek()) == '=') { + in.get(c); + symbol[1] = c; + symbol[2] = '\0'; + kind = GREATEREQ; + length = 2; + break; + } + kind = GREATER; + break; + + case '.': + in.get(c); + kind = DOT; + break; + + case ',': + in.get(c); + kind = COMMA; + break; + + case ';': + in.get(c); + kind = SEMI; + break; + + default: { + istream_pos_type pos = in.tellg(); + + // First, check to see if it's a reserved word, such as: and or not + int result = parse_reserved_word(in); + if (std::isalpha(c) && result == 1) + break; + + // If not, rewind back to the beginning of the word to scan it + // again. If the result was -1, it means no identifier was scanned + // so we don't have to rewind. + if (result == 0) { + in.clear(); + in.seekg(pos, std::ios::beg); + if (in.fail()) + throw_(parse_error, _("Failed to reset input stream")); + } + + // When in relaxed parsing mode, we want to migrate commodity flags + // so that any precision specified by the user updates the current + // maximum displayed precision. + parse_flags_t parse_flags; + + if (pflags.has_flags(PARSE_NO_MIGRATE)) + parse_flags.add_flags(PARSE_NO_MIGRATE); + if (pflags.has_flags(PARSE_NO_REDUCE)) + parse_flags.add_flags(PARSE_NO_REDUCE); + + 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::isdigit(c) || c == '.') + expected('\0', c); + + parse_ident(in); + } else { + kind = VALUE; + value = temp; + length = static_cast<std::size_t>(in.tellg() - pos); + } + } + catch (const std::exception& err) { + kind = ERROR; + length = static_cast<std::size_t>(in.tellg() - pos); + throw; + } + break; + } + } +} + +void expr_t::token_t::rewind(std::istream& in) +{ + in.seekg(- length, std::ios::cur); + if (in.fail()) + throw_(parse_error, _("Failed to rewind input stream")); +} + + +void expr_t::token_t::unexpected() +{ + kind_t prev_kind = kind; + + kind = ERROR; + + switch (prev_kind) { + case TOK_EOF: + throw_(parse_error, _("Unexpected end of expression")); + case IDENT: + throw_(parse_error, _("Unexpected symbol '%1'") << value); + case VALUE: + throw_(parse_error, _("Unexpected value '%1'") << value); + default: + throw_(parse_error, _("Unexpected token '%1'") << symbol); + } +} + +void expr_t::token_t::expected(char wanted, char c) +{ + kind = ERROR; + + if (c == '\0' || c == -1) { + if (wanted == '\0' || wanted == -1) + throw_(parse_error, _("Unexpected end")); + else + throw_(parse_error, _("Missing '%1'") << wanted); + } else { + if (wanted == '\0' || wanted == -1) + throw_(parse_error, _("Invalid char '%1'") << c); + else + throw_(parse_error, _("Invalid char '%1' (wanted '%2')") << c << wanted); + } +} + +} // namespace ledger diff --git a/src/token.h b/src/token.h new file mode 100644 index 00000000..670f16e3 --- /dev/null +++ b/src/token.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup expr Value expressions + */ + +/** + * @file token.h + * @author John Wiegley + * + * @ingroup expr + */ +#ifndef _TOKEN_H +#define _TOKEN_H + +#include "expr.h" + +namespace ledger { + +struct expr_t::token_t : public noncopyable +{ + enum kind_t { + ERROR, // an error occurred while tokenizing + VALUE, // any kind of literal value + IDENT, // [A-Za-z_][-A-Za-z0-9_:]* + MASK, // /regexp/ + + LPAREN, // ( + RPAREN, // ) + + EQUAL, // == + NEQUAL, // != + LESS, // < + LESSEQ, // <= + GREATER, // > + GREATEREQ, // >= + + DEFINE, // := + ASSIGN, // = + MATCH, // =~ + NMATCH, // !~ + MINUS, // - + PLUS, // + + STAR, // * + SLASH, // / + KW_DIV, // div + + EXCLAM, // !, not + KW_AND, // &, &&, and + KW_OR, // |, ||, or + KW_MOD, // % + + KW_IF, // if + KW_ELSE, // else + + QUERY, // ? + COLON, // : + + DOT, // . + COMMA, // , + SEMI, // ; + + TOK_EOF, + UNKNOWN + + } kind; + + char symbol[3]; + value_t value; + std::size_t length; + + explicit token_t() : kind(UNKNOWN), length(0) { + TRACE_CTOR(expr_t::token_t, ""); + } + ~token_t() throw() { + TRACE_DTOR(expr_t::token_t); + } + + token_t& operator=(const token_t& other) { + if (&other == this) + return *this; + assert(false); // only one token object is used at a time + return *this; + } + + void clear() { + kind = UNKNOWN; + length = 0; + value = NULL_VALUE; + + symbol[0] = '\0'; + symbol[1] = '\0'; + symbol[2] = '\0'; + } + + int parse_reserved_word(std::istream& in); + void parse_ident(std::istream& in); + void next(std::istream& in, const parse_flags_t& flags); + void rewind(std::istream& in); + void unexpected(); + void expected(char wanted, char c = '\0'); +}; + +} // namespace ledger + +#endif // _TOKEN_H diff --git a/src/unistring.h b/src/unistring.h new file mode 100644 index 00000000..7c433d9d --- /dev/null +++ b/src/unistring.h @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2003-2009, 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 utils + */ + +/** + * @file unistring.h + * @author John Wiegley + * + * @ingroup utils + */ +#ifndef _UNISTRING_H +#define _UNISTRING_H + +namespace ledger { + +/** + * @class unistring + * + * @brief Abstract working with UTF-32 encoded Unicode strings + * + * The input to the string is a UTF8 encoded ledger::string, which can + * then have its true length be taken, or characters extracted. + */ +class unistring +{ +public: + std::vector<boost::uint32_t> utf32chars; + + unistring() { + TRACE_CTOR(unistring, ""); + } + unistring(const std::string& input) + { + TRACE_CTOR(unistring, "std::string"); + + const char * p = input.c_str(); + std::size_t len = input.length(); + + VERIFY(utf8::is_valid(p, p + len)); + utf8::unchecked::utf8to32(p, p + len, std::back_inserter(utf32chars)); + } + ~unistring() { + TRACE_DTOR(unistring); + } + + std::size_t length() const { + return utf32chars.size(); + } + + std::string extract(const std::string::size_type begin = 0, + const std::string::size_type len = 0) const + { + std::string utf8result; + std::string::size_type this_len = length(); + + assert(begin <= this_len); + assert(begin + len <= this_len); + + if (this_len) + utf8::unchecked::utf32to8 + (utf32chars.begin() + begin, + utf32chars.begin() + begin + + (len ? (len > this_len ? this_len : len) : this_len), + std::back_inserter(utf8result)); + + return utf8result; + } +}; + +inline void justify(std::ostream& out, + const std::string& str, + int width, + bool right = false, + bool redden = false) +{ + if (! right) { + if (redden) out << "\033[31m"; + out << str; + if (redden) out << "\033[0m"; + } + + unistring temp(str); + + int spacing = width - int(temp.length()); + while (spacing-- > 0) + out << ' '; + + if (right) { + if (redden) out << "\033[31m"; + out << str; + if (redden) out << "\033[0m"; + } +} + +} // namespace ledger + +#endif // _UNISTRING_H diff --git a/src/utils.cc b/src/utils.cc new file mode 100644 index 00000000..f2460ba1 --- /dev/null +++ b/src/utils.cc @@ -0,0 +1,821 @@ +/* + * Copyright (c) 2003-2009, 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 "times.h" + +/********************************************************************** + * + * Assertions + */ + +#if defined(ASSERTS_ON) + +namespace ledger { + +DECLARE_EXCEPTION(assertion_failed, std::logic_error); + +void debug_assert(const string& reason, + const string& func, + const string& file, + std::size_t line) +{ + std::ostringstream buf; + buf << "Assertion failed in \"" << file << "\", line " << line + << ": " << func << ": " << reason; + throw assertion_failed(buf.str()); +} + +} // namespace ledger + +#endif + +/********************************************************************** + * + * Verification (basically, very slow asserts) + */ + +#if defined(VERIFY_ON) + +namespace ledger { + +bool verify_enabled = false; + +typedef std::pair<std::string, std::size_t> allocation_pair; +typedef std::map<void *, allocation_pair> memory_map; +typedef std::multimap<void *, allocation_pair> objects_map; + +typedef std::pair<std::size_t, std::size_t> count_size_pair; +typedef std::map<std::string, count_size_pair> object_count_map; + +namespace { + bool memory_tracing_active = false; + + memory_map * live_memory = NULL; + memory_map * freed_memory = NULL; + object_count_map * live_memory_count = NULL; + object_count_map * total_memory_count = NULL; + objects_map * live_objects = NULL; + object_count_map * live_object_count = NULL; + object_count_map * total_object_count = NULL; + object_count_map * total_ctor_count = NULL; +} + +void initialize_memory_tracing() +{ + memory_tracing_active = false; + + live_memory = new memory_map; + freed_memory = new memory_map; + live_memory_count = new object_count_map; + total_memory_count = new object_count_map; + live_objects = new objects_map; + live_object_count = new object_count_map; + total_object_count = new object_count_map; + total_ctor_count = new object_count_map; + + memory_tracing_active = true; +} + +void shutdown_memory_tracing() +{ + memory_tracing_active = false; + + if (live_objects) { + IF_DEBUG("memory.counts") + report_memory(std::cerr, true); + else IF_DEBUG("memory.counts.live") + report_memory(std::cerr); + else if (live_objects->size() > 0) + report_memory(std::cerr); + } + + checked_delete(live_memory); live_memory = NULL; + checked_delete(freed_memory); freed_memory = NULL; + checked_delete(live_memory_count); live_memory_count = NULL; + checked_delete(total_memory_count); total_memory_count = NULL; + checked_delete(live_objects); live_objects = NULL; + checked_delete(live_object_count); live_object_count = NULL; + checked_delete(total_object_count); total_object_count = NULL; + checked_delete(total_ctor_count); total_ctor_count = NULL; +} + +inline void add_to_count_map(object_count_map& the_map, + const char * name, std::size_t size) +{ + object_count_map::iterator k = the_map.find(name); + if (k != the_map.end()) { + (*k).second.first++; + (*k).second.second += size; + } else { + std::pair<object_count_map::iterator, bool> result = + the_map.insert(object_count_map::value_type(name, count_size_pair(1, size))); + VERIFY(result.second); + } +} + +std::size_t current_memory_size() +{ + std::size_t memory_size = 0; + + foreach (const object_count_map::value_type& pair, *live_memory_count) + memory_size += pair.second.second; + + return memory_size; +} + +static void trace_new_func(void * ptr, const char * which, std::size_t size) +{ + if (! live_memory || ! memory_tracing_active) return; + + memory_tracing_active = false; + + memory_map::iterator i = freed_memory->find(ptr); + if (i != freed_memory->end()) + freed_memory->erase(i); + + live_memory->insert + (memory_map::value_type(ptr, allocation_pair(which, size))); + + add_to_count_map(*live_memory_count, which, size); + add_to_count_map(*total_memory_count, which, size); + add_to_count_map(*total_memory_count, "__ALL__", size); + + memory_tracing_active = true; +} + +static void trace_delete_func(void * ptr, const char * which) +{ + if (! live_memory || ! memory_tracing_active) return; + + memory_tracing_active = false; + + // Ignore deletions of memory not tracked, since it's possible that + // a user (like boost) allocated a block of memory before memory + // tracking began, and then deleted it before memory tracking ended. + // If it really is a double-delete, the malloc library on OS/X will + // notify me. + + memory_map::iterator i = live_memory->find(ptr); + if (i == live_memory->end()) { + i = freed_memory->find(ptr); + if (i != freed_memory->end()) + VERIFY(! "Freeing a block of memory twice"); +#if 0 + // There can be memory allocated by Boost or the standard library, which + // was allocated before memory tracing got turned on, that the system + // might free for some coincidental reason. As such, we can't rely on + // this check being valid. I've seen cases where processes ran to + // completion with it on, and then others where valid processes failed. + else + VERIFY(! "Freeing an unknown block of memory"); +#endif + memory_tracing_active = true; + return; + } + + std::size_t size = (*i).second.second; + VERIFY((*i).second.first == which); + + live_memory->erase(i); + + freed_memory->insert + (memory_map::value_type(ptr, allocation_pair(which, size))); + + object_count_map::iterator j = live_memory_count->find(which); + VERIFY(j != live_memory_count->end()); + + (*j).second.second -= size; + if (--(*j).second.first == 0) + live_memory_count->erase(j); + + memory_tracing_active = true; +} + +} // namespace ledger + +void * operator new(std::size_t size) throw (std::bad_alloc) { + void * ptr = std::malloc(size); + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_new_func(ptr, "new", size); + return ptr; +} +void * operator new(std::size_t size, const std::nothrow_t&) throw() { + void * ptr = std::malloc(size); + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_new_func(ptr, "new", size); + return ptr; +} +void * operator new[](std::size_t size) throw (std::bad_alloc) { + void * ptr = std::malloc(size); + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_new_func(ptr, "new[]", size); + return ptr; +} +void * operator new[](std::size_t size, const std::nothrow_t&) throw() { + void * ptr = std::malloc(size); + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_new_func(ptr, "new[]", size); + return ptr; +} +void operator delete(void * ptr) throw() { + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_delete_func(ptr, "new"); + std::free(ptr); +} +void operator delete(void * ptr, const std::nothrow_t&) throw() { + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_delete_func(ptr, "new"); + std::free(ptr); +} +void operator delete[](void * ptr) throw() { + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_delete_func(ptr, "new[]"); + std::free(ptr); +} +void operator delete[](void * ptr, const std::nothrow_t&) throw() { + if (DO_VERIFY() && ledger::memory_tracing_active) + ledger::trace_delete_func(ptr, "new[]"); + std::free(ptr); +} + +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; +} + +std::size_t current_objects_size() +{ + std::size_t objects_size = 0; + + foreach (const object_count_map::value_type& pair, *live_object_count) + objects_size += pair.second.second; + + return objects_size; +} + +void trace_ctor_func(void * ptr, const char * cls_name, const char * args, + std::size_t cls_size) +{ + if (! live_objects || ! memory_tracing_active) return; + + memory_tracing_active = false; + + static char name[1024]; + std::strcpy(name, cls_name); + std::strcat(name, "("); + std::strcat(name, args); + std::strcat(name, ")"); + + DEBUG("memory.debug", "TRACE_CTOR " << ptr << " " << name); + + live_objects->insert + (objects_map::value_type(ptr, allocation_pair(cls_name, cls_size))); + + add_to_count_map(*live_object_count, cls_name, cls_size); + add_to_count_map(*total_object_count, cls_name, cls_size); + add_to_count_map(*total_object_count, "__ALL__", cls_size); + add_to_count_map(*total_ctor_count, name, cls_size); + + memory_tracing_active = true; +} + +void trace_dtor_func(void * ptr, const char * cls_name, std::size_t cls_size) +{ + if (! live_objects || ! memory_tracing_active) return; + + memory_tracing_active = false; + + DEBUG("memory.debug", "TRACE_DTOR " << ptr << " " << cls_name); + + objects_map::iterator i = live_objects->find(ptr); + if (i == live_objects->end()) { + warning_(_("Attempting to delete %1 a non-living %2") << ptr << cls_name); + memory_tracing_active = true; + return; + } + + std::size_t ptr_count = live_objects->count(ptr); + for (std::size_t x = 0; x < ptr_count; x++, i++) { + if ((*i).second.first == cls_name) { + live_objects->erase(i); + break; + } + } + + object_count_map::iterator k = live_object_count->find(cls_name); + if (k == live_object_count->end()) { + warning_(_("Failed to find %1 in live object counts") << cls_name); + memory_tracing_active = true; + return; + } + + (*k).second.second -= cls_size; + if (--(*k).second.first == 0) + live_object_count->erase(k); + + memory_tracing_active = true; +} + +void report_memory(std::ostream& out, bool report_all) +{ + if (! live_memory || ! memory_tracing_active) return; + + if (live_memory_count->size() > 0) { + out << "NOTE: There may be memory held by Boost " + << "and libstdc++ after ledger::shutdown()" << std::endl; + out << "Live memory count:" << std::endl; + report_count_map(out, *live_memory_count); + } + + 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 + << std::endl; + } + + if (report_all && total_memory_count->size() > 0) { + out << "Total memory counts:" << std::endl; + report_count_map(out, *total_memory_count); + } + + if (live_object_count->size() > 0) { + out << "Live object count:" << std::endl; + report_count_map(out, *live_object_count); + } + + 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 + << std::endl; + } + + if (report_all) { + if (total_object_count->size() > 0) { + out << "Total object counts:" << std::endl; + report_count_map(out, *total_object_count); + } + + if (total_ctor_count->size() > 0) { + out << "Total constructor counts:" << std::endl; + report_count_map(out, *total_ctor_count); + } + } +} + +} // namespace ledger + +#endif // VERIFY_ON + +/********************************************************************** + * + * String wrapper + */ + +namespace ledger { + +#if defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + +string::string() : std::string() { + TRACE_CTOR(string, ""); +} +string::string(const string& str) : std::string(str) { + TRACE_CTOR(string, "copy"); +} +string::string(const std::string& str) : std::string(str) { + TRACE_CTOR(string, "const std::string&"); +} +string::string(size_type len, char x) : std::string(len, x) { + TRACE_CTOR(string, "size_type, char"); +} +string::string(const char * str) : std::string(str) { + TRACE_CTOR(string, "const char *"); +} +string::string(const char * str, const char * end) : std::string(str, end) { + TRACE_CTOR(string, "const char *, const char *"); +} +string::string(const string& str, size_type x) : std::string(str, x) { + TRACE_CTOR(string, "const string&, size_type"); +} +string::string(const string& str, size_type x, size_type y) + : std::string(str, x, y) { + TRACE_CTOR(string, "const string&, size_type, size_type"); +} +string::string(const char * str, size_type x) : std::string(str, x) { + TRACE_CTOR(string, "const char *, size_type"); +} +string::string(const char * str, size_type x, size_type y) + : std::string(str, x, y) { + TRACE_CTOR(string, "const char *, size_type, size_type"); +} +string::~string() throw() { + TRACE_DTOR(string); +} + +#endif // defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + +string empty_string(""); + +strings_list split_arguments(const char * line) +{ + strings_list args; + + char buf[4096]; + char * q = buf; + char in_quoted_string = '\0'; + + for (const char * p = line; *p; p++) { + if (! in_quoted_string && std::isspace(*p)) { + if (q != buf) { + *q = '\0'; + args.push_back(buf); + q = buf; + } + } + else if (in_quoted_string != '\'' && *p == '\\') { + p++; + if (! *p) + throw_(std::logic_error, _("Invalid use of backslash")); + *q++ = *p; + } + else if (in_quoted_string != '"' && *p == '\'') { + if (in_quoted_string == '\'') + in_quoted_string = '\0'; + else + in_quoted_string = '\''; + } + else if (in_quoted_string != '\'' && *p == '"') { + if (in_quoted_string == '"') + in_quoted_string = '\0'; + else + in_quoted_string = '"'; + } + else { + *q++ = *p; + } + } + + if (in_quoted_string) + throw_(std::logic_error, + _("Unterminated string, expected '%1'") << in_quoted_string); + + if (q != buf) { + *q = '\0'; + args.push_back(buf); + } + + return args; +} + +} // namespace ledger + +/********************************************************************** + * + * Logging + */ + +#if defined(LOGGING_ON) + +namespace ledger { + +log_level_t _log_level = LOG_WARN; +std::ostream * _log_stream = &std::cerr; +std::ostringstream _log_buffer; + +#if defined(TRACING_ON) +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; + +bool logger_func(log_level_t level) +{ + if (! logger_has_run) { + logger_has_run = true; + logger_start = TRUE_CURRENT_TIME(); + +#if defined(VERIFY_ON) + IF_VERIFY() + *_log_stream << " TIME OBJSZ MEMSZ" << std::endl; +#else + IF_VERIFY() + *_log_stream << " TIME" << std::endl; +#endif + } + + *_log_stream << std::right << std::setw(5) + << (TRUE_CURRENT_TIME() - + logger_start).total_milliseconds() << "ms"; + +#if defined(VERIFY_ON) + IF_VERIFY() { + *_log_stream << std::right << std::setw(6) << std::setprecision(3); + stream_memory_size(*_log_stream, current_objects_size()); + *_log_stream << std::right << std::setw(6) << std::setprecision(3); + stream_memory_size(*_log_stream, current_memory_size()); + } +#endif + + *_log_stream << " " << std::left << std::setw(7); + + switch (level) { + case LOG_CRIT: *_log_stream << "[CRIT]"; break; + case LOG_FATAL: *_log_stream << "[FATAL]"; break; + case LOG_ASSERT: *_log_stream << "[ASSRT]"; break; + case LOG_ERROR: *_log_stream << "[ERROR]"; break; + case LOG_VERIFY: *_log_stream << "[VERFY]"; break; + case LOG_WARN: *_log_stream << "[WARN]"; break; + case LOG_INFO: *_log_stream << "[INFO]"; break; + case LOG_EXCEPT: *_log_stream << "[EXCPT]"; break; + case LOG_DEBUG: *_log_stream << "[DEBUG]"; break; + case LOG_TRACE: *_log_stream << "[TRACE]"; break; + + case LOG_OFF: + case LOG_ALL: + assert(false); + break; + } + + *_log_stream << ' ' << _log_buffer.str() << std::endl; + _log_buffer.str(""); + + return true; +} + +} // namespace ledger + +#if defined(DEBUG_ON) + +namespace ledger { + +optional<std::string> _log_category; + +struct __maybe_enable_debugging { + __maybe_enable_debugging() { + const char * p = std::getenv("LEDGER_DEBUG"); + if (p != NULL) { + _log_level = LOG_DEBUG; + _log_category = p; + } + } +} __maybe_enable_debugging_obj; + +} // namespace ledger + +#endif // DEBUG_ON +#endif // LOGGING_ON + +/********************************************************************** + * + * Timers (allows log xacts to specify cumulative time spent) + */ + +#if defined(LOGGING_ON) && defined(TIMERS_ON) + +namespace ledger { + +struct timer_t +{ + log_level_t level; + ptime begin; + time_duration spent; + std::string description; + bool active; + + timer_t(log_level_t _level, std::string _description) + : level(_level), begin(TRUE_CURRENT_TIME()), + spent(time_duration(0, 0, 0, 0)), + description(_description), active(true) {} +}; + +typedef std::map<std::string, timer_t> timer_map; + +static timer_map timers; + +void start_timer(const char * name, log_level_t lvl) +{ +#if defined(VERIFY_ON) + bool tracing_active = memory_tracing_active; + memory_tracing_active = false; +#endif + + timer_map::iterator i = timers.find(name); + if (i == timers.end()) { + timers.insert(timer_map::value_type(name, timer_t(lvl, _log_buffer.str()))); + } else { + assert((*i).second.description == _log_buffer.str()); + (*i).second.begin = TRUE_CURRENT_TIME(); + (*i).second.active = true; + } + _log_buffer.str(""); + +#if defined(VERIFY_ON) + memory_tracing_active = tracing_active; +#endif +} + +void stop_timer(const char * name) +{ +#if defined(VERIFY_ON) + bool tracing_active = memory_tracing_active; + memory_tracing_active = false; +#endif + + timer_map::iterator i = timers.find(name); + assert(i != timers.end()); + + (*i).second.spent += TRUE_CURRENT_TIME() - (*i).second.begin; + (*i).second.active = false; + +#if defined(VERIFY_ON) + memory_tracing_active = tracing_active; +#endif +} + +void finish_timer(const char * name) +{ +#if defined(VERIFY_ON) + bool tracing_active = memory_tracing_active; + memory_tracing_active = false; +#endif + + timer_map::iterator i = timers.find(name); + if (i == timers.end()) { +#if defined(VERIFY_ON) + memory_tracing_active = tracing_active; +#endif + return; + } + + time_duration spent = (*i).second.spent; + if ((*i).second.active) { + spent = TRUE_CURRENT_TIME() - (*i).second.begin; + (*i).second.active = false; + } + + _log_buffer << (*i).second.description << ' '; + + bool need_paren = + (*i).second.description[(*i).second.description.size() - 1] != ':'; + + if (need_paren) + _log_buffer << '('; + + _log_buffer << spent.total_milliseconds() << "ms"; + + if (need_paren) + _log_buffer << ')'; + + logger_func((*i).second.level); + + timers.erase(i); + +#if defined(VERIFY_ON) + memory_tracing_active = tracing_active; +#endif +} + +} // namespace ledger + +#endif // LOGGING_ON && TIMERS_ON + +/********************************************************************** + * + * Signal handlers + */ + +caught_signal_t caught_signal = NONE_CAUGHT; + +void sigint_handler(int) +{ + caught_signal = INTERRUPTED; +} + +void sigpipe_handler(int) +{ + caught_signal = PIPE_CLOSED; +} + +/********************************************************************** + * + * General utility functions + */ + +namespace ledger { + +const string version = PACKAGE_VERSION; + +path expand_path(const path& pathname) +{ + if (pathname.empty()) + return pathname; + + std::string path_string = pathname.string(); + const char * pfx = NULL; + string::size_type pos = path_string.find_first_of('/'); + + if (path_string.length() == 1 || pos == 1) { + pfx = std::getenv("HOME"); +#ifdef HAVE_GETPWUID + if (! pfx) { + // Punt. We're trying to expand ~/, but HOME isn't set + struct passwd * pw = getpwuid(getuid()); + if (pw) + pfx = pw->pw_dir; + } +#endif + } +#ifdef HAVE_GETPWNAM + else { + string user(path_string, 1, pos == string::npos ? + string::npos : pos - 1); + struct passwd * pw = getpwnam(user.c_str()); + if (pw) + pfx = pw->pw_dir; + } +#endif + + // if we failed to find an expansion, return the path unchanged. + + if (! pfx) + return pathname; + + string result(pfx); + + if (pos == string::npos) + return result; + + if (result.length() == 0 || result[result.length() - 1] != '/') + result += '/'; + + result += path_string.substr(pos + 1); + + return result; +} + +path resolve_path(const path& pathname) +{ + path temp = pathname; + if (temp.string()[0] == '~') + temp = expand_path(temp); + temp.normalize(); + return temp; +} + +} // namespace ledger diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 00000000..ab8fb495 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,713 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @defgroup util General utilities + */ + +/** + * @file utils.h + * @author John Wiegley + * + * @ingroup util + * + * @brief General utility facilities used by Ledger + */ +#ifndef _UTILS_H +#define _UTILS_H + +/** + * @name Default values + */ +/*@{*/ + +#if defined(DEBUG_MODE) +#define VERIFY_ON 1 +#define TRACING_ON 1 +#define DEBUG_ON 1 +#define TIMERS_ON 1 +#elif defined(NDEBUG) +#define NO_ASSERTS 1 +#define NO_LOGGING 1 +#else +#define TRACING_ON 1 // use --trace X to enable +#define TIMERS_ON 1 +#endif + +/*@}*/ + +/** + * @name Forward declarations + */ +/*@{*/ + +namespace ledger { + using namespace boost; + +#if defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + class string; +#else + typedef std::string string; +#endif + + typedef std::list<string> strings_list; + + typedef posix_time::ptime ptime; + typedef ptime::time_duration_type time_duration; + typedef gregorian::date date; + typedef gregorian::date_duration date_duration; + typedef posix_time::seconds seconds; + + typedef boost::filesystem::path path; + typedef boost::filesystem::ifstream ifstream; + typedef boost::filesystem::ofstream ofstream; + typedef boost::filesystem::filesystem_error filesystem_error; +} + +/*@}*/ + +/** + * @name Assertions + */ +/*@{*/ + +#ifdef assert +#undef assert +#endif + +#if ! defined(NO_ASSERTS) +#define ASSERTS_ON 1 +#endif +#if defined(ASSERTS_ON) + +namespace ledger { + void debug_assert(const string& reason, const string& func, + const string& file, std::size_t line); +} + +#define assert(x) \ + ((x) ? ((void)0) : debug_assert(#x, BOOST_CURRENT_FUNCTION, \ + __FILE__, __LINE__)) + +#else // ! ASSERTS_ON + +#define assert(x) + +#endif // ASSERTS_ON + +/*@}*/ + +/** + * @name Verification (i.e., heavy asserts) + */ +/*@{*/ + +#if defined(VERIFY_ON) + +namespace ledger { + +extern bool verify_enabled; + +#define VERIFY(x) (ledger::verify_enabled ? assert(x) : ((void)0)) +#define DO_VERIFY() ledger::verify_enabled + +void initialize_memory_tracing(); +void shutdown_memory_tracing(); + +std::size_t current_memory_size(); +std::size_t current_objects_size(); + +void trace_ctor_func(void * ptr, const char * cls_name, const char * args, + std::size_t cls_size); +void trace_dtor_func(void * ptr, const char * cls_name, std::size_t cls_size); + +#define TRACE_CTOR(cls, args) \ + (DO_VERIFY() ? \ + ledger::trace_ctor_func(this, #cls, args, sizeof(cls)) : ((void)0)) +#define TRACE_DTOR(cls) \ + (DO_VERIFY() ? \ + ledger::trace_dtor_func(this, #cls, sizeof(cls)) : ((void)0)) + +void report_memory(std::ostream& out, bool report_all = false); + +} // namespace ledger + +#else // ! VERIFY_ON + +#define VERIFY(x) +#define DO_VERIFY() true +#define TRACE_CTOR(cls, args) +#define TRACE_DTOR(cls) + +#endif // VERIFY_ON + +#define IF_VERIFY() if (DO_VERIFY()) + +/*@}*/ + +/** + * @name String wrapper + * + * This string type is a wrapper around std::string that allows us to trace + * constructor and destructor calls. It also makes ledger's use of strings a + * unique type, that the Boost.Python code can use as the basis for + * transparent Unicode conversions. + */ +/*@{*/ + +namespace ledger { + +#if defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + +class string : public std::string +{ +public: + string(); + string(const string& str); + string(const std::string& str); + string(size_type len, char x); + template<class _InputIterator> + string(_InputIterator __beg, _InputIterator __end) + : std::string(__beg, __end) { + TRACE_CTOR(string, "InputIterator, InputIterator"); + } + string(const char * str); + string(const char * str, const char * end); + string(const string& str, size_type x); + string(const string& str, size_type x, size_type y); + string(const char * str, size_type x); + string(const char * str, size_type x, size_type y); + ~string() throw(); + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<std::string>(*this); + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +inline string operator+(const string& __lhs, const string& __rhs) +{ + string __str(__lhs); + __str.append(__rhs); + return __str; +} + +string operator+(const char* __lhs, const string& __rhs); +string operator+(char __lhs, const string& __rhs); + +inline string operator+(const string& __lhs, const char* __rhs) +{ + string __str(__lhs); + __str.append(__rhs); + return __str; +} + +inline string operator+(const string& __lhs, char __rhs) +{ + typedef string __string_type; + typedef string::size_type __size_type; + __string_type __str(__lhs); + __str.append(__size_type(1), __rhs); + return __str; +} + +inline bool operator==(const string& __lhs, const string& __rhs) +{ return __lhs.compare(__rhs) == 0; } + +inline bool operator==(const char* __lhs, const string& __rhs) +{ return __rhs.compare(__lhs) == 0; } + +inline bool operator==(const string& __lhs, const char* __rhs) +{ return __lhs.compare(__rhs) == 0; } + +inline bool operator!=(const string& __lhs, const string& __rhs) +{ return __rhs.compare(__lhs) != 0; } + +inline bool operator!=(const char* __lhs, const string& __rhs) +{ return __rhs.compare(__lhs) != 0; } + +inline bool operator!=(const string& __lhs, const char* __rhs) +{ return __lhs.compare(__rhs) != 0; } + +#endif // defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + +extern string empty_string; + +strings_list split_arguments(const char * line); + +} // namespace ledger + +/*@}*/ + +/** + * @name Tracing and logging + */ +/*@{*/ + +#if ! defined(NO_LOGGING) +#define LOGGING_ON 1 +#endif +#if defined(LOGGING_ON) + +namespace ledger { + +enum log_level_t { + LOG_OFF = 0, + LOG_CRIT, + LOG_FATAL, + LOG_ASSERT, + LOG_ERROR, + LOG_VERIFY, + LOG_WARN, + LOG_INFO, + LOG_EXCEPT, + LOG_DEBUG, + LOG_TRACE, + LOG_ALL +}; + +extern log_level_t _log_level; +extern std::ostream * _log_stream; +extern std::ostringstream _log_buffer; + +bool logger_func(log_level_t level); + +#define LOGGER(cat) \ + static const char * const _this_category = cat + +#if defined(TRACING_ON) + +extern uint8_t _trace_level; + +#define SHOW_TRACE(lvl) \ + (ledger::_log_level >= ledger::LOG_TRACE && lvl <= ledger::_trace_level) +#define TRACE(lvl, msg) \ + (SHOW_TRACE(lvl) ? \ + ((ledger::_log_buffer << msg), \ + ledger::logger_func(ledger::LOG_TRACE)) : false) + +#else // TRACING_ON + +#define SHOW_TRACE(lvl) false +#define TRACE(lvl, msg) + +#endif // TRACING_ON + +#if defined(DEBUG_ON) + +extern optional<std::string> _log_category; + +inline bool category_matches(const char * cat) { + return _log_category && starts_with(cat, *_log_category); +} + +#define SHOW_DEBUG(cat) \ + (ledger::_log_level >= ledger::LOG_DEBUG && ledger::category_matches(cat)) +#define SHOW_DEBUG_() SHOW_DEBUG(_this_category) + +#define DEBUG(cat, msg) \ + (SHOW_DEBUG(cat) ? \ + ((ledger::_log_buffer << msg), \ + ledger::logger_func(ledger::LOG_DEBUG)) : false) +#define DEBUG_(msg) DEBUG(_this_category, msg) + +#else // DEBUG_ON + +#define SHOW_DEBUG(cat) false +#define SHOW_DEBUG_() false +#define DEBUG(cat, msg) +#define DEBUG_(msg) + +#endif // DEBUG_ON + +#define LOG_MACRO(level, msg) \ + (ledger::_log_level >= level ? \ + ((ledger::_log_buffer << msg), ledger::logger_func(level)) : false) + +#define SHOW_INFO() (ledger::_log_level >= ledger::LOG_INFO) +#define SHOW_WARN() (ledger::_log_level >= ledger::LOG_WARN) +#define SHOW_ERROR() (ledger::_log_level >= ledger::LOG_ERROR) +#define SHOW_FATAL() (ledger::_log_level >= ledger::LOG_FATAL) +#define SHOW_CRITICAL() (ledger::_log_level >= ledger::LOG_CRIT) + +#define INFO(msg) LOG_MACRO(ledger::LOG_INFO, msg) +#define WARN(msg) LOG_MACRO(ledger::LOG_WARN, msg) +#define ERROR(msg) LOG_MACRO(ledger::LOG_ERROR, msg) +#define FATAL(msg) LOG_MACRO(ledger::LOG_FATAL, msg) +#define CRITICAL(msg) LOG_MACRO(ledger::LOG_CRIT, msg) +#define EXCEPTION(msg) LOG_MACRO(ledger::LOG_EXCEPT, msg) + +} // namespace ledger + +#else // ! LOGGING_ON + +#define LOGGER(cat) + +#define SHOW_TRACE(lvl) false +#define SHOW_DEBUG(cat) false +#define SHOW_DEBUG_() false +#define SHOW_INFO() false +#define SHOW_WARN() false +#define SHOW_ERROR() false +#define SHOW_FATAL() false +#define SHOW_CRITICAL() false + +#define TRACE(lvl, msg) +#define DEBUG(cat, msg) +#define DEBUG_(msg) +#define INFO(msg) +#define WARN(msg) +#define ERROR(msg) +#define FATAL(msg) +#define CRITICAL(msg) + +#endif // LOGGING_ON + +#define IF_TRACE(lvl) if (SHOW_TRACE(lvl)) +#define IF_DEBUG(cat) if (SHOW_DEBUG(cat)) +#define IF_DEBUG_() if (SHOW_DEBUG_()) +#define IF_INFO() if (SHOW_INFO()) +#define IF_WARN() if (SHOW_WARN()) +#define IF_ERROR() if (SHOW_ERROR()) +#define IF_FATAL() if (SHOW_FATAL()) +#define IF_CRITICAL() if (SHOW_CRITICAL()) + +/*@}*/ + +/** + * @name Timers + * This allows log xacts to specify cumulative time spent. + */ +/*@{*/ + +#if defined(LOGGING_ON) && defined(TIMERS_ON) + +namespace ledger { + +void start_timer(const char * name, log_level_t lvl); +void stop_timer(const char * name); +void finish_timer(const char * name); + +#if defined(TRACING_ON) +#define TRACE_START(name, lvl, msg) \ + (SHOW_TRACE(lvl) ? \ + ((ledger::_log_buffer << msg), \ + ledger::start_timer(#name, ledger::LOG_TRACE)) : ((void)0)) +#define TRACE_STOP(name, lvl) \ + (SHOW_TRACE(lvl) ? ledger::stop_timer(#name) : ((void)0)) +#define TRACE_FINISH(name, lvl) \ + (SHOW_TRACE(lvl) ? ledger::finish_timer(#name) : ((void)0)) +#else +#define TRACE_START(name, lvl, msg) +#define TRACE_STOP(name) +#define TRACE_FINISH(name) +#endif + +#if defined(DEBUG_ON) +#define DEBUG_START(name, cat, msg) \ + (SHOW_DEBUG(cat) ? \ + ((ledger::_log_buffer << msg), \ + ledger::start_timer(#name, ledger::LOG_DEBUG)) : ((void)0)) +#define DEBUG_START_(name, msg) \ + DEBUG_START_(name, _this_category, msg) +#define DEBUG_STOP(name, cat) \ + (SHOW_DEBUG(cat) ? ledger::stop_timer(#name) : ((void)0)) +#define DEBUG_STOP_(name) \ + DEBUG_STOP_(name, _this_category) +#define DEBUG_FINISH(name, cat) \ + (SHOW_DEBUG(cat) ? ledger::finish_timer(#name) : ((void)0)) +#define DEBUG_FINISH_(name) \ + DEBUG_FINISH_(name, _this_category) +#else +#define DEBUG_START(name, cat, msg) +#define DEBUG_START_(name, msg) +#define DEBUG_STOP(name) +#define DEBUG_FINISH(name) +#endif + +#define INFO_START(name, msg) \ + (SHOW_INFO() ? \ + ((ledger::_log_buffer << msg), \ + ledger::start_timer(#name, ledger::LOG_INFO)) : ((void)0)) +#define INFO_STOP(name) \ + (SHOW_INFO() ? stop_timer(#name) : ((void)0)) +#define INFO_FINISH(name) \ + (SHOW_INFO() ? finish_timer(#name) : ((void)0)) + +} // namespace ledger + +#else // ! (LOGGING_ON && TIMERS_ON) + +#define TRACE_START(lvl, msg, name) +#define TRACE_STOP(name) +#define TRACE_FINISH(name) + +#define DEBUG_START(name, msg) +#define DEBUG_START_(name, cat, msg) +#define DEBUG_STOP(name) +#define DEBUG_FINISH(name) + +#define INFO_START(name, msg) +#define INFO_STOP(name) +#define INFO_FINISH(name) + +#endif // TIMERS_ON + +/*@}*/ + +/* + * These files define the other internal facilities. + */ + +#include "error.h" + +enum caught_signal_t { + NONE_CAUGHT, + INTERRUPTED, + PIPE_CLOSED +}; + +extern caught_signal_t caught_signal; + +void sigint_handler(int sig); +void sigpipe_handler(int sig); + +inline void check_for_signal() { + switch (caught_signal) { + case NONE_CAUGHT: + break; + case INTERRUPTED: + throw std::runtime_error(_("Interrupted by user (use Control-D to quit)")); + case PIPE_CLOSED: + throw std::runtime_error(_("Pipe terminated")); + } +} + +/** + * @name General utility functions + */ +/*@{*/ + +#define foreach BOOST_FOREACH + +namespace ledger { + +template <typename T, typename U> +inline T& downcast(U& object) { + return *polymorphic_downcast<T *>(&object); +} + +path resolve_path(const path& pathname); + +#ifdef HAVE_REALPATH +extern "C" char * realpath(const char *, char resolved_path[]); +#endif + +inline const string& either_or(const string& first, + const string& second) { + return first.empty() ? second : first; +} + +inline char * skip_ws(char * ptr) { + while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n') + ptr++; + return ptr; +} + +inline char * trim_ws(char * ptr) { + std::size_t len = std::strlen(ptr); + int i = int(len) - 1; + while (i >= 0 && (ptr[i] == ' ' || ptr[i] == '\t' || ptr[i] == '\n')) + ptr[i--] = '\0'; + return skip_ws(ptr); +} + +inline char * next_element(char * buf, bool variable = false) { + for (char * p = buf; *p; p++) { + if (! (*p == ' ' || *p == '\t')) + continue; + + if (! variable) { + *p = '\0'; + return skip_ws(p + 1); + } + else if (*p == '\t') { + *p = '\0'; + return skip_ws(p + 1); + } + else if (*(p + 1) == ' ') { + *p = '\0'; + return skip_ws(p + 2); + } + } + return NULL; +} + +inline char peek_next_nonws(std::istream& in) { + char c = static_cast<char>(in.peek()); + while (in.good() && ! in.eof() && std::isspace(c)) { + in.get(c); + c = static_cast<char>(in.peek()); + } + return c; +} + +#define READ_INTO(str, targ, size, var, cond) { \ + char * _p = targ; \ + var = static_cast<char>(str.peek()); \ + while (str.good() && ! str.eof() && var != '\n' && \ + (cond) && _p - targ < size) { \ + str.get(var); \ + if (str.eof()) \ + break; \ + if (var == '\\') { \ + str.get(var); \ + if (in.eof()) \ + break; \ + switch (var) { \ + case 'b': var = '\b'; break; \ + case 'f': var = '\f'; break; \ + case 'n': var = '\n'; break; \ + case 'r': var = '\r'; break; \ + case 't': var = '\t'; break; \ + case 'v': var = '\v'; break; \ + default: break; \ + } \ + } \ + *_p++ = var; \ + var = static_cast<char>(str.peek()); \ + } \ + *_p = '\0'; \ + } + +#define READ_INTO_(str, targ, size, var, idx, cond) { \ + char * _p = targ; \ + var = static_cast<char>(str.peek()); \ + while (str.good() && ! str.eof() && var != '\n' && \ + (cond) && _p - targ < size) { \ + str.get(var); \ + if (str.eof()) \ + break; \ + idx++; \ + if (var == '\\') { \ + str.get(var); \ + if (in.eof()) \ + break; \ + switch (var) { \ + case 'b': var = '\b'; break; \ + case 'f': var = '\f'; break; \ + case 'n': var = '\n'; break; \ + case 'r': var = '\r'; break; \ + case 't': var = '\t'; break; \ + case 'v': var = '\v'; break; \ + default: break; \ + } \ + idx++; \ + } \ + *_p++ = var; \ + var = static_cast<char>(str.peek()); \ + } \ + *_p = '\0'; \ + } + +inline string to_hex(uint_least32_t * message_digest, const int len = 1) +{ + std::ostringstream buf; + + for(int i = 0; i < 5 ; i++) { + buf.width(8); + buf.fill('0'); + buf << std::hex << message_digest[i]; + if (i + 1 >= len) + break; // only output the first LEN dwords + } + return buf.str(); +} + +class push_xml +{ + std::ostream& out; + string tag; + bool leave_open; + +public: + push_xml(std::ostream& _out, const string& _tag, bool has_attrs = false, + bool _leave_open = false) + : out(_out), tag(_tag), leave_open(_leave_open) { + out << '<' << tag; + if (! has_attrs) + out << '>'; + } + ~push_xml() { + if (! leave_open) + out << "</" << tag << '>'; + } + + void close_attrs() { + out << '>'; + } + + static string guard(const string& str) { + std::ostringstream buf; + foreach (const char& ch, str) { + switch (ch) { + case '<': + buf << "<"; + break; + case '>': + buf << ">"; + break; + case '&': + buf << "&"; + break; + default: + buf << ch; + break; + } + } + return buf.str(); + } +}; + +extern const string version; + +} // namespace ledger + +/*@}*/ + +#endif // _UTILS_H diff --git a/src/value.cc b/src/value.cc new file mode 100644 index 00000000..cce4c4e8 --- /dev/null +++ b/src/value.cc @@ -0,0 +1,1864 @@ +/* + * Copyright (c) 2003-2009, 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 "value.h" +#include "commodity.h" +#include "annotate.h" +#include "pool.h" +#include "unistring.h" // for justify() + +namespace ledger { + +intrusive_ptr<value_t::storage_t> value_t::true_value; +intrusive_ptr<value_t::storage_t> value_t::false_value; + +value_t::storage_t& value_t::storage_t::operator=(const value_t::storage_t& rhs) +{ + type = rhs.type; + + switch (type) { + case BALANCE: + data = new balance_t(*boost::get<balance_t *>(rhs.data)); + break; + case SEQUENCE: + data = new sequence_t(*boost::get<sequence_t *>(rhs.data)); + break; + + default: + data = rhs.data; + break; + } + + return *this; +} + +void value_t::initialize() +{ + true_value = new storage_t; + true_value->type = BOOLEAN; + true_value->data = true; + + false_value = new storage_t; + false_value->type = BOOLEAN; + false_value->data = false; +} + +void value_t::shutdown() +{ + true_value = intrusive_ptr<storage_t>(); + false_value = intrusive_ptr<storage_t>(); +} + +value_t::operator bool() const +{ + switch (type()) { + case VOID: + return false; + case BOOLEAN: + return as_boolean(); + case DATETIME: + return is_valid(as_datetime()); + case DATE: + return is_valid(as_date()); + case INTEGER: + return as_long(); + case AMOUNT: + return as_amount(); + case BALANCE: + return as_balance(); + case STRING: + return ! as_string().empty(); + case MASK: { + std::ostringstream out; + out << *this; + throw_(value_error, + _("Cannot determine truth of %1 (did you mean 'account =~ %2'?)") + << label() << out.str()); + } + case SEQUENCE: + if (! as_sequence().empty()) { + foreach (const value_t& value, as_sequence()) { + if (value) + return true; + } + } + return false; + case SCOPE: + return as_scope() != NULL; + default: + break; + } + + throw_(value_error, _("Cannot determine truth of %1") << label()); + return false; +} + +void value_t::set_type(type_t new_type) +{ + if (new_type == VOID) { +#if BOOST_VERSION >= 103700 + storage.reset(); +#else + storage = intrusive_ptr<storage_t>(); +#endif + } else { + if (! storage || storage->refc > 1) + storage = new storage_t; + else + storage->destroy(); + storage->type = new_type; + } +} + +bool value_t::to_boolean() const +{ + if (is_boolean()) { + return as_boolean(); + } else { + value_t temp(*this); + temp.in_place_cast(BOOLEAN); + return temp.as_boolean(); + } +} + +datetime_t value_t::to_datetime() const +{ + if (is_datetime()) { + return as_datetime(); + } else { + value_t temp(*this); + temp.in_place_cast(DATETIME); + return temp.as_datetime(); + } +} + +date_t value_t::to_date() const +{ + if (is_date()) { + return as_date(); + } else { + value_t temp(*this); + temp.in_place_cast(DATE); + return temp.as_date(); + } +} + +int value_t::to_int() const +{ + if (is_long()) { + return static_cast<int>(as_long()); + } else { + value_t temp(*this); + temp.in_place_cast(INTEGER); + return static_cast<int>(temp.as_long()); + } +} + +long value_t::to_long() const +{ + if (is_long()) { + return as_long(); + } else { + value_t temp(*this); + temp.in_place_cast(INTEGER); + return temp.as_long(); + } +} + +amount_t value_t::to_amount() const +{ + if (is_amount()) { + return as_amount(); + } else { + value_t temp(*this); + temp.in_place_cast(AMOUNT); + return temp.as_amount(); + } +} + +balance_t value_t::to_balance() const +{ + if (is_balance()) { + return as_balance(); + } else { + value_t temp(*this); + temp.in_place_cast(BALANCE); + return temp.as_balance(); + } +} + +string value_t::to_string() const +{ + if (is_string()) { + return as_string(); + } else { + value_t temp(*this); + temp.in_place_cast(STRING); + return temp.as_string(); + } +} + +mask_t value_t::to_mask() const +{ + if (is_mask()) { + return as_mask(); + } else { + value_t temp(*this); + temp.in_place_cast(MASK); + return temp.as_mask(); + } +} + +value_t::sequence_t value_t::to_sequence() const +{ + if (is_sequence()) { + return as_sequence(); + } else { + value_t temp(*this); + temp.in_place_cast(SEQUENCE); + return temp.as_sequence(); + } +} + + +void value_t::in_place_simplify() +{ +#if defined(DEBUG_ON) + LOGGER("value.simplify"); +#endif + + if (is_realzero()) { + DEBUG_("Zeroing type " << static_cast<int>(type())); + set_long(0L); + return; + } + + if (is_balance() && as_balance().single_amount()) { + DEBUG_("Reducing balance to amount"); + DEBUG_("as a balance it looks like: " << *this); + in_place_cast(AMOUNT); + DEBUG_("as an amount it looks like: " << *this); + } + +#ifdef REDUCE_TO_INTEGER // this is off by default + if (is_amount() && ! as_amount().has_commodity() && + as_amount().fits_in_long()) { + DEBUG_("Reducing amount to integer"); + in_place_cast(INTEGER); + } +#endif +} + +value_t value_t::number() const +{ + switch (type()) { + case VOID: + return 0L; + case BOOLEAN: + return as_boolean() ? 1L : 0L; + case INTEGER: + return as_long(); + case AMOUNT: + return as_amount().number(); + case BALANCE: + return as_balance().number(); + case SEQUENCE: + if (! as_sequence().empty()) { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp += value.number(); + return temp; + } + break; + default: + break; + } + + throw_(value_error, _("Cannot determine numeric value of %1") << label()); + return false; +} + +value_t& value_t::operator+=(const value_t& val) +{ + if (is_string()) { + if (val.is_string()) + as_string_lval() += val.as_string(); + else + as_string_lval() += val.to_string(); + return *this; + } + else if (is_sequence()) { + if (val.is_sequence()) { + if (size() == val.size()) { + sequence_t::iterator i = begin(); + sequence_t::const_iterator j = val.begin(); + + for (; i != end(); i++, j++) + *i += *j; + } else { + throw_(value_error, _("Cannot add sequences of different lengths")); + } + } else { + as_sequence_lval().push_back(val); + } + return *this; + } + + switch (type()) { + case DATETIME: + switch (val.type()) { + case INTEGER: + as_datetime_lval() += + time_duration_t(0, 0, static_cast<time_duration_t::sec_type>(val.as_long())); + return *this; + case AMOUNT: + as_datetime_lval() += + time_duration_t(0, 0, static_cast<time_duration_t::sec_type> + (val.as_amount().to_long())); + return *this; + default: + break; + } + break; + + case DATE: + switch (val.type()) { + case INTEGER: + as_date_lval() += gregorian::date_duration(val.as_long()); + return *this; + case AMOUNT: + as_date_lval() += gregorian::date_duration(val.as_amount().to_long()); + return *this; + default: + break; + } + break; + + case INTEGER: + switch (val.type()) { + case INTEGER: + as_long_lval() += val.as_long(); + return *this; + case AMOUNT: + if (val.as_amount().has_commodity()) { + in_place_cast(BALANCE); + return *this += val; + } + in_place_cast(AMOUNT); + as_amount_lval() += val.as_amount(); + return *this; + case BALANCE: + in_place_cast(BALANCE); + as_balance_lval() += val.as_balance(); + return *this; + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + if (as_amount().has_commodity()) { + in_place_cast(BALANCE); + return *this += val; + } else { + as_amount_lval() += val.as_long(); + return *this; + } + break; + + case AMOUNT: + if (as_amount().commodity() != val.as_amount().commodity()) { + in_place_cast(BALANCE); + return *this += val; + } else { + as_amount_lval() += val.as_amount(); + return *this; + } + break; + + case BALANCE: + in_place_cast(BALANCE); + as_balance_lval() += val.as_balance(); + return *this; + + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + as_balance_lval() += val.to_amount(); + return *this; + case AMOUNT: + as_balance_lval() += val.as_amount(); + return *this; + case BALANCE: + as_balance_lval() += val.as_balance(); + return *this; + default: + break; + } + break; + + default: + break; + } + + throw_(value_error, _("Cannot add %1 to %2") << val.label() << label()); + return *this; +} + +value_t& value_t::operator-=(const value_t& val) +{ + if (is_sequence()) { + sequence_t& seq(as_sequence_lval()); + + if (val.is_sequence()) { + if (size() == val.size()) { + sequence_t::iterator i = begin(); + sequence_t::const_iterator j = val.begin(); + + for (; i != end(); i++, j++) + *i -= *j; + } else { + throw_(value_error, _("Cannot subtract sequences of different lengths")); + } + } else { + sequence_t::iterator i = std::find(seq.begin(), seq.end(), val); + if (i != seq.end()) + seq.erase(i); + } + return *this; + } + + switch (type()) { + case DATETIME: + switch (val.type()) { + case INTEGER: + as_datetime_lval() -= + time_duration_t(0, 0, static_cast<time_duration_t::sec_type>(val.as_long())); + return *this; + case AMOUNT: + as_datetime_lval() -= + time_duration_t(0, 0, static_cast<time_duration_t::sec_type> + (val.as_amount().to_long())); + return *this; + default: + break; + } + break; + + case DATE: + switch (val.type()) { + case INTEGER: + as_date_lval() -= gregorian::date_duration(val.as_long()); + return *this; + case AMOUNT: + as_date_lval() -= gregorian::date_duration(val.as_amount().to_long()); + return *this; + default: + break; + } + break; + + case INTEGER: + switch (val.type()) { + case INTEGER: + as_long_lval() -= val.as_long(); + return *this; + case AMOUNT: + in_place_cast(AMOUNT); + as_amount_lval() -= val.as_amount(); + in_place_simplify(); + return *this; + case BALANCE: + in_place_cast(BALANCE); + as_balance_lval() -= val.as_balance(); + in_place_simplify(); + return *this; + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + if (as_amount().has_commodity()) { + in_place_cast(BALANCE); + *this -= val; + in_place_simplify(); + return *this; + } else { + as_amount_lval() -= val.as_long(); + in_place_simplify(); + return *this; + } + break; + + case AMOUNT: + if (as_amount().commodity() != val.as_amount().commodity()) { + in_place_cast(BALANCE); + *this -= val; + in_place_simplify(); + return *this; + } else { + as_amount_lval() -= val.as_amount(); + in_place_simplify(); + return *this; + } + break; + + case BALANCE: + in_place_cast(BALANCE); + as_balance_lval() -= val.as_balance(); + in_place_simplify(); + return *this; + + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + as_balance_lval() -= val.to_amount(); + in_place_simplify(); + return *this; + case AMOUNT: + as_balance_lval() -= val.as_amount(); + in_place_simplify(); + return *this; + case BALANCE: + as_balance_lval() -= val.as_balance(); + in_place_simplify(); + return *this; + default: + break; + } + break; + + default: + break; + } + + throw_(value_error, _("Cannot subtract %1 from %2") << val.label() << label()); + + return *this; +} + +value_t& value_t::operator*=(const value_t& val) +{ + if (is_string()) { + string temp; + long count = val.to_long(); + for (long i = 0; i < count; i++) + temp += as_string(); + set_string(temp); + return *this; + } + else if (is_sequence()) { + value_t temp; + long count = val.to_long(); + for (long i = 0; i < count; i++) + temp += as_sequence(); + return *this = temp; + } + + switch (type()) { + case INTEGER: + switch (val.type()) { + case INTEGER: + as_long_lval() *= val.as_long(); + return *this; + case AMOUNT: + set_amount(val.as_amount() * as_long()); + return *this; + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + as_amount_lval() *= val.as_long(); + return *this; + case AMOUNT: + as_amount_lval() *= val.as_amount(); + return *this; + case BALANCE: + if (val.as_balance().single_amount()) { + as_amount_lval() *= val.simplified().as_amount(); + return *this; + } + break; + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + as_balance_lval() *= val.as_long(); + return *this; + case AMOUNT: + if (as_balance().single_amount()) { + in_place_simplify(); + as_amount_lval() *= val.as_amount(); + return *this; + } + else if (! val.as_amount().has_commodity()) { + as_balance_lval() *= val.as_amount(); + return *this; + } + break; + default: + break; + } + break; + + default: + break; + } + + DEBUG("value.multiply.error", "Left: " << *this); + DEBUG("value.multiply.error", "Right: " << val); + + throw_(value_error, _("Cannot multiply %1 with %2") << label() << val.label()); + + return *this; +} + +value_t& value_t::operator/=(const value_t& val) +{ + switch (type()) { + case INTEGER: + switch (val.type()) { + case INTEGER: + as_long_lval() /= val.as_long(); + return *this; + case AMOUNT: + set_amount(val.as_amount() / as_long()); + return *this; + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + as_amount_lval() /= val.as_long(); + return *this; + + case AMOUNT: + as_amount_lval() /= val.as_amount(); + return *this; + case BALANCE: + if (val.as_balance().single_amount()) { + value_t simpler(val.simplified()); + switch (simpler.type()) { + case INTEGER: + as_amount_lval() /= simpler.as_long(); + break; + case AMOUNT: + as_amount_lval() /= simpler.as_amount(); + break; + default: + assert(false); + break; + } + return *this; + } + break; + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + as_balance_lval() /= val.as_long(); + return *this; + case AMOUNT: + if (as_balance().single_amount()) { + in_place_simplify(); + as_amount_lval() /= val.as_amount(); + return *this; + } + else if (! val.as_amount().has_commodity()) { + as_balance_lval() /= val.as_amount(); + return *this; + } + break; + default: + break; + } + break; + + default: + break; + } + + throw_(value_error, _("Cannot divide %1 by %2") << label() << val.label()); + + return *this; +} + + +bool value_t::is_equal_to(const value_t& val) const +{ + switch (type()) { + case VOID: + return val.type() == VOID; + + case BOOLEAN: + if (val.is_boolean()) + return as_boolean() == val.as_boolean(); + break; + + case DATETIME: + if (val.is_datetime()) + return as_datetime() == val.as_datetime(); + break; + + case DATE: + if (val.is_date()) + return as_date() == val.as_date(); + break; + + case INTEGER: + switch (val.type()) { + case INTEGER: + return as_long() == val.as_long(); + case AMOUNT: + return val.as_amount() == to_amount(); + case BALANCE: + return val.as_balance() == to_amount(); + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + return as_amount() == val.as_long(); + case AMOUNT: + return as_amount() == val.as_amount(); + case BALANCE: + return val.as_balance() == as_amount(); + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + return as_balance() == val.to_amount(); + case AMOUNT: + return as_balance() == val.as_amount(); + case BALANCE: + return as_balance() == val.as_balance(); + default: + break; + } + break; + + case STRING: + if (val.is_string()) + return as_string() == val.as_string(); + break; + + case MASK: + if (val.is_mask()) + return as_mask() == val.as_mask(); + break; + + case SEQUENCE: + if (val.is_sequence()) + return as_sequence() == val.as_sequence(); + break; + + default: + break; + } + + throw_(value_error, _("Cannot compare %1 by %2") << label() << val.label()); + + return *this; +} + +bool value_t::is_less_than(const value_t& val) const +{ + switch (type()) { + case DATETIME: + if (val.is_datetime()) + return as_datetime() < val.as_datetime(); + break; + + case DATE: + if (val.is_date()) + return as_date() < val.as_date(); + break; + + case INTEGER: + switch (val.type()) { + case INTEGER: + return as_long() < val.as_long(); + case AMOUNT: + return val.as_amount() >= as_long(); + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + return as_amount() < val.as_long(); + case AMOUNT: + try { + return as_amount() < val.as_amount(); + } + catch (const amount_error&) { + return compare_amount_commodities()(&as_amount(), &val.as_amount()); + } + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + case AMOUNT: { + if (val.is_nonzero()) + break; + + bool no_amounts = true; + foreach (const balance_t::amounts_map::value_type& pair, + as_balance().amounts) { + if (pair.second >= 0L) + return false; + no_amounts = false; + } + return ! no_amounts; + } + default: + break; + } + break; + + case STRING: + if (val.is_string()) + return as_string() < val.as_string(); + break; + + case SEQUENCE: + switch (val.type()) { + case INTEGER: + case AMOUNT: { + if (val.is_nonzero()) + break; + + bool no_amounts = true; + foreach (const value_t& value, as_sequence()) { + if (value >= 0L) + return false; + no_amounts = false; + } + return ! no_amounts; + } + default: + break; + } + break; + + default: + break; + } + + throw_(value_error, _("Cannot compare %1 to %2") << label() << val.label()); + + return *this; +} + +bool value_t::is_greater_than(const value_t& val) const +{ + switch (type()) { + case DATETIME: + if (val.is_datetime()) + return as_datetime() > val.as_datetime(); + break; + + case DATE: + if (val.is_date()) + return as_date() > val.as_date(); + break; + + case INTEGER: + switch (val.type()) { + case INTEGER: + return as_long() > val.as_long(); + case AMOUNT: + return val.as_amount() > as_long(); + default: + break; + } + break; + + case AMOUNT: + switch (val.type()) { + case INTEGER: + return as_amount() > val.as_long(); + case AMOUNT: + return as_amount() > val.as_amount(); + default: + break; + } + break; + + case BALANCE: + switch (val.type()) { + case INTEGER: + case AMOUNT: { + if (val.is_nonzero()) + break; + + bool no_amounts = true; + foreach (const balance_t::amounts_map::value_type& pair, + as_balance().amounts) { + if (pair.second <= 0L) + return false; + no_amounts = false; + } + return ! no_amounts; + } + default: + break; + } + break; + + case STRING: + if (val.is_string()) + return as_string() > val.as_string(); + break; + + case SEQUENCE: + switch (val.type()) { + case INTEGER: + case AMOUNT: { + if (val.is_nonzero()) + break; + + bool no_amounts = true; + foreach (const value_t& value, as_sequence()) { + if (value <= 0L) + return false; + no_amounts = false; + } + return ! no_amounts; + } + default: + break; + } + break; + + default: + break; + } + + throw_(value_error, _("Cannot compare %1 to %2") << label() << val.label()); + + return *this; +} + +void value_t::in_place_cast(type_t cast_type) +{ + if (type() == cast_type) + return; + + _dup(); + + if (cast_type == BOOLEAN) { + set_boolean(bool(*this)); + return; + } + else if (cast_type == SEQUENCE) { + sequence_t temp; + if (! is_null()) + temp.push_back(*this); + set_sequence(temp); + return; + } + + switch (type()) { + case BOOLEAN: + switch (cast_type) { + case AMOUNT: + set_amount(as_boolean() ? 1L : 0L); + return; + case STRING: + set_string(as_boolean() ? "true" : "false"); + return; + default: + break; + } + break; + + case DATE: + switch (cast_type) { + case DATETIME: + set_datetime(datetime_t(as_date(), time_duration(0, 0, 0, 0))); + return; + case STRING: + set_string(format_date(as_date(), FMT_WRITTEN)); + return; + default: + break; + } + break; + case DATETIME: + switch (cast_type) { + case DATE: + set_date(as_datetime().date()); + return; + case STRING: + set_string(format_datetime(as_datetime(), FMT_WRITTEN)); + return; + default: + break; + } + break; + + case INTEGER: + switch (cast_type) { + case AMOUNT: + set_amount(as_long()); + return; + case BALANCE: + set_balance(to_amount()); + return; + case STRING: + set_string(lexical_cast<string>(as_long())); + return; + default: + break; + } + break; + + case AMOUNT: { + const amount_t& amt(as_amount()); + switch (cast_type) { + case INTEGER: + if (amt.is_null()) + set_long(0L); + else + set_long(as_amount().to_long()); + return; + case BALANCE: + if (amt.is_null()) + set_balance(balance_t()); + else + set_balance(as_amount()); + return; + case STRING: + if (amt.is_null()) + set_string(""); + else + set_string(as_amount().to_string()); + return; + default: + break; + } + break; + } + + case BALANCE: { + const balance_t& bal(as_balance()); + switch (cast_type) { + case AMOUNT: { + if (bal.amounts.size() == 1) { + // Because we are changing the current balance value to an amount + // value, and because set_amount takes a reference (and that memory is + // about to be repurposed), we must pass in a copy. + set_amount(amount_t((*bal.amounts.begin()).second)); + return; + } + else if (bal.amounts.size() == 0) { + set_amount(0L); + return; + } + else { + throw_(value_error, _("Cannot convert %1 with multiple commodities to %2") + << label() << label(cast_type)); + } + break; + } + case STRING: + if (bal.is_empty()) + set_string(""); + else + set_string(as_balance().to_string()); + return; + default: + break; + } + break; + } + + case STRING: + switch (cast_type) { + case INTEGER: { + if (all(as_string(), is_digit())) { + set_long(lexical_cast<long>(as_string())); + return; + } + break; + } + case AMOUNT: + set_amount(amount_t(as_string())); + return; + case DATE: + set_date(parse_date(as_string())); + return; + case DATETIME: + set_datetime(parse_datetime(as_string())); + return; + case MASK: + set_mask(as_string()); + return; + default: + break; + } + break; + + default: + break; + } + + throw_(value_error, + _("Cannot convert %1 to %2") << label() << label(cast_type)); +} + +void value_t::in_place_negate() +{ + switch (type()) { + case BOOLEAN: + set_boolean(! as_boolean()); + return; + case INTEGER: + case DATETIME: + set_long(- as_long()); + return; + case DATE: + set_long(- as_long()); + return; + case AMOUNT: + as_amount_lval().in_place_negate(); + return; + case BALANCE: + as_balance_lval().in_place_negate(); + return; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(- value); + *this = temp; + return; + } + default: + break; + } + + throw_(value_error, _("Cannot negate %1") << label()); +} + +void value_t::in_place_not() +{ + switch (type()) { + case BOOLEAN: + set_boolean(! as_boolean()); + return; + case INTEGER: + case DATETIME: + set_boolean(! as_long()); + return; + case DATE: + set_boolean(! as_long()); + return; + case AMOUNT: + set_boolean(! as_amount()); + return; + case BALANCE: + set_boolean(! as_balance()); + return; + case STRING: + set_boolean(as_string().empty()); + return; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(! value); + *this = temp; + return; + } + default: + break; + } + + throw_(value_error, _("Cannot 'not' %1") << label()); +} + +bool value_t::is_realzero() const +{ + switch (type()) { + case BOOLEAN: + return ! as_boolean(); + case INTEGER: + return as_long() == 0; + case DATETIME: + return ! is_valid(as_datetime()); + case DATE: + return ! is_valid(as_date()); + case AMOUNT: + return as_amount().is_realzero(); + case BALANCE: + return as_balance().is_realzero(); + case STRING: + return as_string().empty(); + case SEQUENCE: + return as_sequence().empty(); + + case SCOPE: + return as_scope() == NULL; + + default: + throw_(value_error, _("Cannot determine if %1 is really zero") << label()); + } + return false; +} + +bool value_t::is_zero() const +{ + switch (type()) { + case BOOLEAN: + return ! as_boolean(); + case INTEGER: + return as_long() == 0; + case DATETIME: + return ! is_valid(as_datetime()); + case DATE: + return ! is_valid(as_date()); + case AMOUNT: + return as_amount().is_zero(); + case BALANCE: + return as_balance().is_zero(); + case STRING: + return as_string().empty(); + case SEQUENCE: + return as_sequence().empty(); + + case SCOPE: + return as_scope() == NULL; + + default: + throw_(value_error, _("Cannot determine if %1 is zero") << label()); + } + return false; +} + +value_t value_t::value(const bool primary_only, + const optional<datetime_t>& moment, + const optional<commodity_t&>& in_terms_of) const +{ + switch (type()) { + case INTEGER: + return NULL_VALUE; + + case AMOUNT: + if (optional<amount_t> val = + as_amount().value(primary_only, moment, in_terms_of)) + return *val; + return NULL_VALUE; + + case BALANCE: + if (optional<balance_t> bal = + as_balance().value(primary_only, moment, in_terms_of)) + return *bal; + return NULL_VALUE; + + default: + break; + } + + throw_(value_error, _("Cannot find the value of %1") << label()); + return NULL_VALUE; +} + +value_t value_t::price() const +{ + switch (type()) { + case AMOUNT: + return as_amount().price(); + case BALANCE: + return as_balance().price(); + default: + return *this; + } +} + +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(false, moment, *commodity); + if (! result.is_null()) + return result; + } + } + return *this; +} + +void value_t::in_place_reduce() +{ + switch (type()) { + case AMOUNT: + as_amount_lval().in_place_reduce(); + return; + case BALANCE: + as_balance_lval().in_place_reduce(); + return; + default: + return; + } + + //throw_(value_error, "Cannot reduce " << label()); +} + +void value_t::in_place_unreduce() +{ + switch (type()) { + case AMOUNT: + as_amount_lval().in_place_unreduce(); + return; + case BALANCE: + as_balance_lval().in_place_unreduce(); + return; + default: + return; + } + + //throw_(value_error, "Cannot reduce " << label()); +} + +value_t value_t::abs() const +{ + switch (type()) { + case INTEGER: { + long val = as_long(); + if (val < 0) + return - val; + return val; + } + case AMOUNT: + return as_amount().abs(); + case BALANCE: + return as_balance().abs(); + default: + break; + } + + throw_(value_error, _("Cannot abs %1") << label()); + return NULL_VALUE; +} + +void value_t::in_place_round() +{ + switch (type()) { + case INTEGER: + return; + case AMOUNT: + as_amount_lval().in_place_round(); + return; + case BALANCE: + as_balance_lval().in_place_round(); + return; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(value.rounded()); + *this = temp; + return; + } + default: + break; + } + + throw_(value_error, _("Cannot set rounding for %1") << label()); +} + +void value_t::in_place_truncate() +{ + switch (type()) { + case INTEGER: + return; + case AMOUNT: + as_amount_lval().in_place_truncate(); + return; + case BALANCE: + as_balance_lval().in_place_truncate(); + return; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(value.truncated()); + *this = temp; + return; + } + default: + break; + } + + throw_(value_error, _("Cannot truncate %1") << label()); +} + +void value_t::in_place_floor() +{ + switch (type()) { + case INTEGER: + return; + case AMOUNT: + as_amount_lval().in_place_floor(); + return; + case BALANCE: + as_balance_lval().in_place_floor(); + return; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(value.floored()); + *this = temp; + return; + } + default: + break; + } + + throw_(value_error, _("Cannot floor %1") << label()); +} + +void value_t::in_place_unround() +{ + switch (type()) { + case INTEGER: + return; + case AMOUNT: + as_amount_lval().in_place_unround(); + return; + case BALANCE: + as_balance_lval().in_place_unround(); + return; + case SEQUENCE: { + value_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(value.unrounded()); + *this = temp; + return; + } + default: + break; + } + + throw_(value_error, _("Cannot unround %1") << label()); +} + +void value_t::annotate(const annotation_t& details) +{ + if (is_amount()) + as_amount_lval().annotate(details); + else + throw_(value_error, _("Cannot annotate %1") << label()); +} + +bool value_t::has_annotation() const +{ + if (is_amount()) + return as_amount().has_annotation(); + else + throw_(value_error, + _("Cannot determine whether %1 is annotated") << label()); + return false; +} + +annotation_t& value_t::annotation() +{ + if (is_amount()) + return as_amount_lval().annotation(); + else { + throw_(value_error, _("Cannot request annotation of %1") << label()); + return as_amount_lval().annotation(); // quiet g++ warning + } +} + +value_t value_t::strip_annotations(const keep_details_t& what_to_keep) const +{ + if (what_to_keep.keep_all()) + return *this; + + switch (type()) { + case VOID: + case BOOLEAN: + case INTEGER: + case DATETIME: + case DATE: + case STRING: + case MASK: + case SCOPE: + return *this; + + case SEQUENCE: { + sequence_t temp; + foreach (const value_t& value, as_sequence()) + temp.push_back(value.strip_annotations(what_to_keep)); + return temp; + } + + case AMOUNT: + return as_amount().strip_annotations(what_to_keep); + case BALANCE: + return as_balance().strip_annotations(what_to_keep); + + default: + assert(false); + break; + } + assert(false); + return NULL_VALUE; +} + +void value_t::print(std::ostream& out, + const int first_width, + const int latter_width, + const bool right_justify, + const bool colorize) const +{ + if (first_width > 0 && + (! is_amount() || as_amount().is_zero()) && + ! is_balance() && ! is_string()) { + out.width(first_width); + + if (right_justify) + out << std::right; + else + out << std::left; + } + + switch (type()) { + case VOID: + out << ""; + break; + + case BOOLEAN: + out << (as_boolean() ? "1" : "0"); + break; + + case DATETIME: + out << format_datetime(as_datetime(), FMT_WRITTEN); + break; + + case DATE: + out << format_date(as_date(), FMT_WRITTEN); + break; + + case INTEGER: + if (colorize && as_long() < 0) + justify(out, to_string(), first_width, right_justify, true); + else + out << as_long(); + break; + + case AMOUNT: { + if (as_amount().is_zero()) { + out << 0; + } else { + std::ostringstream buf; + buf << as_amount(); + justify(out, buf.str(), first_width, right_justify, + colorize && as_amount().sign() < 0); + } + break; + } + + case BALANCE: + as_balance().print(out, first_width, latter_width, right_justify, + colorize); + break; + + case STRING: + if (first_width > 0) + justify(out, as_string(), first_width, right_justify); + else + out << as_string(); + break; + + case MASK: + out << '/' << as_mask() << '/'; + break; + + case SEQUENCE: { + out << '('; + bool first = true; + foreach (const value_t& value, as_sequence()) { + if (first) + first = false; + else + out << ", "; + + value.print(out, first_width, latter_width, right_justify, + colorize); + } + out << ')'; + break; + } + + case SCOPE: + out << "<SCOPE>"; + break; + + default: + throw_(value_error, _("Cannot print %1") << label()); + } +} + +void value_t::dump(std::ostream& out, const bool relaxed) const +{ + switch (type()) { + case VOID: + out << "null"; + break; + + case BOOLEAN: + if (as_boolean()) + out << "true"; + else + out << "false"; + break; + + case DATETIME: + out << '[' << format_datetime(as_datetime(), FMT_WRITTEN) << ']'; + break; + case DATE: + out << '[' << format_date(as_date(), FMT_WRITTEN) << ']'; + break; + + case INTEGER: + out << as_long(); + break; + + case AMOUNT: + if (! relaxed) + out << '{'; + out << as_amount(); + if (! relaxed) + out << '}'; + break; + + case BALANCE: + out << as_balance(); + break; + + case STRING: + out << '"'; + foreach (const char& ch, as_string()) { + switch (ch) { + case '"': + out << "\\\""; + break; + case '\\': + out << "\\\\"; + break; + default: + out << ch; + break; + } + } + out << '"'; + break; + + case MASK: + out << '/' << as_mask() << '/'; + break; + + case SCOPE: + out << as_scope(); + break; + + case SEQUENCE: { + out << '('; + bool first = true; + foreach (const value_t& value, as_sequence()) { + if (first) + first = false; + else + out << ", "; + + value.dump(out, relaxed); + } + out << ')'; + break; + } + + default: + assert(false); + break; + } +} + +bool value_t::valid() const +{ + switch (type()) { + case AMOUNT: + return as_amount().valid(); + case BALANCE: + return as_balance().valid(); + default: + break; + } + return true; +} + +bool sort_value_is_less_than(const std::list<sort_value_t>& left_values, + const std::list<sort_value_t>& right_values) +{ + std::list<sort_value_t>::const_iterator left_iter = left_values.begin(); + std::list<sort_value_t>::const_iterator right_iter = right_values.begin(); + + while (left_iter != left_values.end() && right_iter != right_values.end()) { + // Don't even try to sort balance values + if (! (*left_iter).value.is_balance() && + ! (*right_iter).value.is_balance()) { + DEBUG("value.sort", + " Comparing " << (*left_iter).value << " < " << (*right_iter).value); + if ((*left_iter).value < (*right_iter).value) { + DEBUG("value.sort", " is less"); + return ! (*left_iter).inverted; + } + else if ((*left_iter).value > (*right_iter).value) { + DEBUG("value.sort", " is greater"); + return (*left_iter).inverted; + } + } + left_iter++; right_iter++; + } + + assert(left_iter == left_values.end()); + assert(right_iter == right_values.end()); + + return false; +} + +void to_xml(std::ostream& out, const value_t& value) +{ + switch (value.type()) { + case value_t::VOID: + out << "<void />"; + break; + case value_t::BOOLEAN: { + push_xml y(out, "boolean"); + out << (value.as_boolean() ? "true" : "false"); + break; + } + case value_t::INTEGER: { + push_xml y(out, "integer"); + out << value.as_long(); + break; + } + + case value_t::AMOUNT: + to_xml(out, value.as_amount()); + break; + case value_t::BALANCE: + to_xml(out, value.as_balance()); + break; + + case value_t::DATETIME: + to_xml(out, value.as_datetime()); + break; + case value_t::DATE: + to_xml(out, value.as_date()); + break; + case value_t::STRING: { + push_xml y(out, "string"); + out << y.guard(value.as_string()); + break; + } + case value_t::MASK: + to_xml(out, value.as_mask()); + break; + + case value_t::SEQUENCE: { + push_xml y(out, "sequence"); + foreach (const value_t& member, value.as_sequence()) + to_xml(out, member); + break; + } + + case value_t::SCOPE: + default: + assert(false); + break; + } +} + +} // namespace ledger diff --git a/src/value.h b/src/value.h new file mode 100644 index 00000000..ffbb89e8 --- /dev/null +++ b/src/value.h @@ -0,0 +1,998 @@ +/* + * Copyright (c) 2003-2009, 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 value.h + * @author John Wiegley + * + * @ingroup math + * + * @brief Abstract dynamic type representing various numeric types + * + * A value_t object can be one of many types, and changes its type + * dynamically based on how it is used. For example, if you assign + * the number 10 to a value object, it's internal type will be + * INTEGER. + */ +#ifndef _VALUE_H +#define _VALUE_H + +#include "balance.h" // includes amount.h +#include "mask.h" + +namespace ledger { + +DECLARE_EXCEPTION(value_error, std::runtime_error); + +class scope_t; + +/** + * @class value_t + * + * @brief Dynamic type representing various numeric types. + * + * The following type is a polymorphous value type used solely for + * performance reasons. The alternative is to compute value + * expressions (valexpr.cc) in terms of the largest data type, + * balance_t. This was found to be prohibitively expensive, especially + * when large logic chains were involved, since many temporary + * allocations would occur for every operator. With value_t, and the + * fact that logic chains only need boolean values to continue, no + * memory allocations need to take place at all. + */ +class value_t + : public ordered_field_operators<value_t, + equality_comparable<value_t, balance_t, + additive<value_t, balance_t, + multiplicative<value_t, balance_t, + ordered_field_operators<value_t, amount_t, + ordered_field_operators<value_t, double, + ordered_field_operators<value_t, unsigned long, + ordered_field_operators<value_t, long> > > > > > > > +{ +public: + /** + * The sequence_t member type abstracts the type used to represent a + * resizable "array" of value_t objects. + */ + typedef std::deque<value_t> sequence_t; + typedef sequence_t::iterator iterator; + typedef sequence_t::const_iterator const_iterator; + typedef sequence_t::difference_type difference_type; + + /** + * type_t gives the type of the data contained or referenced by a + * value_t object. Use the type() method to get a value of type + * type_t. + */ + enum type_t { + VOID, // a null value (i.e., uninitialized) + BOOLEAN, // a boolean + DATETIME, // a date and time (Boost posix_time) + DATE, // a date (Boost gregorian::date) + INTEGER, // a signed integer value + AMOUNT, // a ledger::amount_t + BALANCE, // a ledger::balance_t + STRING, // a string object + MASK, // a regular expression mask + SEQUENCE, // a vector of value_t objects + SCOPE // a pointer to a scope + }; + +private: + class storage_t + { + friend class value_t; + + /** + * The `data' member holds the actual bytes relating to whatever + * has been stuffed into this storage object. There is a set of + * asserts in value.cc to guarantee that the sizeof expression + * used here is indeed at least as big as the largest object that + * will ever be copied into `data'. + * + * The `type' member holds the value_t::type_t value representing + * the type of the object stored. + */ + variant<bool, // BOOLEAN + datetime_t, // DATETIME + date_t, // DATE + long, // INTEGER + amount_t, // AMOUNT + balance_t *, // BALANCE + string, // STRING + mask_t, // MASK + sequence_t *, // SEQUENCE + scope_t * // SCOPE + > data; + + type_t type; + + /** + * `refc' holds the current reference count for each storage_t + * object. + */ + mutable int refc; + + /** + * Constructor. Since all storage object are assigned to after + * construction, the only constructors allowed are explicit, and + * copy (see below). The default starting type is VOID, which + * should rarely ever be seen in practice, since the first thing + * that value_t typically does is to assign a valid value. + */ + explicit storage_t() : type(VOID), refc(0) { + TRACE_CTOR(value_t::storage_t, ""); + } + + public: // so `checked_delete' can access it + /** + * Destructor. Must only be called when the reference count has + * reached zero. The `destroy' method is used to do the actual + * cleanup of the data, since it's quite possible for `destroy' to + * be called while the object is still active -- to clear the + * stored data for subsequent reuse of the storage_t object. + */ + ~storage_t() { + TRACE_DTOR(value_t::storage_t); + VERIFY(refc == 0); + destroy(); + } + + private: + /** + * Assignment and copy operators. These are called when making a + * new copy of a storage object in order to modify the copy. + */ + explicit storage_t(const storage_t& rhs) + : type(rhs.type), refc(0) { + TRACE_CTOR(value_t::storage_t, "copy"); + *this = rhs; + } + storage_t& operator=(const storage_t& rhs); + + /** + * Reference counting methods. The intrusive_ptr_* methods are + * used by boost::intrusive_ptr to manage the calls to acquire and + * release. + */ + void acquire() const { + DEBUG("value.storage.refcount", + "Acquiring " << this << ", refc now " << refc + 1); + VERIFY(refc >= 0); + refc++; + } + void release() const { + DEBUG("value.storage.refcount", + "Releasing " << this << ", refc now " << refc - 1); + VERIFY(refc > 0); + if (--refc == 0) + checked_delete(this); + } + + friend inline void intrusive_ptr_add_ref(value_t::storage_t * storage) { + storage->acquire(); + } + friend inline void intrusive_ptr_release(value_t::storage_t * storage) { + storage->release(); + } + + void destroy() { + DEBUG("value.storage.refcount", "Destroying " << this); + switch (type) { + case VOID: + return; + case BALANCE: + checked_delete(boost::get<balance_t *>(data)); + break; + case SEQUENCE: + checked_delete(boost::get<sequence_t *>(data)); + break; + default: + break; + } + data = false; + type = VOID; + } + +#if defined(HAVE_BOOST_SERIALIZATION) + private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & data; + ar & type; + ar & refc; + } +#endif // HAVE_BOOST_SERIALIZATION + }; + + /** + * The actual data for each value_t is kept in reference counted storage. + * Data is modified using a copy-on-write policy. + */ + intrusive_ptr<storage_t> storage; + + /** + * Make a private copy of the current value (if necessary) so it can + * subsequently be modified. + */ + void _dup() { + VERIFY(storage); + if (storage->refc > 1) + storage = new storage_t(*storage.get()); + } + + /** + * Because boolean "true" and "false" are so common, a pair of static + * references are kept to prevent the creation of throwaway storage_t + * objects just to represent these two common values. + */ + static intrusive_ptr<storage_t> true_value; + static intrusive_ptr<storage_t> false_value; + +public: + static void initialize(); + static void shutdown(); + +public: + /** + * Constructors. value_t objects may be constructed from almost any + * value type that they can contain, including variations on those + * types (such as long, unsigned long, etc). The ordering of the + * methods here reflects the ordering of the constants in type_t + * above. + * + * One constructor of special note is that taking a string or + * character pointer as an argument. Because value_t("$100") is + * interpreted as a commoditized amount, the form value_t("$100", + * true) is required to represent the literal string "$100", and not + * the amount "one hundred dollars". + */ + value_t() { + TRACE_CTOR(value_t, ""); + } + + value_t(const bool val) { + TRACE_CTOR(value_t, "const bool"); + set_boolean(val); + } + + value_t(const datetime_t& val) { + TRACE_CTOR(value_t, "const datetime_t&"); + set_datetime(val); + } + value_t(const date_t& val) { + TRACE_CTOR(value_t, "const date_t&"); + set_date(val); + } + + value_t(const long val) { + TRACE_CTOR(value_t, "const long"); + set_long(val); + } + value_t(const unsigned long val) { + TRACE_CTOR(value_t, "const unsigned long"); + set_amount(val); + } + value_t(const double val) { + TRACE_CTOR(value_t, "const double"); + set_amount(val); + } + value_t(const amount_t& val) { + TRACE_CTOR(value_t, "const amount_t&"); + set_amount(val); + } + value_t(const balance_t& val) { + TRACE_CTOR(value_t, "const balance_t&"); + set_balance(val); + } + value_t(const mask_t& val) { + TRACE_CTOR(value_t, "const mask_t&"); + set_mask(val); + } + + 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)); + } + 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)); + } + + value_t(const sequence_t& val) { + TRACE_CTOR(value_t, "const sequence_t&"); + set_sequence(val); + } + + explicit value_t(scope_t * item) { + TRACE_CTOR(value_t, "scope_t *"); + set_scope(item); + } + + /** + * Destructor. This does not do anything, because the intrusive_ptr + * that refers to our storage object will decrease its reference + * count itself upon destruction. + */ + ~value_t() { + TRACE_DTOR(value_t); + } + + /** + * Assignment and copy operators. Values are cheaply copied by + * simply creating another reference to the other value's storage + * object. A true copy is only ever made prior to modification. + */ + value_t(const value_t& val) { + TRACE_CTOR(value_t, "copy"); + *this = val; + } + value_t& operator=(const value_t& val) { + if (! (this == &val || storage == val.storage)) + storage = val.storage; + return *this; + } + + /** + * Comparison operators. Values can be compared to other values + */ + bool is_equal_to(const value_t& val) const; + bool is_less_than(const value_t& val) const; + bool is_greater_than(const value_t& val) const; + + template <typename T> + bool operator==(const T& amt) const { + return is_equal_to(amt); + } + template <typename T> + bool operator<(const T& amt) const { + return is_less_than(amt); + } + template <typename T> + bool operator>(const T& amt) const { + return is_greater_than(amt); + } + + /** + * Binary arithmetic operators. + * + * add(amount_t, optional<amount_t>) allows for the possibility of + * adding both an amount and its cost in a single operation. + * Otherwise, there is no way to separately represent the "cost" + * part of an amount addition statement. + */ + value_t& operator+=(const value_t& val); + value_t& operator-=(const value_t& val); + value_t& operator*=(const value_t& val); + value_t& operator/=(const value_t& val); + + /** + * Unary arithmetic operators. + */ + value_t negated() const { + value_t temp = *this; + temp.in_place_negate(); + return temp; + } + void in_place_negate(); // exists for efficiency's sake + void in_place_not(); // exists for efficiency's sake + + value_t operator-() const { + return negated(); + } + + value_t abs() const; + + value_t rounded() const { + value_t temp(*this); + temp.in_place_round(); + return temp; + } + void in_place_round(); + + value_t truncated() const { + value_t temp(*this); + temp.in_place_truncate(); + return temp; + } + void in_place_truncate(); + + value_t floored() const { + value_t temp(*this); + temp.in_place_floor(); + return temp; + } + void in_place_floor(); + + value_t unrounded() const { + value_t temp(*this); + temp.in_place_unround(); + return temp; + } + void in_place_unround(); + + value_t reduced() const { + value_t temp(*this); + temp.in_place_reduce(); + return temp; + } + void in_place_reduce(); // exists for efficiency's sake + + value_t unreduced() const { + value_t temp(*this); + temp.in_place_unreduce(); + return temp; + } + 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 bool primary_only = false, + const optional<datetime_t>& moment = none, + const optional<commodity_t&>& in_terms_of = none) const; + + value_t price() const; + + value_t exchange_commodities(const std::string& commodities, + const bool add_prices = false, + const optional<datetime_t>& moment = none); + + /** + * Truth tests. + */ + operator bool() const; + + bool is_nonzero() const { + return ! is_zero(); + } + + bool is_realzero() const; + bool is_zero() const; + bool is_null() const { + if (! storage) { + VERIFY(is_type(VOID)); + return true; + } else { + VERIFY(! is_type(VOID)); + return false; + } + } + + type_t type() const { + return storage ? storage->type : VOID; + } + bool is_type(type_t _type) const { + return type() == _type; + } + +private: + void set_type(type_t new_type); + +public: + /** + * Data manipulation methods. A value object may be truth tested for the + * existence of every type it can contain: + * + * is_boolean() + * is_long() + * is_datetime() + * is_date() + * is_amount() + * is_balance() + * is_string() + * is_mask() + * is_sequence() + * is_pointer() + * + * There are corresponding as_*() methods that represent a value as a + * reference to its underlying type. For example, as_long() returns a + * reference to a "const long". + * + * There are also as_*_lval() methods, which represent the underlying data + * as a reference to a non-const type. The difference here is that an + * _lval() call causes the underlying data to be fully copied before the + * resulting reference is returned. + * + * Lastly, there are corresponding set_*(data) methods for directly + * assigning data of a particular type, rather than using the regular + * assignment operator (whose implementation simply calls the various set_ + * methods). + */ + bool is_boolean() const { + return is_type(BOOLEAN); + } + bool& as_boolean_lval() { + VERIFY(is_boolean()); + _dup(); + return boost::get<bool>(storage->data); + } + const bool& as_boolean() const { + VERIFY(is_boolean()); + return boost::get<bool>(storage->data); + } + void set_boolean(const bool val) { + set_type(BOOLEAN); + storage = val ? true_value : false_value; + } + + bool is_datetime() const { + return is_type(DATETIME); + } + datetime_t& as_datetime_lval() { + VERIFY(is_datetime()); + _dup(); + return boost::get<datetime_t>(storage->data); + } + const datetime_t& as_datetime() const { + VERIFY(is_datetime()); + return boost::get<datetime_t>(storage->data); + } + void set_datetime(const datetime_t& val) { + set_type(DATETIME); + storage->data = val; + } + + bool is_date() const { + return is_type(DATE); + } + date_t& as_date_lval() { + VERIFY(is_date()); + _dup(); + return boost::get<date_t>(storage->data); + } + const date_t& as_date() const { + VERIFY(is_date()); + return boost::get<date_t>(storage->data); + } + void set_date(const date_t& val) { + set_type(DATE); + storage->data = val; + } + + bool is_long() const { + return is_type(INTEGER); + } + long& as_long_lval() { + VERIFY(is_long()); + _dup(); + return boost::get<long>(storage->data); + } + const long& as_long() const { + VERIFY(is_long()); + return boost::get<long>(storage->data); + } + void set_long(const long val) { + set_type(INTEGER); + storage->data = val; + } + + bool is_amount() const { + return is_type(AMOUNT); + } + amount_t& as_amount_lval() { + VERIFY(is_amount()); + _dup(); + return boost::get<amount_t>(storage->data); + } + const amount_t& as_amount() const { + VERIFY(is_amount()); + return boost::get<amount_t>(storage->data); + } + void set_amount(const amount_t& val) { + VERIFY(val.valid()); + set_type(AMOUNT); + storage->data = val; + } + + bool is_balance() const { + return is_type(BALANCE); + } + balance_t& as_balance_lval() { + VERIFY(is_balance()); + _dup(); + return *boost::get<balance_t *>(storage->data); + } + const balance_t& as_balance() const { + VERIFY(is_balance()); + return *boost::get<balance_t *>(storage->data); + } + void set_balance(const balance_t& val) { + VERIFY(val.valid()); + set_type(BALANCE); + storage->data = new balance_t(val); + } + + bool is_string() const { + return is_type(STRING); + } + string& as_string_lval() { + VERIFY(is_string()); + _dup(); + return boost::get<string>(storage->data); + } + const string& as_string() const { + VERIFY(is_string()); + return boost::get<string>(storage->data); + } + void set_string(const string& val = "") { + set_type(STRING); + storage->data = val; + VERIFY(boost::get<string>(storage->data) == val); + } + void set_string(const char * val = "") { + set_type(STRING); + storage->data = string(val); + VERIFY(boost::get<string>(storage->data) == val); + } + + bool is_mask() const { + return is_type(MASK); + } + mask_t& as_mask_lval() { + VERIFY(is_mask()); + _dup(); + VERIFY(boost::get<mask_t>(storage->data).valid()); + return boost::get<mask_t>(storage->data); + } + const mask_t& as_mask() const { + VERIFY(is_mask()); + VERIFY(boost::get<mask_t>(storage->data).valid()); + return boost::get<mask_t>(storage->data); + } + void set_mask(const string& val) { + set_type(MASK); + storage->data = mask_t(val); + } + void set_mask(const mask_t& val) { + set_type(MASK); + storage->data = val; + } + + bool is_sequence() const { + return is_type(SEQUENCE); + } + sequence_t& as_sequence_lval() { + VERIFY(is_sequence()); + _dup(); + return *boost::get<sequence_t *>(storage->data); + } + const sequence_t& as_sequence() const { + VERIFY(is_sequence()); + return *boost::get<sequence_t *>(storage->data); + } + void set_sequence(const sequence_t& val) { + set_type(SEQUENCE); + storage->data = new sequence_t(val); + } + + /** + * Dealing with scope pointers. + */ + bool is_scope() const { + return is_type(SCOPE); + } + scope_t * as_scope() const { + VERIFY(is_scope()); + return boost::get<scope_t *>(storage->data); + } + void set_scope(scope_t * val) { + set_type(SCOPE); + storage->data = val; + } + + /** + * Data conversion methods. These methods convert a value object to + * its underlying type, where possible. If not possible, an + * exception is thrown. + */ + bool to_boolean() const; + int to_int() const; + long to_long() const; + datetime_t to_datetime() const; + date_t to_date() const; + amount_t to_amount() const; + balance_t to_balance() const; + string to_string() const; + mask_t to_mask() const; + sequence_t to_sequence() const; + + /** + * Dynamic typing conversion methods. + * + * `cast(type_t)' returns a new value whose type has been cast to + * the given type, but whose value is based on the original value. + * For example, the uncommoditized AMOUNT "100.00" could be cast to + * an INTEGER value. If a cast would lose information or is not + * meaningful, an exception is thrown. + * + * `simplify()' is an automatic cast to the simplest type that can + * still represent the original value. + * + * There are also "in-place" versions of these two methods: + * in_place_cast + * in_place_simplify + */ + value_t casted(type_t cast_type) const { + value_t temp(*this); + temp.in_place_cast(cast_type); + return temp; + } + void in_place_cast(type_t cast_type); + + value_t simplified() const { + value_t temp = *this; + temp.in_place_simplify(); + return temp; + } + void in_place_simplify(); + + value_t number() const; + + /** + * Annotated commodity methods. + */ + void annotate(const annotation_t& details); + bool has_annotation() const; + + annotation_t& annotation(); + const annotation_t& annotation() const { + return const_cast<value_t&>(*this).annotation(); + } + + value_t strip_annotations(const keep_details_t& what_to_keep) const; + + /** + * Collection-style access methods for SEQUENCE values. + */ + value_t& operator[](const std::size_t index) { + VERIFY(! is_null()); + if (is_sequence()) + return as_sequence_lval()[index]; + else if (index == 0) + return *this; + + assert(false); + static value_t null; + return null; + } + const value_t& operator[](const std::size_t index) const { + VERIFY(! is_null()); + if (is_sequence()) + return as_sequence()[index]; + else if (index == 0) + return *this; + + assert(false); + static value_t null; + return null; + } + + void push_front(const value_t& val) { + if (is_null()) + *this = sequence_t(); + if (! is_sequence()) + in_place_cast(SEQUENCE); + as_sequence_lval().push_front(val); + } + + void push_back(const value_t& val) { + if (is_null()) + *this = sequence_t(); + if (! is_sequence()) + in_place_cast(SEQUENCE); + as_sequence_lval().push_back(val); + } + + void pop_back() { + VERIFY(! is_null()); + + if (! is_sequence()) { +#if BOOST_VERSION >= 103700 + storage.reset(); +#else + storage = intrusive_ptr<storage_t>(); +#endif + } else { + as_sequence_lval().pop_back(); + + const sequence_t& seq(as_sequence()); + std::size_t new_size = seq.size(); + if (new_size == 0) { +#if BOOST_VERSION >= 103700 + storage.reset(); +#else + storage = intrusive_ptr<storage_t>(); +#endif + } + else if (new_size == 1) { + *this = seq.front(); + } + } + } + + sequence_t::iterator begin() { + return as_sequence_lval().begin(); + } + + sequence_t::iterator end() { + return as_sequence_lval().end(); + } + + sequence_t::const_iterator begin() const { + return as_sequence().begin(); + } + + sequence_t::const_iterator end() const { + return as_sequence().end(); + } + + std::size_t size() const { + if (is_null()) + return 0; + else if (is_sequence()) + return as_sequence().size(); + else + return 1; + } + + bool empty() const { + return size() == 0; + } + + /** + * Informational methods. + */ + string label(optional<type_t> the_type = none) const { + switch (the_type ? *the_type : type()) { + case VOID: + return _("an uninitialized value"); + case BOOLEAN: + return _("a boolean"); + case DATETIME: + return _("a date/time"); + case DATE: + return _("a date"); + case INTEGER: + return _("an integer"); + case AMOUNT: + return _("an amount"); + case BALANCE: + return _("a balance"); + case STRING: + return _("a string"); + case MASK: + return _("a regexp"); + case SEQUENCE: + return _("a sequence"); + case SCOPE: + return _("a scope"); + default: + assert(false); + break; + } + assert(false); + return _("<invalid>"); + } + + /** + * Printing methods. + */ + void print(std::ostream& out, + const int first_width = -1, + const int latter_width = -1, + const bool right_justify = false, + const bool colorize = false) const; + void dump(std::ostream& out, const bool relaxed = true) const; + + /** + * Debugging methods. + */ + bool valid() 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 & true_value; + ar & false_value; + ar & storage; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +#define NULL_VALUE (value_t()) + +inline value_t string_value(const string& str = "") { + return value_t(str, true); +} + +#define VALUE_OR_ZERO(val) ((val).is_null() ? value_t(0L) : (val)) +#define SIMPLIFIED_VALUE_OR_ZERO(val) \ + ((val).is_null() ? value_t(0L) : (val).simplified()) + +inline value_t mask_value(const string& str) { + return value_t(mask_t(str)); +} + +inline std::ostream& operator<<(std::ostream& out, const value_t& val) { + val.print(out); + return out; +} + +inline string value_context(const value_t& val) { + std::ostringstream buf; + val.print(buf, 20, 20, true); + return buf.str(); +} + +template <typename T> +inline value_t& add_or_set_value(value_t& lhs, const T& rhs) { + if (lhs.is_null()) + lhs = rhs; + else + lhs += rhs; + return lhs; +} + +struct sort_value_t +{ + bool inverted; + value_t value; + + sort_value_t() : inverted(false) {} +}; + +bool sort_value_is_less_than(const std::list<sort_value_t>& left_values, + const std::list<sort_value_t>& right_values); + +void to_xml(std::ostream& out, const value_t& value); + +} // namespace ledger + +#endif // _VALUE_H diff --git a/src/xact.cc b/src/xact.cc new file mode 100644 index 00000000..623c5772 --- /dev/null +++ b/src/xact.cc @@ -0,0 +1,781 @@ +/* + * Copyright (c) 2003-2009, 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 "xact.h" +#include "post.h" +#include "account.h" +#include "journal.h" +#include "pool.h" + +namespace ledger { + +xact_base_t::xact_base_t(const xact_base_t& xact_base) + : item_t(xact_base), journal(xact_base.journal) +{ + TRACE_CTOR(xact_base_t, "copy"); +} + +xact_base_t::~xact_base_t() +{ + TRACE_DTOR(xact_base_t); + + if (! has_flags(ITEM_TEMP)) { + foreach (post_t * post, posts) { + // If the posting is a temporary, it will be destructed when the + // temporary is. + assert(! post->has_flags(ITEM_TEMP)); + checked_delete(post); + } + } +} + +void xact_base_t::add_post(post_t * post) +{ + // You can add temporary postings to transactions, but not real postings to + // temporary transactions. + if (! post->has_flags(ITEM_TEMP)) + assert(! has_flags(ITEM_TEMP)); + + posts.push_back(post); +} + +bool xact_base_t::remove_post(post_t * post) +{ + posts.remove(post); + post->xact = NULL; + return true; +} + +bool xact_base_t::has_xdata() +{ + foreach (post_t * post, posts) + if (post->has_xdata()) + return true; + + return false; +} + +void xact_base_t::clear_xdata() +{ + foreach (post_t * post, posts) + if (! post->has_flags(ITEM_TEMP)) + post->clear_xdata(); +} + +value_t xact_base_t::magnitude() const +{ + value_t halfbal = 0L; + foreach (const post_t * post, posts) { + if (post->amount.sign() > 0) { + if (post->cost) + halfbal += *post->cost; + else + halfbal += post->amount; + } + } + return halfbal; +} + +bool xact_base_t::finalize() +{ + // Scan through and compute the total balance for the xact. This is used + // for auto-calculating the value of xacts with no cost, and the per-unit + // price of unpriced commodities. + + value_t balance; + post_t * null_post = NULL; + + foreach (post_t * post, posts) { + if (! post->must_balance()) + continue; + + amount_t& p(post->cost ? *post->cost : post->amount); + if (! p.is_null()) { + DEBUG("xact.finalize", "post must balance = " << p.reduced()); + if (! post->cost && post->amount.has_annotation() && + post->amount.annotation().price) { + // If the amount has no cost, but is annotated with a per-unit + // price, use the price times the amount as the cost + post->cost = (*post->amount.annotation().price * + post->amount).unrounded(); + DEBUG("xact.finalize", + "annotation price = " << *post->amount.annotation().price); + DEBUG("xact.finalize", "amount = " << post->amount); + DEBUG("xact.finalize", "priced cost = " << *post->cost); + post->add_flags(POST_COST_CALCULATED); + add_or_set_value(balance, post->cost->rounded().reduced()); + } else { + // If the amount was a cost, it very likely has the "keep_precision" + // flag set, meaning commodity display precision is ignored when + // displaying the amount. We never want this set for the balance, + // so we must clear the flag in a temporary to avoid it propagating + // into the balance. + add_or_set_value(balance, p.keep_precision() ? + p.rounded().reduced() : p.reduced()); + } + } + else if (null_post) { + throw_(std::logic_error, + _("Only one posting with null amount allowed per transaction")); + } + else { + null_post = post; + } + } + VERIFY(balance.valid()); + +#if defined(DEBUG_ON) + DEBUG("xact.finalize", "initial balance = " << balance); + DEBUG("xact.finalize", "balance is " << balance.label()); + if (balance.is_balance()) + DEBUG("xact.finalize", "balance commodity count = " + << balance.as_balance().amounts.size()); +#endif + + // If there is only one post, balance against the default account if one has + // been set. + + if (journal && journal->bucket && posts.size() == 1 && ! balance.is_null()) { + null_post = new post_t(journal->bucket, ITEM_GENERATED); + null_post->_state = (*posts.begin())->_state; + add_post(null_post); + } + + if (null_post != NULL) { + // If one post has no value at all, its value will become the inverse of + // the rest. If multiple commodities are involved, multiple posts are + // generated to balance them all. + + DEBUG("xact.finalize", "there was a null posting"); + + if (balance.is_balance()) { + bool first = true; + const balance_t& bal(balance.as_balance()); + foreach (const balance_t::amounts_map::value_type& pair, bal.amounts) { + 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()) { + throw_(balance_error, _("Transaction does not balance")); + } + balance = NULL_VALUE; + } + else if (balance.is_balance() && + balance.as_balance().amounts.size() == 2) { + // When an xact involves two different commodities (regardless of how + // many posts there are) determine the conversion ratio by dividing the + // total value of one commodity by the total value of the other. This + // establishes the per-unit cost for this post for both commodities. + + DEBUG("xact.finalize", "there were exactly two commodities"); + + bool saw_cost = false; + post_t * top_post = NULL; + + foreach (post_t * post, posts) { + if (! post->amount.is_null()) { + if (post->amount.has_annotation()) + top_post = post; + else if (! top_post) + top_post = post; + } + + if (post->cost && ! post->has_flags(POST_COST_CALCULATED)) { + saw_cost = true; + break; + } + } + + if (! saw_cost && top_post) { + const balance_t& bal(balance.as_balance()); + + DEBUG("xact.finalize", "there were no costs, and a valid top_post"); + + balance_t::amounts_map::const_iterator a = bal.amounts.begin(); + + const amount_t * x = &(*a++).second; + const amount_t * y = &(*a++).second; + + if (x->commodity() != top_post->amount.commodity()) { + const amount_t * t = x; + x = y; + y = t; + } + + if (*x && *y) { + DEBUG("xact.finalize", "primary amount = " << *x); + DEBUG("xact.finalize", "secondary amount = " << *y); + + commodity_t& comm(x->commodity()); + amount_t per_unit_cost; + amount_t total_cost; + + foreach (post_t * post, posts) { + if (post != top_post && post->must_balance() && + ! post->amount.is_null() && + post->amount.has_annotation() && + post->amount.annotation().price) { + amount_t temp = *post->amount.annotation().price * post->amount; + if (total_cost.is_null()) { + total_cost = temp; + y = &total_cost; + } else { + total_cost += temp; + } + DEBUG("xact.finalize", "total_cost = " << total_cost); + } + } + per_unit_cost = (*y / *x).abs().unrounded(); + + DEBUG("xact.finalize", "per_unit_cost = " << per_unit_cost); + + foreach (post_t * post, posts) { + const amount_t& amt(post->amount); + + if (post->must_balance() && amt.commodity() == comm) { + balance -= amt; + post->cost = per_unit_cost * amt; + post->add_flags(POST_COST_CALCULATED); + balance += *post->cost; + + DEBUG("xact.finalize", "set post->cost to = " << *post->cost); + } + } + } + } + } + + // Now that the post list has its final form, calculate the balance once + // more in terms of total cost, accounting for any possible gain/loss + // amounts. + + DEBUG("xact.finalize", "resolved balance = " << balance); + + posts_list copy(posts); + + foreach (post_t * post, copy) { + if (! post->cost) + continue; + + if (post->amount.commodity() == post->cost->commodity()) + throw_(balance_error, + _("A posting's cost must be of a different commodity than its amount")); + + cost_breakdown_t breakdown = + commodity_pool_t::current_pool->exchange + (post->amount, *post->cost, false, + datetime_t(date(), time_duration(0, 0, 0, 0))); + + if (post->amount.has_annotation() && + breakdown.basis_cost.commodity() == + breakdown.final_cost.commodity()) { + if (amount_t gain_loss = (breakdown.basis_cost - + breakdown.final_cost).rounded()) { + DEBUG("xact.finalize", "gain_loss = " << gain_loss); + + add_or_set_value(balance, gain_loss.reduced()); + + account_t * account; + if (gain_loss.sign() > 0) + account = journal->find_account(_("Equity:Capital Gains")); + else + account = journal->find_account(_("Equity:Capital Losses")); + + post_t * p = new post_t(account, gain_loss, ITEM_GENERATED); + p->set_state(post->state()); + add_post(p); + DEBUG("xact.finalize", "added gain_loss, balance = " << balance); + } + } else { + post->amount = breakdown.amount; + DEBUG("xact.finalize", "added breakdown, balance = " << balance); + } + } + + DEBUG("xact.finalize", "final balance = " << balance); + + if (! balance.is_null() && ! balance.is_zero()) { + add_error_context(item_context(*this, _("While balancing transaction"))); + add_error_context(_("Unbalanced remainder is:")); + add_error_context(value_context(balance)); + add_error_context(_("Amount to balance against:")); + add_error_context(value_context(magnitude())); + throw_(balance_error, _("Transaction does not balance")); + } + + // Add a pointer to each posting to their related accounts + + if (dynamic_cast<xact_t *>(this)) { + bool all_null = true; + bool some_null = false; + + foreach (post_t * post, posts) { + if (! post->amount.is_null()) { + all_null = false; + post->amount.in_place_reduce(); + } else { + some_null = true; + } + + post->account->add_post(post); + + post->xdata().add_flags(POST_EXT_VISITED); + post->account->xdata().add_flags(ACCOUNT_EXT_VISITED); + } + + if (all_null) + return false; // ignore this xact completely + else if (some_null) + throw_(balance_error, + _("There cannot be null amounts after balancing a transaction")); + } + + VERIFY(valid()); + + return true; +} + +bool xact_base_t::verify() +{ + // Scan through and compute the total balance for the xact. + + value_t balance; + + foreach (post_t * post, posts) { + if (! post->must_balance()) + continue; + + amount_t& p(post->cost ? *post->cost : post->amount); + assert(! p.is_null()); + + // If the amount was a cost, it very likely has the "keep_precision" flag + // set, meaning commodity display precision is ignored when displaying the + // amount. We never want this set for the balance, so we must clear the + // flag in a temporary to avoid it propagating into the balance. + add_or_set_value(balance, p.keep_precision() ? + p.rounded().reduced() : p.reduced()); + } + VERIFY(balance.valid()); + + // Now that the post list has its final form, calculate the balance once + // more in terms of total cost, accounting for any possible gain/loss + // amounts. + + foreach (post_t * post, posts) { + if (! post->cost) + continue; + + if (post->amount.commodity() == post->cost->commodity()) + throw_(amount_error, + _("A posting's cost must be of a different commodity than its amount")); + } + + if (! balance.is_null() && ! balance.is_zero()) { + add_error_context(item_context(*this, _("While balancing transaction"))); + add_error_context(_("Unbalanced remainder is:")); + add_error_context(value_context(balance)); + add_error_context(_("Amount to balance against:")); + add_error_context(value_context(magnitude())); + throw_(balance_error, _("Transaction does not balance")); + } + + VERIFY(valid()); + + return true; +} + +xact_t::xact_t(const xact_t& e) + : xact_base_t(e), code(e.code), payee(e.payee) +{ + TRACE_CTOR(xact_t, "copy"); +} + +void xact_t::add_post(post_t * post) +{ + post->xact = this; + 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 +{ + SHA1 sha; + sha.Reset(); + sha << idstring().c_str(); + uint_least32_t message_digest[5]; + sha.Result(message_digest); + return to_hex(message_digest, 5); +} + +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) + return string_value(*xact.code); + else + return string_value(empty_string); + } + + value_t get_payee(xact_t& xact) { + return string_value(xact.payee); + } + + template <value_t (*Func)(xact_t&)> + value_t get_wrapper(call_scope_t& scope) { + return (*Func)(find_scope<xact_t>(scope)); + } +} + +expr_t::ptr_op_t xact_t::lookup(const symbol_t::kind_t kind, + const string& name) +{ + if (kind != symbol_t::FUNCTION) + return item_t::lookup(kind, name); + + switch (name[0]) { + case 'c': + if (name == "code") + 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>); + break; + + case 'p': + if (name[1] == '\0' || name == "payee") + return WRAP_FUNCTOR(get_wrapper<&get_payee>); + break; + } + + return item_t::lookup(kind, name); +} + +bool xact_t::valid() const +{ + if (! _date) { + DEBUG("ledger.validate", "xact_t: ! _date"); + return false; + } + + foreach (post_t * post, posts) + if (post->xact != this || ! post->valid()) { + DEBUG("ledger.validate", "xact_t: post not valid"); + return false; + } + + return true; +} + +namespace { + + bool post_pred(expr_t::ptr_op_t op, post_t& post) + { + switch (op->kind) { + case expr_t::op_t::VALUE: + return op->as_value().to_boolean(); + break; + + case expr_t::op_t::O_MATCH: + if (op->left()->kind == expr_t::op_t::IDENT && + op->left()->as_ident() == "account" && + op->right()->kind == expr_t::op_t::VALUE && + op->right()->as_value().is_mask()) + return op->right()->as_value().as_mask() + .match(post.reported_account()->fullname()); + else + break; + + case expr_t::op_t::O_NOT: + return ! post_pred(op->left(), post); + + case expr_t::op_t::O_AND: + return post_pred(op->left(), post) && post_pred(op->right(), post); + + case expr_t::op_t::O_OR: + return post_pred(op->left(), post) || post_pred(op->right(), post); + + case expr_t::op_t::O_QUERY: + if (post_pred(op->left(), post)) + return post_pred(op->right()->left(), post); + else + return post_pred(op->right()->right(), post); + + default: + break; + } + + throw_(calc_error, _("Unhandled operator")); + return false; + } + +} // unnamed namespace + +void auto_xact_t::extend_xact(xact_base_t& xact) +{ + posts_list initial_posts(xact.posts.begin(), xact.posts.end()); + + try { + + bool needs_further_verification = false; + + foreach (post_t * initial_post, initial_posts) { + if (initial_post->has_flags(ITEM_GENERATED)) + continue; + + bool matches_predicate = false; + if (try_quick_match) { + try { + bool found_memoized_result = false; + if (! memoized_results.empty()) { + std::map<string, bool>::iterator i = + memoized_results.find(initial_post->account->fullname()); + if (i != memoized_results.end()) { + found_memoized_result = true; + matches_predicate = (*i).second; + } + } + + // Since the majority of people who use automated transactions simply + // match against account names, try using a *much* faster version of + // the predicate evaluator. + if (! found_memoized_result) { + matches_predicate = post_pred(predicate.get_op(), *initial_post); + memoized_results.insert + (std::pair<string, bool>(initial_post->account->fullname(), + matches_predicate)); + } + } + catch (...) { + DEBUG("xact.extend.fail", + "The quick matcher failed, going back to regular eval"); + try_quick_match = false; + matches_predicate = predicate(*initial_post); + } + } else { + matches_predicate = predicate(*initial_post); + } + if (matches_predicate) { + foreach (post_t * post, posts) { + amount_t post_amount; + if (post->amount.is_null()) { + if (! post->amount_expr) + throw_(amount_error, + _("Automated transaction's posting has no amount")); + + bind_scope_t bound_scope(*scope_t::default_scope, *initial_post); + value_t result(post->amount_expr->calc(bound_scope)); + if (result.is_long()) { + post_amount = result.to_amount(); + } else { + if (! result.is_amount()) + throw_(amount_error, + _("Amount expressions must result in a simple amount")); + post_amount = result.as_amount(); + } + } else { + post_amount = post->amount; + } + + amount_t amt; + if (! post_amount.commodity()) + amt = initial_post->amount * post_amount; + else + amt = post_amount; + + IF_DEBUG("xact.extend") { + DEBUG("xact.extend", + "Initial post on line " << initial_post->pos->beg_line << ": " + << "amount " << initial_post->amount << " (precision " + << initial_post->amount.precision() << ")"); + +#if defined(DEBUG_ON) + if (initial_post->amount.keep_precision()) + DEBUG("xact.extend", " precision is kept"); +#endif + + DEBUG("xact.extend", + "Posting on line " << post->pos->beg_line << ": " + << "amount " << post_amount << ", amt " << amt + << " (precision " << post_amount.precision() + << " != " << amt.precision() << ")"); + +#if defined(DEBUG_ON) + if (post_amount.keep_precision()) + DEBUG("xact.extend", " precision is kept"); + if (amt.keep_precision()) + DEBUG("xact.extend", " amt precision is kept"); +#endif + } + + account_t * account = post->account; + string fullname = account->fullname(); + assert(! fullname.empty()); + if (fullname == "$account" || fullname == "@account") + account = initial_post->account; + + // Copy over details so that the resulting post is a mirror of + // the automated xact's one. + 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; + } + } + } + + if (needs_further_verification) + xact.verify(); + + } + catch (const std::exception& err) { + add_error_context(item_context(*this, _("While applying automated transaction"))); + add_error_context(item_context(xact, _("While extending transaction"))); + throw; + } +} + +void extend_xact_base(journal_t * journal, + xact_base_t& base) +{ + foreach (auto_xact_t * xact, journal->auto_xacts) + xact->extend_xact(base); +} + +void to_xml(std::ostream& out, const xact_t& xact) +{ + push_xml x(out, "transaction", true, true); + + if (xact.state() == item_t::CLEARED) + out << " state=\"cleared\""; + else if (xact.state() == item_t::PENDING) + out << " state=\"pending\""; + + if (xact.has_flags(ITEM_GENERATED)) + out << " generated=\"true\""; + + x.close_attrs(); + + if (xact._date) { + 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.code) { + push_xml y(out, "code"); + out << y.guard(*xact.code); + } + + { + push_xml y(out, "payee"); + out << y.guard(xact.payee); + } + + if (xact.note) { + push_xml y(out, "note"); + out << y.guard(*xact.note); + } + + if (xact.metadata) { + push_xml y(out, "metadata"); + foreach (const item_t::string_map::value_type& pair, *xact.metadata) { + if (pair.second) { + push_xml z(out, "variable"); + { + push_xml w(out, "key"); + out << y.guard(pair.first); + } + { + push_xml w(out, "value"); + out << y.guard(*pair.second); + } + } else { + push_xml z(out, "tag"); + out << y.guard(pair.first); + } + } + } +} + +} // namespace ledger diff --git a/src/xact.h b/src/xact.h new file mode 100644 index 00000000..c819b2a0 --- /dev/null +++ b/src/xact.h @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2003-2009, 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 xact.h + * @author John Wiegley + * + * @ingroup data + */ +#ifndef _XACT_H +#define _XACT_H + +#include "item.h" +#include "predicate.h" + +namespace ledger { + +class post_t; +class journal_t; + +typedef std::list<post_t *> posts_list; + +class xact_base_t : public item_t +{ +public: + journal_t * journal; + posts_list posts; + + xact_base_t() : item_t(), journal(NULL) { + TRACE_CTOR(xact_base_t, ""); + } + xact_base_t(const xact_base_t& e); + + virtual ~xact_base_t(); + + virtual void add_post(post_t * post); + virtual bool remove_post(post_t * post); + + posts_list::iterator posts_begin() { + return posts.begin(); + } + posts_list::iterator posts_end() { + return posts.end(); + } + + value_t magnitude() const; + + bool finalize(); + bool verify(); + + bool has_xdata(); + void clear_xdata(); + + virtual bool valid() const { + return true; + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<item_t>(*this); + ar & journal; + ar & posts; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class xact_t : public xact_base_t +{ +public: + optional<string> code; + string payee; + + xact_t() { + TRACE_CTOR(xact_t, ""); + } + xact_t(const xact_t& e); + + virtual ~xact_t() { + TRACE_DTOR(xact_t); + } + + 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); + + virtual bool valid() 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 & boost::serialization::base_object<xact_base_t>(*this); + ar & code; + ar & payee; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class auto_xact_t : public xact_base_t +{ +public: + predicate_t predicate; + bool try_quick_match; + + std::map<string, bool> memoized_results; + + auto_xact_t() : try_quick_match(true) { + 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) { + TRACE_CTOR(auto_xact_t, "copy"); + } + auto_xact_t(const predicate_t& _predicate) + : predicate(_predicate), try_quick_match(true) + { + TRACE_CTOR(auto_xact_t, "const predicate_t&"); + } + + virtual ~auto_xact_t() { + TRACE_DTOR(auto_xact_t); + } + + virtual void extend_xact(xact_base_t& xact); + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<xact_base_t>(*this); + ar & predicate; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +class period_xact_t : public xact_base_t +{ + public: + date_interval_t period; + string period_string; + + period_xact_t() { + TRACE_CTOR(period_xact_t, ""); + } + period_xact_t(const period_xact_t& e) + : xact_base_t(e), period(e.period), period_string(e.period_string) { + TRACE_CTOR(period_xact_t, "copy"); + } + period_xact_t(const string& _period) + : period(_period), period_string(_period) { + TRACE_CTOR(period_xact_t, "const string&"); + } + + virtual ~period_xact_t() { + TRACE_DTOR(period_xact_t); + } + +#if defined(HAVE_BOOST_SERIALIZATION) +private: + /** Serialization. */ + + friend class boost::serialization::access; + + template<class Archive> + void serialize(Archive& ar, const unsigned int /* version */) { + ar & boost::serialization::base_object<xact_base_t>(*this); + ar & period; + ar & period_string; + } +#endif // HAVE_BOOST_SERIALIZATION +}; + +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; + +void to_xml(std::ostream& out, const xact_t& xact); + +} // namespace ledger + +#endif // _XACT_H diff --git a/src/xml.cc b/src/xml.cc new file mode 100644 index 00000000..9cff980a --- /dev/null +++ b/src/xml.cc @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2003-2009, 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 "xml.h" +#include "xact.h" +#include "post.h" +#include "account.h" +#include "session.h" +#include "report.h" + +namespace ledger { + +namespace { + void xml_account(std::ostream& out, const account_t * acct) { + if ((acct->has_xdata() && + acct->xdata().has_flags(ACCOUNT_EXT_VISITED)) || + acct->children_with_flags(ACCOUNT_EXT_VISITED)) { + out << "<account id=\""; + out.width(sizeof(unsigned long) * 2); + out.fill('0'); + out << std::hex << reinterpret_cast<unsigned long>(acct); + out << "\">\n"; + + out << "<name>" << acct->name << "</name>\n"; + value_t total = acct->amount(); + if (! total.is_null()) { + out << "<amount>\n"; + to_xml(out, total); + out << "</amount>\n"; + } + total = acct->total(); + if (! total.is_null()) { + out << "<total>\n"; + to_xml(out, total); + out << "</total>\n"; + } + out << "</account>\n"; + } + + foreach (const accounts_map::value_type& pair, acct->accounts) + xml_account(out, pair.second); + } + + void xml_transaction(std::ostream& out, const xact_t * xact) { + to_xml(out, *xact); + + foreach (const post_t * post, xact->posts) + if (post->has_xdata() && + post->xdata().has_flags(POST_EXT_VISITED)) + to_xml(out, *post); + + out << "</transaction>\n"; + } +} + +void format_xml::flush() +{ + std::ostream& out(report.output_stream); + + out << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + out << "<ledger version=\"" << VERSION << "\">\n"; + + out << "<commodities>\n"; + foreach (const commodities_pair& pair, commodities) { + to_xml(out, *pair.second, true); + out << '\n'; + } + out << "</commodities>\n"; + + out << "<accounts>\n"; + xml_account(out, report.session.journal->master); + out << "</accounts>\n"; + + out << "<transactions>\n"; + foreach (const xact_t * xact, transactions) + xml_transaction(out, xact); + out << "</transactions>\n"; + + out << "</ledger>\n"; + out.flush(); +} + +void format_xml::operator()(post_t& post) +{ + assert(post.xdata().has_flags(POST_EXT_VISITED)); + + commodities.insert(commodities_pair(post.amount.commodity().symbol(), + &post.amount.commodity())); + + if (transactions_set.find(post.xact) == transactions_set.end()) + transactions.push_back(post.xact); +} + +} // namespace ledger diff --git a/src/xml.h b/src/xml.h new file mode 100644 index 00000000..30a7b1b1 --- /dev/null +++ b/src/xml.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2003-2009, John Wiegley. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @addtogroup report + */ + +/** + * @file xml.h + * @author John Wiegley + * + * @ingroup report + * + * @brief Brief + * + * Long. + */ +#ifndef _XML_H +#define _XML_H + +#include "chain.h" + +namespace ledger { + +class xact_t; +class account_t; +class commodity_t; +class post_t; +class report_t; + +/** + * @brief Brief + * + * Long. + */ +class format_xml : public item_handler<post_t> +{ +protected: + report_t& report; + + typedef std::map<string, commodity_t *> commodities_map; + typedef std::pair<string, commodity_t *> commodities_pair; + + commodities_map commodities; + std::set<xact_t *> transactions_set; + std::deque<xact_t *> transactions; + +public: + format_xml(report_t& _report) : report(_report) { + TRACE_CTOR(format_xml, "report&"); + } + virtual ~format_xml() { + TRACE_DTOR(format_xml); + } + + virtual void flush(); + virtual void operator()(post_t& post); +}; + +} // namespace ledger + +#endif // _XML_H |