Jabda
Jabda

Reputation: 1792

Perl: sort a hash by value, then by key

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).

Upvotes: 3

Views: 4590

Answers (4)

user2404501
user2404501

Reputation:

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).

Upvotes: 10

ikegami
ikegami

Reputation: 385655

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

Upvotes: 3

raina77ow
raina77ow

Reputation: 106385

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

Upvotes: 1

TLP
TLP

Reputation: 67900

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

Upvotes: 4

Related Questions