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

Beloved Monks,
The quest of a newbie into Perl realms is dangerous and adventurous. I am seeking knowledge from the elders regarding on how can I sort a hash permanently (for example by sorting its keys).

Example:

use strict; my %foo = (1 => 'value1',2 => 'value2',3 => 'value3');

Every time I want to print the hash values sorted by their keys, I have do this:

print "$foo{$_}\n" foreach sort {$a <=> $b} keys %foo;

Instead of repeating the sorting, I want to write a subroutine that will permanently sort the hash.

sub sort_hash { my %hash = @_; #do the sorting part here, please help :) return %hash; } my %foo_sorted = sort_hash(%foo); print "$foo_sorted{$_}\n" foreach keys %foo_sorted;

The last print should produce the same results as the first print.
Bless me with your knowledge!

Replies are listed 'Best First'.
Re: Permanently sort a hash
by ikegami (Patriarch) on Jun 04, 2009 at 14:53 UTC

    If you don't want to call sort repeatedly, save the result.

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

    Update: Or if you no longer need the keys:

    my @sorted_values = @foo{ sort { $a <=> $b } keys(%foo) };
      ikegami,
      While you know and it should be obvious, it may be worth pointing out that any modification to the keys of the hash (inserts or deletes) requires the sorted keys to be re-cached.

      Cheers - L~R

        Yes, but that would also be the case for the ideal solution the OP posted.
Re: Permanently sort a hash
by kennethk (Abbot) on Jun 04, 2009 at 14:43 UTC
      kennethk,
      Actually, that FAQ is wrong.

      Although this does keep your hash sorted, you might not like the slow down you suffer from the tie interface.

      What Tie::IxHash actually does is preserve your insertion order. It does have rudimentary capability to re-order by asciibetical sorted keys or values but it is not persistent. If you add key/val pairs, they don't insert into their sorted position and you have to re-order them again. You also can't do anything more complex when sorting.

      Tie::Hash::Sorted OTOH does answer the FAQ but I feel a bit odd sending a doc patch to p5p pimping my own module.

      Cheers - L~R

      Thank you for such a quick reply!
      The reason why I wanted it sorted, is because I have to print frequently a hash where key order matters. Instead of sorting every time the keys before looping I wanted to have it sorted permanently.

        ikegami's solution is a good one then - cache the sorted keys. You can even forgo the foreach loop using hash Slices:

        my @sorted_keys = sort { $a <=> $b } keys(%foo); print join("\n",@hash{@sorted_keys})
Re: Permanently sort a hash
by herveus (Prior) on Jun 04, 2009 at 15:01 UTC
    Howdy!

    Why do you feel you need your hashes to behave in a way they weren't meant to?

    The short (and flippant) answer to your question is "you can't". Hashes are unordered collections by their very nature. If you want the keys in a specific order, you have to impose that order on the list that you get by calling keys(). If the set of keys is static, you can store the list in an array in any desired order. That array will keep them in that order always, but now you have to keep the array and the hash in sync.

    You cannot reliably predict the order that keys() will produce for any set of keys with more than one item. The order will depend on the set of values and the order in which they were added to the hash. If you examine the source code for your specific instance of perl, you can examine the hash function. Then you have to understand how that interacts with keys() to extract the list.

    Of course, there are modules that will let you impose an ordering on keys(), but they have to subvert the standard behavior of hashes.

    This question is analogous to asking for the rows in a table in a database to be kept "sorted". All that really means is "I don't want to have to sort the data myself each time I use it". One of the "costs" of using a hash is that the data is inherently unordered. If you must have it in a specific order, you either have to sort it into that order each time, or you have to use an array to maintain the desired order. Or you have to put an ORDER BY clause on your SELECT statement.

    yours,
    Michael
Re: Permanently sort a hash
by Arunbear (Prior) on Jun 04, 2009 at 15:54 UTC
    Try DB_File (from the FAQ mentioned by kennethk), e.g.
    use strict; use warnings; use DB_File; tie my %foo, "DB_File", undef, undef, undef, $DB_BTREE; %foo = (1 => 'value1',2 => 'value2',3 => 'value3'); while(my($k, $v) = each %foo) { print "$k -> $v\n"; }
      Interesting, but it might not apply here. It returns the keys in lexical order (1,10,2,3,4,5,6,7,8,9) while the OP is sorting the keys in numerical order (1,2,3,4,5,6,7,8,9,10).
        The default sort is lexical, but it can be made numeric:
        use strict; use warnings; use DB_File; $DB_BTREE->{'compare'} = sub { $_[0] <=> $_[1] }; tie my %foo, "DB_File", undef, undef, undef, $DB_BTREE; for (1 .. 10) { $foo{$_} = 'value' . $_; } while(my($k, $v) = each %foo) { print "$k -> $v\n"; }
Re: Permanently sort a hash
by jettero (Monsignor) on Jun 04, 2009 at 15:03 UTC
    Don't forget Tie::IxHash and what L~R says below.

    -Paul

      jettero,
      Yes, but while that module provides rudimentary sorting capability - it is primarily designed to preserve insertion order. I wrote Tie::Hash::Sorted for far more complex sorting needs as well as a number of optimizations under the covers.

      Cheers - L~R

Re: Permanently sort a hash
by Anonymous Monk on Jun 04, 2009 at 15:30 UTC

    Thank you everyone for your replies. Appreciate your help a lot, especially herveus's explanation! I will give a try to Tie::Hash::Sorted although I think ikegami's solution is the simplest in my case.