nikmit has asked for the wisdom of the Perl Monks concerning the following question:

I guess this is not possible by design, but just in case I am missing something...

Is there a clever/efficient way to make this work (other than with a loop)?

my %hash = map { delete $_->{id_1} => { delete $_->{id_2} => $_ } } @ary_of_hrefs;

It should produce the same result as the below (except not a reference)

my $hash; foreach my $href (@ary_of_hrefs) { my $id_1 = delete $href->{id_1}; my $id_2 = delete $href->{id_2}; $hash->{$id_1}->{$id_2} = $href; }

Replies are listed 'Best First'.
Re: map into a multidimensional hash
by choroba (Cardinal) on Mar 09, 2017 at 12:19 UTC
    If there's always just one id_2 per id_1, you can use
    my %hash = map +($_->{id_1} => { $_->{id_2} => $_->{v} }), @aoh;

    Otherwise, you need to assign to the inner hash not to overwrite a previous branch.

    Tried with

    my @aoh = ( { id_1 => "F1", id_2 => "F2", v => "FV" }, { id_1 => "S1", id_2 => "S2", v => "SV" }, { id_1 => "T1", id_2 => "T2", v => "TV" }, { id_1 => "T1", id_2 => "T3", v => "TW" }, );

    Another approach would be to use Data::Diver:

    use Data::Diver qw{ DiveVal }; my %hash; DiveVal(\%hash, @$_{qw{ id_1 id_2 }}) = $_->{v} for @aoh;

    ($q=q:Sq=~/;[c](.)(.)/;chr(-||-|5+lengthSq)`"S|oS2"`map{chr |+ord }map{substrSq`S_+|`|}3E|-|`7**2-3:)=~y+S|`+$1,++print+eval$q,q,a,

      Wow, I completly failed to see that the issue was with multiple elements sharing the same id_1 with different id_2. In which case the map solution isn't just harder to read, it's also incorrect. I'd ++ multiple times if I could.

      >   DiveVal(\%hash, @$_{qw{ id_1 id_2 }}) = $_->{v} for @aoh;

      Well you are not deleting the ids from the $_ hash and assigning the value instead of $_.

      I'm also not sure why the hash slice works, most probably fresh syntax, I'd probably prefer listing the two values explicitly.

      Cheers Rolf
      (addicted to the Perl Programming Language and ☆☆☆☆ :)
      Je suis Charlie!

Re: map into a multidimensional hash
by Eily (Monsignor) on Mar 09, 2017 at 10:38 UTC

    Same as Discipulus++, I get the same output in both cases. Edit: that's because the map version is incorrect when an id_1 is repeated for distinct id_2, which the AoH that I constructed fails to demonstrate (as pointed out by choroba).

    For me, the clever way to do it would be one that is easy to read, so I'd go for a for loop rather than map

    If you are searching for new/unusual ways to do thing you can use postfix dereferencement to get the two values in one go:

    use feature 'postderef'; # Needed in perl 5.20 and 5.22 my @aoh = ( { id_1 => "F1", id_2 => "F2", v => "FV" }, { id_1 => "S1", id_2 => "S2", v => "SV" }, { id_1 => "T1", id_2 => "T2", v => "TV" }, ); my %hash; foreach my $href (@aoh) { my ($id_1, $id_2) = delete $href->@{'id_1', 'id_2'}; $hash{$id_1}{$id_2} = $href; }

    But if you want to do the construction in two lines (you still have to declare the output hash), you can use the postfix version of for

    my %hash; $hash{delete $_->{id_1}}{delete $_->{id_2}} = $_ for @aoh;

    Edit: postfix deref is only available since perl v5.20, and the explicit use of the feature is required up to v5.22

    Edit: s/[@]ao\Kf/h/; Thanks johngg

Re: map into a multidimensional hash
by marioroy (Prior) on Mar 09, 2017 at 10:26 UTC

    Hello nikmit,

    Starting from Perl 5.8.1, map is context aware, and will "not" construct a list if called in void context.

    use Data::Dumper; my @ary_of_hrefs = ( { id_1 => 'a', id_2 => 'b', f1 => 'p', f2 => 'i' }, { id_1 => 'a', id_2 => 'c', f1 => 'e', f2 => 'm' }, { id_1 => 'b', id_2 => 'b', f1 => 'r', f2 => 'h' }, { id_1 => 'b', id_2 => 'c', f1 => 'l', f2 => 'o' } ); my %hash; map { $hash{ delete $_->{id_1} }{ delete $_->{id_2} } = $_ } @ary_of_hrefs; print Dumper(\%hash), "\n";

    Output:

    $VAR1 = { 'a' => { 'b' => { 'f1' => 'p', 'f2' => 'i' }, 'c' => { 'f1' => 'e', 'f2' => 'm' } }, 'b' => { 'b' => { 'f1' => 'r', 'f2' => 'h' }, 'c' => { 'f1' => 'l', 'f2' => 'o' } } };

    Map, in void context, is similar to a simple for loop.

    for ( @ary_of_hrefs ) { $hash{ delete $_->{id_1} }{ delete $_->{id_2} } = $_; }

    Regards, Mario.

    Edit: Added link to Perl 5.8.1 changelog.

    Edit: Modified input to have two fields.

Re: map into a multidimensional hash
by Discipulus (Canon) on Mar 09, 2017 at 10:21 UTC
    for sure i misunderstand the question.. but i get very similar results:

    use strict; use warnings; use Data::Dumper; my @ary_of_hrefs = ({id_1=>'A',id_2=>'B'},{id_1=>'V',id_2=>'Z'}); my %hash = map { delete $_->{id_1} => { delete $_->{id_2} => $_ } } @a +ry_of_hrefs; print Dumper \%hash; my $hash; foreach my $href ({id_1=>'A',id_2=>'B'},{id_1=>'V',id_2=>'Z'}) { my $id_1 = delete $href->{id_1}; my $id_2 = delete $href->{id_2}; $hash->{$id_1}->{$id_2} = $href; } print Dumper \$hash; __output__ $VAR1 = { 'A' => { 'B' => {} }, 'V' => { 'Z' => {} } }; $VAR1 = \{ 'A' => { 'B' => {} }, 'V' => { 'Z' => {} } };

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
Re: map into a multidimensional hash
by Anonymous Monk on Mar 09, 2017 at 10:06 UTC

    No

    You can't access a %hash before it defined, so you can't do  $hash{$left}{$right} within a map statement that assigns to %hash

Re: map into a multidimensional hash
by LanX (Saint) on Mar 09, 2017 at 15:17 UTC
    Nobody ° offered a postfix for, so for completeness
    $hash{ delete $_->{id_1} }{ delete $_->{id_2} } = $_ for @ary_of_hrefs ;

    Untested, but most code was copied from marioroy's post :)

    Personally I try to avoid map statements in void context

    For clarity I strongly recommend commenting this with a sample of the data before and after.

    (addicted to the Perl Programming Language and ☆☆☆☆ :)
    Je suis Charlie!

    update

    °) Sorry Eily did, tldr my apologies :)