4

I need to convert a flat list of keys into a nested hash, as follow:

my $hash = {};

my @array = qw(key1 key2 lastKey Value);

ToNestedHash($hash, @array);

Would do this:

$hash{'key1'}{'key2'}{'lastKey'} = "Value";

0

4 Answers 4

12
sub to_nested_hash {
    my $ref   = \shift;  
    my $h     = $$ref;
    my $value = pop;
    $ref      = \$$ref->{ $_ } foreach @_;
    $$ref     = $value;
    return $h;
}

Explanation:

  • Take the first value as a hashref
  • Take the last value as the value to be assigned
  • The rest are keys.
  • Then create a SCALAR reference to the base hash.
  • Repeatedly:
    • Dereference the pointer to get the hash (first time) or autovivify the pointer as a hash
    • Get the hash slot for the key
    • And assign the scalar reference to the hash slot.
    • ( Next time around this will autovivify to the indicated hash ).
  • Finally, with the reference to the innermost slot, assign the value.

We know:

  • That the occupants of a hash or array can only be a scalar or reference.
  • That a reference is a scalar of sorts. (my $h = {}; my $a = [];).
  • So, \$h->{ $key } is a reference to a scalar slot on the heap, perhaps autovivified.
  • That a "level" of a nested hash can be autovivified to a hash reference if we address it as so.

It might be more explicit to do this:

foreach my $key ( @_ ) { 
    my $lvl = $$ref = {};
    $ref    = \$lvl->{ $key };
}

But owing to repeated use of these reference idioms, I wrote that line totally as it was and tested it before posting, without error.

As for alternatives, the following version is "easier" (to think up)

sub to_nested_hash {
    $_[0] //= {};
    my $h     = shift;
    my $value = pop;
    eval '$h'.(join '', map "->{\$_[$i]}", 0..$#_).' = $value';
    return $h;
}

But about 6-7 times slower.

Sign up to request clarification or add additional context in comments.

5 Comments

Could you add an explanation, please?
+1 I didn't think to autovivify a hash element by taking a reference to it
@Borodin, it was a god-send when I found it would work. I was using some multi-level hash transforms to do some feed processing and it sped up my most-used routines 600%-1000%.
Made a minor change so my $h; to_nested_hash($h, @keys, $val); works.
Made a minor change so keys such as foo bar and system("rm -rf /") work properly.
1

I reckon this code is better - more amenable to moving into a class method, and optionally setting a value, depending on the supplied parameters. Otherwise the selected answer is neat.

#!/usr/bin/env perl

use strict;
use warnings;
use YAML;

my $hash = {};

my @array = qw(key1 key2 lastKey);
my $val = [qw/some arbitrary data/];

print Dump to_nested_hash($hash, \@array, $val);
print Dump to_nested_hash($hash, \@array);
sub to_nested_hash {
    my ($hash, $array, $val) = @_;
    my $ref   = \$hash;
    my @path = @$array;
    print "ref: $ref\n";
    my $h     = $$ref;
    $ref      = \$$ref->{ $_ } foreach @path;
    $$ref     = $val if $val;
    return $h;
}

Comments

0

Thxs for the good stuff!!!

I did it the recursive way:

sub Hash2Array
{
  my $this = shift;
  my $hash = shift;

  my @array;
  foreach my $k(sort keys %$hash)
  {
    my $v = $hash->{$k};
    push @array,
      ref $v eq "HASH" ? $this->Hash2Array($v, @_, $k) : [ @_, $k, $v ];
  }

  return @array;
}

It would be interesting to have a performance comparison between all of these solutions...

Comments

0

Made a better version of axeman's i think. Easier to understand without the -> and the \shift to me at least. 3 lines without a subroutine.

With subroutine

sub to_nested_hash {
    my $h=shift;
    my($ref,$value)=(\$h,pop);
    $ref=\%{$$ref}{$_} foreach(@_);
    $$ref=$value;
    return $h;
}

my $z={};
to_nested_hash($z,1,2,3,'testing123');

Without subroutine

my $z={};

my $ref=\$z; #scalar reference of a variable which contains a hash reference
$ref=\%{$$ref}{$_} foreach(1,2,3); #keys
$$ref='testing123'; #value

#with %z hash variable just do double backslash to get the scalar reference
#my $ref=\\%z;

Result:

$VAR1 = {
          '1' => {
                   '2' => {
                            '3' => 'testing123'
                          }
                 }
        };

1 Comment

The method without the subroutine didn't compile for me. The $ref=\%{$$ref}{$_} foreach(1,2,3); has a problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.