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
appeared in the original journal file.
.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 \-related Pq Fl r
.It Fl \-related-all

View file

@ -2158,9 +2158,23 @@ alias Checking=Assets:Credit Union:Joint Checking Account
@end smallexample
The aliases are only in effect for transactions read in after the alias
is defined and are effected by @code{account} directives that precede
is defined and are affected by @code{account} directives that precede
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
@c instance_t::assert_directive
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
the file and line number of the problem.
@item --recursive-aliases
Normally, ledger only expands aliases once. With this option, ledger tries
to expand the result of alias expansion recursively, until no more expansions
apply.
@item --time-colon
The @option{--time-colon} option will display the value for a seconds
based commodity as real hours and minutes.

View file

@ -96,6 +96,7 @@ void journal_t::initialize()
check_payees = false;
day_break = false;
checking_style = CHECK_PERMISSIVE;
recursive_aliases = false;
}
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 * master_account)
{
account_t * result = NULL;
// If there any account aliases, substitute before creating an account
// If there are any account aliases, substitute before creating an account
// object.
if (account_aliases.size() > 0) {
accounts_map::const_iterator i = account_aliases.find(name);
if (i != account_aliases.end()) {
result = (*i).second;
} 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));
}
}
}
}
account_t * result = expand_aliases(name);
// Create the account object and associate it with the journal; this
// 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;
}

View file

@ -131,6 +131,7 @@ public:
bool force_checking;
bool check_payees;
bool day_break;
bool recursive_aliases;
payee_mappings_t payee_mappings;
account_mappings_t account_mappings;
accounts_map account_aliases;
@ -167,6 +168,8 @@ public:
account_t * find_account(const string& name, bool auto_create = true);
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 * master = NULL);
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))
journal->day_break = true;
if (HANDLED(recursive_aliases))
journal->recursive_aliases = true;
if (HANDLED(permissive))
journal->checking_style = journal_t::CHECK_PERMISSIVE;
else if (HANDLED(pedantic))
@ -350,6 +353,9 @@ option_t<session_t> * session_t::lookup_option(const char * p)
else OPT(pedantic);
else OPT(permissive);
break;
case 'r':
OPT(recursive_aliases);
break;
case 's':
OPT(strict);
break;

View file

@ -109,6 +109,7 @@ public:
HANDLER(permissive).report(out);
HANDLER(price_db_).report(out);
HANDLER(price_exp_).report(out);
HANDLER(recursive_aliases).report(out);
HANDLER(strict).report(out);
HANDLER(value_expr_).report(out);
}
@ -164,6 +165,7 @@ public:
OPTION(session_t, price_db_);
OPTION(session_t, strict);
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'
// map, which is used by the post parser to resolve alias references.
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 =
context.journal->account_aliases.insert
(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