Merge pull request #247 from ecraven/master

Adding support for recursive aliases.
This commit is contained in:
John Wiegley 2014-02-26 16:06:52 -06:00
commit eddd03ed09
10 changed files with 132 additions and 20 deletions

View file

@ -420,6 +420,9 @@ For use only with the
command, it causes Ledger to print out matching entries exactly as they command, it causes Ledger to print out matching entries exactly as they
appeared in the original journal file. appeared in the original journal file.
.It Fl \-real Pq Fl R .It Fl \-real Pq Fl R
.It Fl \-recursive-aliases
Causes ledger to try to expand aliases recursively, i.e. try to expand
the result of an earlier expansion again, until no more expansions apply.
.It Fl \-register-format Ar FMT .It Fl \-register-format Ar FMT
.It Fl \-related Pq Fl r .It Fl \-related Pq Fl r
.It Fl \-related-all .It Fl \-related-all

View file

@ -2158,9 +2158,23 @@ alias Checking=Assets:Credit Union:Joint Checking Account
@end smallexample @end smallexample
The aliases are only in effect for transactions read in after the alias The aliases are only in effect for transactions read in after the alias
is defined and are effected by @code{account} directives that precede is defined and are affected by @code{account} directives that precede
them. them.
With the option @option{--recursive-aliases}, aliases can refer to other aliases,
the following example produces exactly the same transactions and account names
as the preceding one:
@smallexample
alias Entertainment=Expenses:Entertainment
alias Dining=Entertainment:Dining
alias Checking=Assets:Credit Union:Joint Checking Account
2011/11/30 ChopChop
Dining $10.00
Checking
@end smallexample
@item assert @item assert
@c instance_t::assert_directive @c instance_t::assert_directive
An assertion can throw an error if a condition is not met during An assertion can throw an error if a condition is not met during
@ -5801,6 +5815,11 @@ correct, and if it finds a new account or commodity (same as
a misspelled commodity or account) it will issue a warning giving you a misspelled commodity or account) it will issue a warning giving you
the file and line number of the problem. the file and line number of the problem.
@item --recursive-aliases
Normally, ledger only expands aliases once. With this option, ledger tries
to expand the result of alias expansion recursively, until no more expansions
apply.
@item --time-colon @item --time-colon
The @option{--time-colon} option will display the value for a seconds The @option{--time-colon} option will display the value for a seconds
based commodity as real hours and minutes. based commodity as real hours and minutes.

View file

@ -96,6 +96,7 @@ void journal_t::initialize()
check_payees = false; check_payees = false;
day_break = false; day_break = false;
checking_style = CHECK_PERMISSIVE; checking_style = CHECK_PERMISSIVE;
recursive_aliases = false;
} }
void journal_t::add_account(account_t * acct) void journal_t::add_account(account_t * acct)
@ -121,26 +122,9 @@ account_t * journal_t::find_account_re(const string& regexp)
account_t * journal_t::register_account(const string& name, post_t * post, account_t * journal_t::register_account(const string& name, post_t * post,
account_t * master_account) account_t * master_account)
{ {
account_t * result = NULL; // If there are any account aliases, substitute before creating an account
// If there any account aliases, substitute before creating an account
// object. // object.
if (account_aliases.size() > 0) { account_t * result = expand_aliases(name);
accounts_map::const_iterator i = account_aliases.find(name);
if (i != account_aliases.end()) {
result = (*i).second;
} else {
// only check the very first account for alias expansion, in case
// that can be expanded successfully
size_t colon = name.find(':');
if(colon != string::npos) {
accounts_map::const_iterator j = account_aliases.find(name.substr(0, colon));
if (j != account_aliases.end()) {
result = find_account((*j).second->fullname() + name.substr(colon));
}
}
}
}
// Create the account object and associate it with the journal; this // Create the account object and associate it with the journal; this
// is registering the account. // is registering the account.
@ -178,7 +162,60 @@ account_t * journal_t::register_account(const string& name, post_t * post,
} }
} }
} }
return result;
}
account_t * journal_t::expand_aliases(string name) {
// Aliases are expanded recursively, so if both alias Foo=Bar:Foo and
// alias Bar=Baaz:Bar are in effect, first Foo will be expanded to Bar:Foo,
// then Bar:Foo will be expanded to Baaz:Bar:Foo.
// The expansion loop keeps a list of already expanded names in order to
// prevent infinite excursion. Each alias may only be expanded at most once.
account_t * result = NULL;
bool keep_expanding = true;
std::list<string> already_seen;
// loop until no expansion can be found
do {
if (account_aliases.size() > 0) {
accounts_map::const_iterator i = account_aliases.find(name);
if (i != account_aliases.end()) {
if(std::find(already_seen.begin(), already_seen.end(), name) != already_seen.end()) {
throw_(std::runtime_error,
_f("Infinite recursion on alias expansion for %1%")
% name);
}
// there is an alias for the full account name, including colons
already_seen.push_back(name);
result = (*i).second;
name = result->fullname();
} else {
// only check the very first account for alias expansion, in case
// that can be expanded successfully
size_t colon = name.find(':');
if(colon != string::npos) {
string first_account_name = name.substr(0, colon);
accounts_map::const_iterator j = account_aliases.find(first_account_name);
if (j != account_aliases.end()) {
if(std::find(already_seen.begin(), already_seen.end(), first_account_name) != already_seen.end()) {
throw_(std::runtime_error,
_f("Infinite recursion on alias expansion for %1%")
% first_account_name);
}
already_seen.push_back(first_account_name);
result = find_account((*j).second->fullname() + name.substr(colon));
name = result->fullname();
} else {
keep_expanding = false;
}
} else {
keep_expanding = false;
}
}
} else {
keep_expanding = false;
}
} while(keep_expanding && recursive_aliases);
return result; return result;
} }

View file

@ -131,6 +131,7 @@ public:
bool force_checking; bool force_checking;
bool check_payees; bool check_payees;
bool day_break; bool day_break;
bool recursive_aliases;
payee_mappings_t payee_mappings; payee_mappings_t payee_mappings;
account_mappings_t account_mappings; account_mappings_t account_mappings;
accounts_map account_aliases; accounts_map account_aliases;
@ -167,6 +168,8 @@ public:
account_t * find_account(const string& name, bool auto_create = true); account_t * find_account(const string& name, bool auto_create = true);
account_t * find_account_re(const string& regexp); account_t * find_account_re(const string& regexp);
account_t * expand_aliases(string name);
account_t * register_account(const string& name, post_t * post, account_t * register_account(const string& name, post_t * post,
account_t * master = NULL); account_t * master = NULL);
string register_payee(const string& name, xact_t * xact); string register_payee(const string& name, xact_t * xact);

View file

@ -113,6 +113,9 @@ std::size_t session_t::read_data(const string& master_account)
if (HANDLED(day_break)) if (HANDLED(day_break))
journal->day_break = true; journal->day_break = true;
if (HANDLED(recursive_aliases))
journal->recursive_aliases = true;
if (HANDLED(permissive)) if (HANDLED(permissive))
journal->checking_style = journal_t::CHECK_PERMISSIVE; journal->checking_style = journal_t::CHECK_PERMISSIVE;
else if (HANDLED(pedantic)) else if (HANDLED(pedantic))
@ -350,6 +353,9 @@ option_t<session_t> * session_t::lookup_option(const char * p)
else OPT(pedantic); else OPT(pedantic);
else OPT(permissive); else OPT(permissive);
break; break;
case 'r':
OPT(recursive_aliases);
break;
case 's': case 's':
OPT(strict); OPT(strict);
break; break;

View file

@ -109,6 +109,7 @@ public:
HANDLER(permissive).report(out); HANDLER(permissive).report(out);
HANDLER(price_db_).report(out); HANDLER(price_db_).report(out);
HANDLER(price_exp_).report(out); HANDLER(price_exp_).report(out);
HANDLER(recursive_aliases).report(out);
HANDLER(strict).report(out); HANDLER(strict).report(out);
HANDLER(value_expr_).report(out); HANDLER(value_expr_).report(out);
} }
@ -164,6 +165,7 @@ public:
OPTION(session_t, price_db_); OPTION(session_t, price_db_);
OPTION(session_t, strict); OPTION(session_t, strict);
OPTION(session_t, value_expr_); OPTION(session_t, value_expr_);
OPTION(session_t, recursive_aliases);
}; };
/** /**

View file

@ -977,6 +977,11 @@ void instance_t::account_alias_directive(account_t * account, string alias)
// (account), add a reference to the account in the `account_aliases' // (account), add a reference to the account in the `account_aliases'
// map, which is used by the post parser to resolve alias references. // map, which is used by the post parser to resolve alias references.
trim(alias); trim(alias);
// Ensure that no alias like "alias Foo=Foo" is registered.
if ( alias == account->fullname()) {
throw_(parse_error, _f("Illegal alias %1%=%2%")
% alias % account->fullname());
}
std::pair<accounts_map::iterator, bool> result = std::pair<accounts_map::iterator, bool> result =
context.journal->account_aliases.insert context.journal->account_aliases.insert
(accounts_map::value_type(alias, account)); (accounts_map::value_type(alias, account));

View file

@ -0,0 +1,12 @@
--pedantic
--explicit
alias Foo=Foo
2011-01-01 Test
Foo 10 EUR
Bar
test source -> 1
__ERROR__
While parsing file "$FILE", line 3:
Error: Illegal alias Foo=Foo
end test

View file

@ -0,0 +1,12 @@
alias A=B:A
alias B=C:B
alias C=D:C
2001-01-01 Test
A 10 EUR
Foo
test reg --recursive-aliases
01-Jan-01 Test D:C:B:A 10 EUR 10 EUR
Foo -10 EUR 0
end test

View file

@ -0,0 +1,13 @@
alias A=B:A
alias B=C:B
alias C=D:C
2001-01-01 Test
A 10 EUR
Foo
test reg
01-Jan-01 Test B:A 10 EUR 10 EUR
Foo -10 EUR 0
end test