Last night a friend passed along a short problem to me. I need to take the keys for a multi-level hash, say $hash{a}{b}{c}{d}, and create that structure from just the list qw(a b c d), without using eval or a module, and it had to be really short, and it had to be delivered right away
After my initial solution, I came up with this concept (the argument checking and so on to be done by someone else). I use two hash references. One references the top level of the hash I want to create, and the other one is something like a cursor inside that hash so I can move around in it.
sub set_multi_level_hash { my( $hash, $tiers, $value ) = @_;
my $work_hash = $hash; foreach my $tier ( @$tiers ) { $work_hash->{ $tier } = {} unless exists $work_hash->{ $tier }; $work_hash = $work_hash->{ $tier }; } $work_hash = _get_multi_hash_last_level( $hash, $tiers );
$work_hash->{ $tiers->[-1] } = $value; }
sub _get_multi_hash_last_level { my( $hash, $tiers ) = @_; foreach my $tier ( @{$tiers}[0..$#$tiers-1] ) { $hash = $hash->{ $tier }; } $hash; }
use strict;
use warnings;
use Data::Dumper;
my $hash = {};
set_multi_level_hash($hash, [qw/ a b c d /], 'potato');
print Dumper $hash;
sub set_multi_level_hash
{
my ( $hash, $tiers, $value ) = @_;
for (0 .. $#$tiers)
{
$hash->{$tiers->[$_]} ||= ( $_ == $#$tiers ? $value : {} );
$hash = $hash->{$tiers->[$_]};
}
}
Re:make use of the indices of the $tiers
danboo on 2004-04-28T21:44:36
Oops, to be able to change the value, I suppose the first line in the for loop should be split into:
$hash->{$tiers->[$_]} = {} unless exists $hash->{$tiers->[$_]};
$hash->{$tiers->[$_]} = $value if $_ == $#$tiers;
Re:make use of the indices of the $tiers
brian_d_foy on 2004-04-28T22:21:01
Yeah, I thought about something like that, but then I should go through 0..$#$tiers-1, and do the special case for the last value.
Oh well, too late now. It is out of my hands.
Thanks for the code:)
Re:make use of the indices of the $tiers
jhi on 2004-04-29T06:30:09
I think one needs to specify what happens in this case:
set_multi_level_hash($h, [qw(a b c)], 42);
set_multi_level_hash($h, [qw(a b)], 66);
Oops, we just nuked the first value and the whole "subhash".
In other words, is the level of multiness going to stay the same?
Personally, I would either create a class that guards against the multiness changing, or would make the data structure to be an array reference of hash references, keyed off in the array reference on the depth of $tiers (the latter can of course be wrapped into a class, too.)
Also, after this
set_multi_level_hash($h, [qw(a b c)], 42);
what should this return:
get_multi_level_hash($h, [qw(a b)]);
Are we supposed to get the "subhash", or not, or should we have two different functionalities, "get one value" and "get a subhash with potentially multiple values"?
Re:make use of the indices of the $tiers
brian_d_foy on 2004-04-29T06:52:38
What you are "supposed" to get depends on what your requirements are. In this case, you assign a different value to a hash key, you replace the value, just like you do if you had written it out as "$h->{a}{b} = 42".
I suppose you could create a class to limit operations, but then, you probably would have need ed a lot more time to deliver the code. One of the requirements was "right now". I didn't build in a lot of extra stuff to make variables not be variable.
There is no get_multi_level_hash function, so it shouldn't return anything. If you write one, you can decide what it should do.:)
I had to do someting similar to create a multilevel hash table that returns a list of "cached" things given a list of parameters that represent the hashing value.
The main problem was not creating the various anonymous hashes, but that the last item is not a {} but a []. The trick was to keep a reference to the previous step, to be able to go back and turn a newly created hash reference into an anonymous array.
# $thing is a hash reference
# @keys is the list of $thing attributes I'm using for the hashing algorithm
# $hashtable is a reference to the hash table itself
my $list = $hashtable;
my $last;
($last, $list) = ( $list, $list->{$thing->{$_}} ||= {} ) for @keys;
$list = $last->{$thing->{$keys[-1]}} = [] if ref $list eq 'HASH';
# and now $list points to the list of interesting items
# which was created (empty) if needed
Using $list, $last adds extra obfuscation points, but that wasn't intended, since those are fitting names.