Adding support for recursive aliases.

Alias expansion is now a loop. If you define

alias A=B:A
alias B=C:B

then A will expand to C:B:A.

Also added a short section to the manual about this.
This commit is contained in:
Peter Feigl 2014-02-25 22:50:20 +01:00
parent bc08eed3cb
commit c80b495546
4 changed files with 74 additions and 20 deletions

View file

@ -2158,9 +2158,22 @@ 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.
Aliases can refer to other aliases, the following example produces exactly
the same accounts 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

View file

@ -121,26 +121,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 +161,58 @@ 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
while(keep_expanding) {
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;
}
}
}
}
return result; return result;
} }

View file

@ -167,6 +167,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

@ -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));