2
votes

Similar to this question: Sort by subset of a Perl string

I would like to sort first by value, and 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).

4
2_ is smaller than 34 both numerically and stringwise so that's a bad example. But I know what you meant. updated my answer - user2404501

4 Answers

8
votes

Use:

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 The Alphanum Algorithm and replace $a cmp $b with alphanum($a,$b).

4
votes

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
3
votes

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);
1
votes

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] }