http://www.perlmonks.org?node_id=504165

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

Dear Masters,
Is there a quick way to transform a HoA into AoH like this, from:
my $HoA = { flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane"], };
Into:
# Desired result: my $AoH = [ { flinstones => "fred", jetsons => "george" }, { flinstones => "fred", jetsons => "jane" }, { flinstones => "barney", jetsons => "george" }, { flinstones => "barney", jetsons => "jane" }, ];
The size of the hash and array in HoA input maybe varying.

Update: Correction made on the correct data structure of the desired results, thanks to pg comments.

Update 2: The actual HoA in my production code looks like this:
my $HoA = { '1,2,flintstones' => [ "fred-1 foo-2", "barney-1 bar-2" ], '2,3,jetsons' => [ "george-1 foo-2", "jane-1 bar-2"], };
Update 3:Additional examples:
With array of only one element
my $HoA3 = { 'flintstones' => [ "fred" ], 'jetsons' => [ "george"], }; my $ans3= [ { 'flintstones' => 'fred', 'jetsons' => 'george' } ];
With array of differing size:
my $HoA4 = { 'flintstones' => [ "fred" ], 'jetsons' => [ "george","jane"], }; my $ans4 = [ { 'flintstones' => 'fred', 'jetsons' => 'george' }, { 'flintstones' => 'fred', 'jetsons' => 'jane' } ];


---
neversaint and everlastingly indebted.......

Replies are listed 'Best First'.
Re: Converting HoA into AoH
by BrowserUk (Patriarch) on Oct 31, 2005 at 09:13 UTC

    I think this does it? And should handle your real data without problems.

    Updated: see comment for the one-line change.

    #! perl -slw use strict; use Data::Dumper; sub combs { my @inputs = @_; my @saved = my @index = map{ scalar $#$_ } @inputs; my $next = $#index; return sub{ if( $index[ 0 ] == -1 ) { @index = @saved; return (); } my $rv = [ map{ $inputs[ $_ ][ $index[ $_ ] ] } 0 .. $#inputs +]; if( $index[ $next ] ) { $index[ $next ]--; } else { $next-- until $index[ $next ] or $next == 0; $index[ $next++ ]--; @index[ $next .. $#index ] = @saved[ $next .. $#index ]; $next = $#index; } return $rv; } } sub Cnr{ my $n = shift; return [] unless $n--; map{ my $x = $_; map{ [ $_[$x], @$_ ] } Cnr( $n, @_[ ($x + 1) .. $#_ ] ); } 0 .. ($#_ - $n); } my $HoA = { flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane"], domo => [ "harry", "gato" ], }; my $AoH = [ map { my @array; my $iter = combs @{ $HoA }{ @$_ }; while( my $vals = $iter->() ) { my %hash; @hash{ @$_ } = @$vals; push @array, \%hash; } @array; } Cnr scalar keys %{ $HoA }, keys %{ $HoA } ## } Cnr 2, keys %{ $HoA } ##Replaced ]; print Dumper $AoH; __END__ P:\test>504165 $VAR1 = [ { 'flintstones' => 'barney', 'jetsons' => 'jane', 'domo' => 'gato' }, { 'flintstones' => 'barney', 'jetsons' => 'jane', 'domo' => 'harry' }, { 'flintstones' => 'fred', 'jetsons' => 'jane', 'domo' => 'gato' }, { 'flintstones' => 'fred', 'jetsons' => 'jane', 'domo' => 'harry' }, { 'flintstones' => 'barney', 'jetsons' => 'george', 'domo' => 'gato' }, { 'flintstones' => 'barney', 'jetsons' => 'george', 'domo' => 'harry' }, { 'flintstones' => 'fred', 'jetsons' => 'george', 'domo' => 'gato' }, { 'flintstones' => 'fred', 'jetsons' => 'george', 'domo' => 'harry' } ];

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      Dear BrowserUk,
      Thanks so much BrowserUk. But with your dataset example above it should give this result instead: So the size of individual size of hash must be the same with initial size of HoA.

      ---
      neversaint and everlastingly indebted.......

        There were two possibilities for the extended example, and I chose the wrong one (again).

        But it's only a one line change, to pass the number of keys in the hash rather than the constant 2-- assuming I understood this time?


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Converting HoA into AoH
by pg (Canon) on Oct 31, 2005 at 06:25 UTC

    Hopefully you understand what that data structure means. It means that, at the top level, you are creating a hash ref with two pairs of elements, each with a hash ref as a key. My guess is that you are probably looking for:

    [ { flinstones => "fred", jetsons => "george" }, { flinstones => "fred", jetsons => "jane" }, { flinstones => "barney", jetsons => "george" }, { flinstones => "barney", jetsons => "jane" }, ];

    With your data strcture, try this code:

    use Data::Dumper; use strict; use warnings; my $blah = { { flinstones => "fred", jetsons => "george" }, { flinstones => "fred", jetsons => "jane" }, { flinstones => "barney", jetsons => "george" }, { flinstones => "barney", jetsons => "jane" }, }; print Dumper($blah);

    Which prints:

    $VAR1 = { 'HASH(0x224ef8)' => { 'flinstones' => 'fred', 'jetsons' => 'jane' }, 'HASH(0x1876560)' => { 'flinstones' => 'barney', 'jetsons' => 'jane' } };
Re: Converting HoA into HoH
by Zaxo (Archbishop) on Oct 31, 2005 at 06:28 UTC

    Your desired output looks more like an AoH. It doesn't have any keys in the top level.

    Yet another combinatorial problem. Let's try glob again,

    sub hoa2aoh { my $hoa = shift; my @keys = keys %$hoa; my @patterns = map { local $" = ','; "{@$_}"; } values %$hoa; [ map { my %hash; @hash{@keys} = split ','; # char changed from "\0" \%hash; } glob join ',', @patterns ]; }
    As I say, that returns a reference to an array of hashes.

    Update: glob wasn't liking the "\0" seperator I used at first. It didn't like whitespace, either. Changed to comma, it works now. Also removed superfluous variable.

    After Compline,
    Zaxo

      Dear Zaxo,
      Thanks also for your response. Two points here. Why your subroutine above takes 2 arguments as input?

      Another point is, I tried your code,
      my $ref = hoa2aoh($HoA); print Dumper $ref;
      it returns:
      $VAR1 = [ { 'flintstones' => undef, 'jetsons' => 'george' }, { 'flintstones' => undef, 'jetsons' => 'jane' } ];
      Please correct me if I misinterpret how your function works.

      ---
      neversaint and everlastingly indebted.......

        I had an error in the argument to glob, repaired as noted above.

        The superfluous variable was $hoh in the line

        my ($hoa, $hoh) = shift;
        That doesn't really involve two arguments - the shift only brings in one value, and $hoh remained undefined and unused. I only thought I'd want it at first, and piggybacked its declaration on the arg line.

        After Compline,
        Zaxo

Re: Converting HoA into AoH
by pg (Canon) on Oct 31, 2005 at 06:57 UTC
    use Data::Dumper; use strict; use warnings; my $HoA = { flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane"], }; my $AoH; for my $flintstone (@{$HoA->{"flintstones"}}) { for my $jetson (@{$HoA->{"jetsons"}}) { push @$AoH, {"flintstones" => $ flintstone, "jetsons" => $jets +on}; } } print Dumper($AoH);
      I am surprised no-one else complained yet, but this addresses only the example using hard coding, which cannot be useful to the OP given that the number of nesting levels of your loops translates to the number of families. It should be obvious that a solution should be generic to handle a variable number of set-of-sets of unpredisclosed group names each containing an unpredisclosed number of members each with unpredisclosed names.

      Update and I should have added that, Algorithm::Loops, which someone has already mentioned, handles precisely the situation where loop nesting level is variable, although I am personally more inclined towards the explicit recursion solutions that were also offered in response.

      -M

      Free your mind

        I am surprised no-one else complained yet
        It's out of courtesy, that nobody complained...
Re: Converting HoA into AoH
by Roy Johnson (Monsignor) on Oct 31, 2005 at 18:01 UTC
    You might want to look at Algorithm::Loops to generalize the nested-loops solutions that others have offered. Here's a solution in the classical recursive vein.
    use warnings; use strict; my $HoA = { flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane"], }; use Data::Dumper; print Dumper to_AoH(%$HoA); sub to_AoH { # For no data, return an empty hashref in an arrayref @_ or return [{}]; # Otherwise, recursively map the first key's values over # the AoH of the rest of the list my ($k, $v_aref, @rest) = @_; [ map { my $href = $_; map { {%$href, $k => $_} } @$v_aref } @{to_AoH(@rest)} ] }
    Updated: simpler base case means simpler code.

    Caution: Contents may have been coded under pressure.
Re: Converting HoA into AoH
by tphyahoo (Vicar) on Oct 31, 2005 at 14:37 UTC
    Okay, here we go again, except this time with the cartesian product (minus show duplicates), obtained with Math::Combinatorics::combine. Also, we had to set Data::Dumper::Deepcopy=1 to get a sane output from Data::Dumper.
    use strict; use warnings; use Data::Dumper; use Math::Combinatorics qw(combine); my $HoA_show_characters = { flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane"], }; # create all possible combis along the lines of: #[ # { "flinstones" => "fred" }, # { "flintsones" => "barney" } #]... etc my $AoH_show_characters = []; foreach my $show (keys %$HoA_show_characters) { foreach my $character ( @{ $HoA_show_characters->{$show} } ) { push @$AoH_show_characters, ( { $show => $character } ); } } #now make all possible combis of 2 from the above aoH. # but only accept combis from different shows my $AoH_show_characters_two_at_a_time; foreach my $a_of_hash_combis( combine( 2, @$AoH_show_characters ) ) { my $key1 = ( keys %{ $a_of_hash_combis->[0] } )[0]; my $key2 = ( keys %{ $a_of_hash_combis->[1] } )[0]; unless ( $key1 eq $key2) { #key 1 equals key 2 push @$AoH_show_characters_two_at_a_time, ( $a_of_hash_combis +); } } $Data::Dumper::Deepcopy = 1; print Dumper($AoH_show_characters_two_at_a_time);
      Dear tpyahoo,
      Thanks for the answer. But why the results has to be this complex/deep? I just want simple AoH.

      ---
      neversaint and everlastingly indebted.......
        I'm not really clear on what you mean by that. Is what you had in readmore tags the result that you want? This skips over the women, so I'm assuming that's not it.

        Is it the code you are calling complex, or the resulting datastructure? What exactly is the final datastructure you are looking for?

        Do you want to take your show / char hash combos two at a time, or is one at a time okay? (In which case you get an array of eight hashes.) If an array of eight hashes is what you want, try dumping $AoH_show_characters after the second chunk of code.

        Otherwise, if you want four arrays of two hashes each, like my code produces, I can't see any way to get the job done without this kind of maybe slightly confusing nesting.

        Good luck!

Re: Converting HoA into AoH
by tphyahoo (Vicar) on Oct 31, 2005 at 13:40 UTC
    Not sure if this repeats what's already been said, but in case this helps:

    UPDATE: Except I didn't do the cartesian product... but anyways...

    use strict; use warnings; use Data::Dumper; my $AoH_show_characters = []; my $HoA_show_characters = { flintstones => [ "fred", "barney" ], jetsons => [ "george", "jane"], }; foreach my $show (keys %$HoA_show_characters) { foreach my $character ( @{ $HoA_show_characters->{$show} } ) { push @$AoH_show_characters, ( { $show => $character } ); } } print Dumper($AoH_show_characters);
Re: Converting HoA into AoH
by Zaxo (Archbishop) on Oct 31, 2005 at 17:05 UTC

    This is my previous function rewritten to handle spaces in the data by escaping them. Other surprises may be handled similarly.

    #!/usr/bin/perl use Data::Dumper; sub hoa2aoh { my $hoa = shift; my @keys = keys %$hoa; my @patterns = map { local $" = ','; "{@{[map {s/ /\\ /g; $_} @$_]}}"; } values %$hoa; [ map { my %hash; @hash{@keys} = split ','; \%hash; } glob join ',', @patterns ]; } my $HoA = { '1,2,flintstones' => [ 'fred-1 foo-2', 'barney-1 bar-2' ], '2,3,jetsons' => [ 'george-1 foo-2', 'jane-1 bar-2'], }; print Dumper hoa2aoh($HoA); __END__ $VAR1 = [ { '1,2,flintstones' => 'fred-1 foo-2', '2,3,jetsons' => 'george-1 foo-2' }, { '1,2,flintstones' => 'barney-1 bar-2', '2,3,jetsons' => 'george-1 foo-2' }, { '1,2,flintstones' => 'fred-1 foo-2', '2,3,jetsons' => 'jane-1 bar-2' }, { '1,2,flintstones' => 'barney-1 bar-2', '2,3,jetsons' => 'jane-1 bar-2' } ];

    After Compline,
    Zaxo

      Using glob is a delightful trick, but in general you really need to escape the string much more than this. (To see what I mean, try putting a * in one of the strings.)

      Instead of s/ /\\ /g, use s/(\W)/\\\\\\$1/g. I know it’s a lot of backslashes, but you really do need that many here!

Re: Converting HoA into AoH
by robin (Chaplain) on Oct 31, 2005 at 18:10 UTC

    Here is a confusing but concise recursive function that does what I think you want.

    sub hoa2aoh { my ($h, @r) = shift; keys %$h; my ($k,$v) = each %$h or return [{}]; delete $h->{$k}, return hoa2aoh($h) if !@$v; my $hd = shift @$v; my $r = hoa2aoh($h); (exists $_->{$k} ? push(@r, {%$_, $k=>$hd}) : ($_->{$k} = $hd)) foreach @$r; push @$r, @r; return $r; }
    It destroys the input, so you might need to make a deep copy of it first.

    I would explain how it works, but it’s quite a fun puzzle like this. :-)

    Update: Roy Johnson (above) has just posted a nicer implementation of the same idea.

Re: Converting HoA into HoH
by BrowserUk (Patriarch) on Oct 31, 2005 at 06:19 UTC

    Ths assumes that each array has an even number of elements.

    my $HoH = { map{ $_ => { @{ $HoA->{$_} } } } keys %{ $HoA } };

    Update: I completely misread that. Here's the correct version.

    my $HoH = { map{ $_ => { map{ $_ => } @{ $HoA->{$_} } } } keys %{ $Ho +A } };

    Update2: And still it was wrong. I think you have your answer now, so I'll shut up.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    Lingua non convalesco, consenesco et abolesco. -- Rule 1 has a caveat! -- Who broke the cabal?
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      Dear BrowserUk,
      Thanks a lot for the response. But your code above returns this only:
      $VAR1 = { 'flintstones' => { 'fred' => 'barney' }, 'jetsons' => { 'george' => 'jane' } };
      Please correct me if I'm wrong. And also the size of the array may not be even.

      ---
      neversaint and everlastingly indebted.......
Re: Converting HoA into AoH
by Moron (Curate) on Oct 31, 2005 at 14:38 UTC
    To get all the combinations of the data being asked for (or 'Cartesian Product' as one response put it), there is always Math::Combinatorics

    -M

    Free your mind