Christophe
Christophe

Reputation: 216

Perl: How to turn array into nested hash keys

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";

Upvotes: 4

Views: 2385

Answers (4)

stealth
stealth

Reputation: 359

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'
                          }
                 }
        };

Upvotes: 0

singingfish
singingfish

Reputation: 3167

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;
}

Upvotes: 1

Christophe
Christophe

Reputation: 216

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...

Upvotes: 0

Axeman
Axeman

Reputation: 29854

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.

Upvotes: 12

Related Questions