30

In Perl, I want to sort the keys of a hash by value, numerically:

{
  five => 5
  ten => 10
  one => 1
  four => 4
}

producing two arrays:

(1,4,5,10) and (one, four, five, ten)

And then I want to normalize the values array such that the numbers are sequential:

(1,2,3,4)

How do I do this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gogi
  • 1,695
  • 4
  • 23
  • 36

4 Answers4

68

First sort the keys by the associated value. Then get the values (e.g. by using a hash slice).

my @keys = sort { $h{$a} <=> $h{$b} } keys(%h);
my @vals = @h{@keys};

Or if you have a hash reference.

my @keys = sort { $h->{$a} <=> $h->{$b} } keys(%$h);
my @vals = @{$h}{@keys};
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • 2
    That was easy. Sometimes it's difficult to come up with those nice shortcuts. Thanks ikegami. – Gogi Jun 05 '12 at 19:14
  • You can see more info in [sort function in Perl](http://stackoverflow.com/questions/6454744/sort-function-in-perl/6454804#6454804) – yc_yuy Feb 24 '14 at 06:57
7

How do I sort a hash (optionally by value instead of key)?

To sort a hash, start with the keys. In this example, we give the list of keys to the sort function which then compares them ASCIIbetically (which might be affected by your locale settings). The output list has the keys in ASCIIbetical order. Once we have the keys, we can go through them to create a report which lists the keys in ASCIIbetical order.

my @keys = sort { $a cmp $b } keys %hash;

foreach my $key ( @keys ) {
    printf "%-20s %6d\n", $key, $hash{$key};
}

We could get more fancy in the sort() block though. Instead of comparing the keys, we can compute a value with them and use that value as the comparison.

For instance, to make our report order case-insensitive, we use lc to lowercase the keys before comparing them:

my @keys = sort { lc $a cmp lc $b } keys %hash;

Note: if the computation is expensive or the hash has many elements, you may want to look at the Schwartzian Transform to cache the computation results.

If we want to sort by the hash value instead, we use the hash key to look it up. We still get out a list of keys, but this time they are ordered by their value.

my @keys = sort { $hash{$a} <=> $hash{$b} } keys %hash;

From there we can get more complex. If the hash values are the same, we can provide a secondary sort on the hash key.

my @keys = sort {
$hash{$a} <=> $hash{$b}
or
"\L$a" cmp "\L$b"
} keys %hash;
Eric Leschinski
  • 146,994
  • 96
  • 417
  • 335
  • 2
    Nit: `lc($a) cmp lc($b)`, which you also wrote as `"\L$a" cmp "\L$b"`, won't always do the right thing. You want `fc($a) cmp fc($b)` (`"\F$a" cmp "\F$b"`). Available since 5.16. – ikegami Jul 15 '13 at 13:27
  • Note that I wrote this answer for perlfaq4 (https://perldoc.perl.org/perlfaq4#How-do-I-sort-a-hash-(optionally-by-value-instead-of-key)?) and it was reposted here missing the attribution in the answer. And, as @ikegami notes, I wrote this before fc() was a thing. – brian d foy Jun 23 '23 at 07:42
  • I've made a pull request to perlfaq for this https://github.com/perl-doc-cats/perlfaq/pull/102 – brian d foy Jun 23 '23 at 07:54
3
my ( @nums, @words );
do { push @nums,  shift @$_; 
     push @words, shift @$_; 
   }
    foreach sort { $a->[0] <=> $b->[0] } 
            map  { [ $h->{ $_ }, $_ ] } keys %$h
   ;
Axeman
  • 29,660
  • 2
  • 47
  • 102
0

Sometimes it's best to show rather than tell...

%results = (Paul=>87, Ringo=>93, John=>91, George=>97);

#display the results in ascending key (alphabetical) order
print "key ascending...\n";
foreach $key ( sort { $a cmp $b } keys %results ){
    print "$key=>$results{$key}\n";
}

print "\n";

# display the results in descending key (alphabetical) order
print "key descending...\n";
foreach $key ( sort { $b cmp $a } keys %results ){
    print "$key=>$results{$key}\n";
}

print "\n";

# display the results in descending value (numerical) order
print "value ascending...\n";
foreach $key ( sort { $results{$a} <=> $results{$b} } keys %results ){
    print "$key=>$results{$key}\n";
}

print "\n";

# display the results in ascending value (numerical) order
print "value descending...\n";
foreach $key ( sort { $results{$b} <=> $results{$a} } keys %results ){
    print "$key=>$results{$key}\n";
}
Clarius
  • 1,183
  • 10
  • 10