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

Monks,

I have been trying to create a sorter for a semi-complex data structure. First I store the data structure using Storable, and then I retreive it. The data structure comes back as a hashref and I dereference it back to a hash. Basically the sorter does what its called. It either sorts by alpha of the keys (else), total (if) or value (elsif). The data structure itself looks as follows:

Data structure snippet:

$VAR3 = 'URIBL_OB_SURBL+MPART_ALT_DIFF_COUNT'; $VAR4 = { 'Total' => 10, 'Value' => '3.716' };
$VAR5 = 'BAYES_00+FORGED_RCVD_HELO'; $VAR6 = { 'Total' => 5, 'Value' => '-2.464' }; $VAR7 = 'BAYES_00+HTML_LINK_PUSH_HERE'; $VAR8 = { 'Total' => 1, 'Value' => '-2.202' }; $VAR9 = 'SPF_HELO_PASS'; $VAR10 = { 'Total' => 16, 'Value' => '-0.001' }; $VAR11 = 'BAYES_95+HTML_MIME_NO_HTML_TAG'; $VAR12 = { 'Total' => 1, 'Value' => '4.082' };

I included both methods that I attempted. I can't figure out how to get the sorter to sort (which is one of the primary problems). Sorter creation code:

if ($sort eq 't') { $sorter = sub { sort values %{ do { my %tosort; for my $test (keys %tests) { # Prints values correctly # Values go into hash correctly # print "Test: $test -- Total: ". $tests{$test}{"Total"} ."\n"; $tosort{$test} = $tests{$test}{"Total"}; } return \%tosort; }; } };
} elsif ($sort eq 'v') { $sorter = sub { sort values %{ sub { my %tosort; for my $test (keys %tests) { # Prints values correctly # Values go into hash correctly # print "Test: $test -- Total: ". $tests{$test}{"Value"} ."\n"; $tosort{$test} = $tests{$test}{"Value"}; } return \%tosort; }; } }; } else { $sorter = sub { sort keys %tests }; }

Regardless of the sorter being called, I want to be able to access the keys and the values so I can use them at a later moment.

while ( my ($test,$val) = each %{$sorter->()} ) { print "Test: ". $test Val: $val\n"; # Sorter value # Show me the assocaited %tests stuff print " Total: ". $tests{$test}{"Total"} ."\n"; print " Value: ". $tests{$test}{"Value"} ."\n\n"; }

Basically, the sorter doesn't sort the way I want to (unless its sorting by alpha key), and then assuming the sorter is sorted by "Total", the while loop doesn't give me the key/val of the information I need. And I believe the information I need is the key of the sorted value so I can use it to display the associated values in the %tests global hash.

I am sure that this is a little confusing and I tried my best to articulate the problem as best as possible. Thanks for the help.

Eric

Update: Thanks to everyone who aided me. I ended up using the following method:

sub Print_Tests($$) { my $sort = shift || "a"; my $order = shift || "d"; my (@ordered_keys); # Create the sorter if (($sort eq 't') && ($order eq "a")) { @ordered_keys = sort { $tests{$a}{"Total"} <=> $tests{$b}{"Total"} + } keys %tests; } elsif (($sort eq 't') && ($order eq "d")) { @ordered_keys = sort { $tests{$b}{"Total"} <=> $tests{$a}{"Total"} + } keys %tests; } elsif (($sort eq 'v') && ($order eq "d")) { @ordered_keys = sort { $tests{$b}{"Value"} <=> $tests{$a}{"Value"} + } keys %tests; } elsif (($sort eq 'v') && ($order eq "a")) { @ordered_keys = sort { $tests{$a}{"Value"} <=> $tests{$b}{"Value"} + } keys %tests; } elsif (($sort eq 'a') && ($order eq "a")) { @ordered_keys = sort keys %tests; } else { @ordered_keys = sort reverse keys %tests; } foreach my $key (@ordered_keys) { my $val = $tests{$key}; print "$key\n"; print " Total: $val->{Total}\n"; print " Value: $val->{Value}\n\n"; } }

Replies are listed 'Best First'.
Re: Sorting a hash of a hash using anonymous subs
by Fletch (Bishop) on Jul 27, 2006 at 14:34 UTC

    Hashes are by their very nature unsorted. Calling each, keys, or values on a vanilla hash isn't going to return things in any sort of sorted order. Your sorter does nothing useful as any ordering is lost when you insert things into the new hash.

    What you really need to do is sort a list of the keys using one of your coderefs, then use that to iterate over the contents.

Re: Sorting a hash of a hash using anonymous subs
by ikegami (Patriarch) on Jul 27, 2006 at 15:33 UTC
    while ( my ($test,$val) = each %{$sorter->()} ) calls &$sorter every time through the loop. That meeans that &$sorter creates a new hash and sorts it every time through the loop. It also means you keep calling each on a new hash every time. None of that is good. Also, you want the keys, but you discard them by using values.

    The trick is to sort the keys (by value, if so desired) into an array, and loop over that array.

    my @ordered_keys; if ($sort eq 't') { # Calculate the order in which we want to process %tests. # (Sort keys by ascending numerical Total.) @ordered_keys = sort { $tests{$a}{Total} <=> $tests{$b}{Total} } keys %tests; } elsif ($sort eq 'v') { # Calculate the order in which we want to process %tests. # (Sort keys by ascending numerical Value.) @ordered_keys = sort { $tests{$a}{Value} <=> $tests{$b}{Value} } keys %tests; } foreach my $key (@ordered_keys) { my $val = $tests{$key}; print "$key\n"; print " Total: $val->{Total}\n"; print " Value: $val->{Value}\n\n"; }

    By the way, you were sorting alphabetically. Now, it's sorting numerically.

    By the way, when dumping a hash or an array, pass a reference to the variable to Dumper (e.g. print Dumper \%tests;) for better results.

    Update: The only purpose of using a sub would be to delay the calculation of @ordered_keys. If that's what you wish to do, what follows is a transformation of the above which postpones the calculation of @ordered_keys by use of a sub:

    my $sorter; if ($sort eq 't') { # Sort keys by ascending numerical Total. $sorter = sub { my ($tests) = @_; return sort { $tests->{$a}{Total} <=> $tests->{$b}{Total} } keys %$tests; }; } elsif ($sort eq 'v') { # Sort keys by ascending numerical Value. $sorter = sub { my ($tests) = @_; return sort { $tests->{$a}{Value} <=> $tests->{$b}{Value} } keys %$tests; }; } foreach my $key ($sorter->(\%tests)) { my $val = $tests{$key}; print "$key\n"; print " Total: $val->{Total}\n"; print " Value: $val->{Value}\n\n"; }

    foreach will build the list over which it iterates only once, so it doesn't suffer the same problem as your while solution.

Re: Sorting a hash of a hash using anonymous subs
by johngg (Canon) on Jul 27, 2006 at 22:09 UTC
    I think this does what you want. It works in a similar fashion to your code by using a sort factory to return different sorting routines depending on what was requested. The sorting routines returned can cope with changes to the data structure. I have used an anonymous hash here but you could pass the sort factory a reference to the hash you are using through the rest of the code. Here is the data and the sort factory

    use strict; use warnings; my $rhDataStruct = { q{URIBL_OB_SURBL+MPART_ALT_DIFF_COUNT} => { Total => 10, Value => 3.716 }, q{BAYES_00+FORGED_RCVD_HELO} => { Total => 5, Value => -2.464 }, q{BAYES_00+HTML_LINK_PUSH_HERE} => { Total => 1, Value => -2.202 }, q{SPF_HELO_PASS} => { Total => 16, Value => -0.001 }, q{BAYES_95+HTML_MIME_NO_HTML_TAG} => { Total => 1, Value => 4.082 } }; sub makeSorter { my $rhToSort = shift; my $sortType = shift || q{}; if ($sortType eq q{t}) { return sub { return sort { $rhToSort->{$a}->{Total} <=> $rhToSort->{$b}->{Total} } keys %$rhToSort; }; } elsif ($sortType eq q{v}) { return sub { return sort { $rhToSort->{$a}->{Value} <=> $rhToSort->{$b}->{Value} } keys %$rhToSort; }; } else { return sub { return sort keys %$rhToSort; }; } }

    Here is the rest of the script that does some testing trying different sorts and adding to the data

    and here is the output

    I hope this is of use.

    Cheers,

    JohnGG

    Update: Tidied testing code by putting repetitive prints into a subroutine.

    Update 2: Corrected typo in updated code.

Re: Sorting a hash of a hash using anonymous subs
by ysth (Canon) on Jul 27, 2006 at 15:34 UTC
    Didn't look at your actual sorts, but this part
    while ( my ($test,$val) = each %{$sorter->()} ) {
    isn't going to work very well. &$sorter is going to be called for each iteration of the while loop, and, unless you arrange it to return a reference to the same hash each time, it will get the first key/value over and over.
Re: Sorting a hash of a hash using anonymous subs
by Moron (Curate) on Jul 27, 2006 at 16:04 UTC
    I don't see the need to deref the hashref into a separate hash - it seems much handier as it stands. For example:
    for my $mainkey ( sort keys %$hashref ) { # but then use it my $total = $hashref -> { $mainkey ) { Total }; my $value = $hashref -> { $mainkey ) { Value }; # and do whatever with those }

    -M

    Free your mind

      The reason that I use a has instead of a hashref is that it is used as a hash through the rest of the program. In order to continue this without having to rewrite everything, I just dereference it into a regular hash when I Storable::retrieve() it.

      Eric

        I'm not pushing that issue then, but you could still do something like this:
        for my $mainkey ( sort keys %copy ) { my $subref = $copy{ $mainkey }; my $total = $subref -> { Total }; my $value = $subref -> { Value }; # process $mainkey, $total and $value ... }

        -M

        Free your mind

Re: Sorting a hash of a hash using anonymous subs
by ikegami (Patriarch) on Jul 28, 2006 at 15:21 UTC

    I have two comments concerning the solution you chose.

    First, don't use prototypes unless necessary. They cause a lot of headaches.

    Second, you can simplify your code by handling sorting and ascending/descending seperately.

    sub Print_Tests { my $sort = shift || "a"; my $order = shift || "d"; my (@ordered_keys); # Determine the order. if ($sort eq 't') { @ordered_keys = sort { $tests{$a}{"Total"} <=> $tests{$b}{"Total"} + } keys %tests; } elsif ($sort eq 'v')) { @ordered_keys = sort { $tests{$a}{"Value"} <=> $tests{$b}{"Value"} + } keys %tests; } else { @ordered_keys = sort keys %tests; } @ordered_keys = reverse @ordered_keys if $order eq 'd'; foreach my $key (@ordered_keys) { my $val = $tests{$key}; print "$key\n"; print " Total: $val->{Total}\n"; print " Value: $val->{Value}\n\n"; } }