Perl: How to turn array into nested hash keys

2019-01-28 11:38发布

问题:

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

回答1:

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.



回答2:

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



回答3:

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