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

I have the following table being generated...
Key1 Key2 Value -------------------------------------- red bike 5 red car 4 red shoes 20 yellow shoes 1 yellow skates 1000
What I want to do is have the same table but sorted on the Value column. Therefore I would see 1000 at the top and then 20 next, etc... I read from a delimited file and create a hash of a hash of an array. The array holds the value, a counter, which is incremented within a while loop with the following line
while (<>){ ... $hash{key1}{key2}[0]++; ... }
I currently print the table above with the following:
for $colour (sort keys %hash){ for $device (sort keys %{ $hash{$colour}}){ printf("\n%-55s %-55s %-40s", $colour, $device, $hash{$colour}{$device}[0] ); } }
How do I print the table by sorting strictly on the value?

Replies are listed 'Best First'.
Re: Sorting multi-hash values
by holli (Abbot) on Aug 22, 2007 at 17:57 UTC
    I read from a delimited file and create a hash of a hash of an array.
    See and that exactly is the problem :) Use an AoA and the sort will become trivial.

    Update: Of course you can transform the data to an AoA first:
    my @data; for $colour (sort keys %hash){ for $device (sort keys %{ $hash{$colour}}){ push @data, [ $colour, $device, $hash{$colour}{$device}[0] ] +; } } @data = reverse sort { $a->[2] <=> $b->[2] } @data;


    holli, /regexed monk/
      How did you consider to use reverse instead of $b->[2] <=> $a->[2]?

      Open source softwares? Share and enjoy. Make profit from them if you can. Yet, share and enjoy!

        Because I'm stoned.


        holli, /regexed monk/

        Actually the 'reverse sort' combination is recognized by perl and optimized away for simple comparisons.

        Thus it's actually not a bad idea

        Isn't reverse sort optimized to do both operations at once?
Re: Sorting multi-hash values
by TGI (Parson) on Aug 22, 2007 at 19:05 UTC

    Assuming that you are actually storing more than one value in the final array, and that you need to two level hash to do various lookups, the following approach should work for you.

    First, collect all your data. Then create an array of key pairs. This array will provide the basis for any sorted orderings you may wish to create. Now you can sort the key pair array into whatever order you wish. You may want to look at the sort docs and sorting examples in perldsc for more help.

    # Read in test data. while (<DATA>) { my ( $k1, $k2, $v ) = split; $hash{$k1}{$k2}[0] = $v } # Make an array of all key pairs. my @key_pairs; foreach my $k1 ( keys %hash ) { foreach my $k2 ( keys %{$hash{$k1}} ) { push @key_pairs, [$k1, $k2]; } } # Sort key pairs my @key_pairs_by_value = sort { $hash{ $b->[0] }{ $b->[1]}[0] <=> $hash{ $a->[0] }{ $a->[1]}[0] } @key_pairs; # print sorted for my $keys ( @key_pairs_by_value ){ my ($colour, $device) = @$keys; printf("\n%-55s %-55s %-40s", $colour, $device, $hash{$colour}{$device}[0] ); }


    TGI says moo

Re: Sorting multi-hash values
by dwm042 (Priest) on Aug 22, 2007 at 19:13 UTC
    This was my take on the problem:

    #!/usr/bin/perl use warnings; use strict; my %hash = (); my %value_hash = (); while(<DATA>) { chomp; my ($first_key, $second_key, $value ) = split /\s+/; $hash{$first_key}{$second_key} = $value; } # # Map the hash of a hash of a value into a # hash of the value containing an array of key pairs. # Then sort on the new hash. # for my $colour ( sort keys %hash ) { for my $device ( sort keys %{$hash{$colour}} ) { my $val = $hash{$colour}{$device}; printf "%-15s %-15s %5d\n", $colour, $device, $val; push @{$value_hash{ $val }} , [ $colour, $device ]; } } print "\n\nSorted Result:\n\n"; for my $val ( sort { $b <=> $a } keys %value_hash ) { for my $key_pair ( @{$value_hash{$val}} ) { printf "%-15s %-15s %5d\n", @$key_pair[0], @$key_pair[1], $val; } } __DATA__ red bike 5 red car 4 red shoes 20 yellow shoes 1 yellow skates 1000
    And the output is:

    C:\Code>perl multi_hash_sort.pl red bike 5 red car 4 red shoes 20 yellow shoes 1 yellow skates 1000 Sorted Result: yellow skates 1000 red shoes 20 red bike 5 red car 4 yellow shoes 1
      I believe this is very close but the problem now is my new hash, "$value_hash", can only hold one unique key. Therefore if I have list of values like:
      colour device value ---------------------- red bike 1000 red shoes 1000 blue car 4 black plane 6 blue boat 1000 pink shoes 5 red car 5
      The $value_hash will only have the following list:
      colour device value ------------------------ blue boat 1000 black plane 6 red car 5 blue car 4
      A hashes' keys must be unique, otherwise they get over written with the most recent entry. To prevent losing data, incase I have count values which are duplicates, I recreate a new hash with 3 keys and a value. The keys are "value", "colour", and "device". Creating New Hash "value_hash" (2nd hash):
      foreach my $colour ( keys %hash) { foreach my $device ( keys %{$hash{$colour}}) { my $val = $hash{$colour}{$device}[0]; $value_hash{$val}{$colour}{$device}[0]++; } }
      After that I then print the key (value) in order:
      for my $value (sort {$b <=> $a} keys %value_hash) { for my $colour ( keys %{$value_hash{$value}}) { for my $os ( keys %{$value_hash {$value}{$colour}}) { printf("\n%-55s %-50s %-10s", $colour, $os, $value); } } }
      Which now produces table...
      colour device value ---------------------- red bike 1000 red shoes 1000 blue boat 1000 black plane 6 pink shoes 5 red car 5 blue car 4
      Thank you for pointing me in the right direction monks...Papai
Re: Sorting multi-hash values
by GrandFather (Saint) on Aug 22, 2007 at 21:09 UTC

    For interest. May not be suitable in production code. ;)

    use strict; use warnings; my %hash; my $counter; while (<DATA>) { chomp; my ($key1, $key2) = split; $hash{$key1}{$key2}[0] = ++$counter; } printf "%-12s%-12s%-12s\n", @{$_}[1, 2, 0] for ['Value', 'Key1', 'Key2 +'], ['-' x 12, '-' x 12, '-' x 12], sort {$b->[0] <=> $a->[0]} map {[$hash{$_->[0]}{$_->[1]}[0], @$_]} map {my $key = $_; map {[$key, $_]} keys %{$hash{$_}}} keys %hash; __DATA__ red bike red car red shoes yellow shoes yellow skates

    Prints:

    Key1 Key2 Value ------------------------------------ yellow skates 5 yellow shoes 4 red shoes 3 red car 2 red bike 1

    DWIM is Perl's answer to Gödel