简体   繁体   中英

Perl: sort a hash by value, then by key

similar to this question: sort by subset of perl string

I would like to sort first by value, then by subset of the key.

my %hash

 cat_02 => 0
 cat_04 => 1
 cat_03 => 0
 cat_01 => 3

the output : (could be an array of the keys in this order)

cat_02 => 0
cat_03 => 0
cat_04 => 1
cat_01 => 3

Bonus :the key secondary comparison would recognize 1234_2_C01 and being smaller than 1234_34_C01 (cmp does not)

my %hash = (
  cat_02 => 0,
  cat_04 => 1,
  cat_03 => 0,
  cat_01 => 3
);

print "$_ => $hash{$_}\n"
  for sort { $hash{$a} <=> $hash{$b} or $a cmp $b } keys %hash;

The sort does numeric comparison of the values, and if they're equal, the part after the or is executed, which does string comparison of the keys. This gives the output you asked for.

For smart sorting of strings that contain numbers mixed with non-numeric stuff, grab the alphanum comparison function from http://www.davekoelle.com/alphanum.html and replace $a cmp $b with alphanum($a,$b)

When you have a secondary sort preference, you simply add another level inside the sort routine:

my %hash = (
    cat_02 => 0,
    cat_04 => 1,
    cat_03 => 0,
    cat_01 => 3
);

my @sorted = sort { $hash{$a} <=> $hash{$b} || $a cmp $b } keys %hash;
                  #  primary sort method    ^^ secondary sort method
for (@sorted) {
    print "$_\t=> $hash{$_}\n";
}

Output:

cat_02  => 0
cat_03  => 0
cat_04  => 1
cat_01  => 3

This can easily be done (quickly!) using the Sort::Key:: modules:

use Sort::Key::Natural qw( );
use Sort::Key::Maker intnatkeysort => qw( integer natural );

my @sorted_keys = intnatkeysort { $hash{$_}, $_ } keys(%hash);

Or you can take advantage of the properties of your data and just use a natural sort:

use Sort::Key::Natural qw( natkeysort );

my @sorted_keys = natkeysort { "$hash{$_}-$_" } keys(%hash);

It may be not worth it in this particular case, but Schwartzian transform technique can be used with multi-criteria sort too. Like this ( codepad ):

use warnings;
use strict;

my %myhash = (
  cat_2 => 0, cat_04 => 1,
  cat_03 => 0, dog_02 => 3, 
  cat_10 => 0, cat_01 => 3,
);

my @sorted = 
    map { [$_->[0], $myhash{$_->[0]}] } 
    sort { $a->[1] <=> $b->[1]  or  $a->[2] <=> $b->[2] } 
    map { m/([0-9]+)$/ && [$_, $myhash{$_}, $1] } 
    keys %myhash;

print $_->[0] . ' => ' . $_->[1] . "\n" for @sorted;

Obviously, the key to this technique is using more than one additional element in the cache.

Two things here: 1) @sorted actually becomes array of arrays (each element of this array is key-value pair); 2) sorting in this example is based on keys' digits suffix (with numeric, not string comparison), but it can be adjusted in any direction if needed.

For example, when keys match pattern XXX_DD_XXX (and it's DD that should be compared), change the second map clause with this:

    map { m/_([0-9]+)_/ && [$_, $myhash{$_}, $1] } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM