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

Brethern --

I want to sort a multidimensional hash by one of it's values and want to be able to build the sort criteria on the fly. I can get this to work fine if it isn't in a subroutine but as soon as I do this in a sub (passing the hash by reference), I start getting wacky results.

I am including the code I have been using to test and wondering what I am doing wrong.

Many thanks in advance,
mdog

$hash{'Me'}->{"name"} = "Me"; $hash{'Me'}->{"number"} = "7.7"; $hash{'Chuck'}->{"name"} = "Chuck"; $hash{'Chuck'}->{"number"} = "7.7"; $hash{'Zed'}->{"name"} = "Zed"; $hash{'Zed'}->{"number"} = "7.7"; $hash{'Wife'}->{"name"} = "Wife"; $hash{'Wife'}->{"number"} = "7.6"; $hash{'Dad'}->{"name"} = "Dad"; $hash{'Dad'}->{"number"} = "53"; $hash{'Michael'}->{"name"} = "Michael"; $hash{'Michael'}->{"number"} = "24"; test(\%hash); sub test{ my($hr) = @_; # sort criteria embedded: expected results @sortedKeys = sort { $hr->{$b}->{"number"} <=> $hr->{$a}->{"number +"} } keys %$hr; print "sort criteria embedded\n"; foreach my $sortedKey(@sortedKeys){ print $hr->{$sortedKey}->{"number"} . " " . $hr->{$sortedKey}- +>{'name'} . "\n"; } # sort criteria eval-ed: wacky results my $sortCriteria = '$hr->{$b}->{"number"} <=> $hr->{$a}->{"number" +}'; # WHAT AM I DOING WRONG ON THE NEXT LINE? @sortedKeys = sort { eval $sortCriteria } keys %$hr; print "\n\nsort criteria eval-ed\n"; foreach my $sortedKey(@sortedKeys){ print $hr->{$sortedKey}->{"number"} . " " . $hr->{$sortedKey}- +>{'name'} . "\n"; } }

The results:

sort criteria embedded 53 Dad 24 Michael 7.7 Me 7.7 Zed 7.7 Chuck 7.6 Wife sort criteria eval-ed 53 Dad 7.6 Wife 7.7 Me 7.7 Zed 24 Michael 7.7 Chuck

Replies are listed 'Best First'.
Re: Sorting Hash: Sort Criteria Moved Into Eval
by keszler (Priest) on Aug 06, 2004 at 00:53 UTC
    On this:

    C:\>perl -v This is perl, v5.8.4 built for MSWin32-x86-multi-thread

    both lists are sorted identically using your exact code.

    Using:

    $ perl -v This is perl, v5.6.1 built for i386-linux

    the results are the same as yours. Changing the eval'd version to:

    # sort criteria eval-ed: wacky results my $sortCriteria = sub { $hr->{$b}->{"number"} <=> $hr->{$a}->{"numb +er"};}; # WHAT AM I DOING WRONG ON THE NEXT LINE? @sortedKeys = sort { &{$sortCriteria} } keys %$hr; print "\n\nsort criteria eval-ed\n"; foreach my $sortedKey(@sortedKeys){ print $hr->{$sortedKey}->{"number"} . " " . $hr->{$sortedKey}->{'n +ame'} . "\n"; }

    yields an identically sorted list, leading me to:

    # sort criteria eval-ed: wacky results my $sortCriteria = '$hr->{$b}->{"number"} <=> $hr->{$a}->{"number"}; +'; # WHAT AM I DOING WRONG ON THE NEXT LINE? @sortedKeys = sort { &x($sortCriteria) } keys %$hr; print "\n\nsort criteria eval-ed\n"; foreach my $sortedKey(@sortedKeys){ print $hr->{$sortedKey}->{"number"} . " " . $hr->{$sortedKey}->{'n +ame'} . "\n"; } sub x { eval shift; }

    yielding the same identically sorted list but using an easily changed string.

Re: Sorting Hash: Sort Criteria Moved Into Eval
by BrowserUk (Patriarch) on Aug 06, 2004 at 01:33 UTC

    I also only see the problem on 5.6.1 and not on 5.8.4, which suggests it is a bug that has been fixed.

    The cause is that (in 5.6.1) the values of $a and $b are undef inside eval.

    Which makes every eval produce a "Use of uninitialized value in numeric comparison (<=>) at (eval 1) line 1." when warnings are enabled.

    (And unhelpfully, a segfault under strict!).

    The sort order that you see after the evaled version is probably just the natural key iteration order as no sort has taken place.

    Use warnings (-w) chalks up another victory :)

    ps. There has to be a better way than using eval in a sort block.


    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "Think for yourself!" - Abigail
    "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon
Re: Sorting Hash: Sort Criteria Moved Into Eval
by davido (Cardinal) on Aug 06, 2004 at 02:13 UTC

    I was playing around with something similar a few months ago, and posted a working startingpoint as Fun with complex sorting on arbitrary criteria list.. Later in that same thread tilly, stvn and others contributed some ideas on how to improve on my brainstorm. You may find the thread useful as an inspiration for your current situation.

    It's a fun thing to think about; building flexible sort criteria on the fly through the use of eval.


    Dave

Re: Sorting Hash: Sort Criteria Moved Into Eval
by Aristotle (Chancellor) on Aug 06, 2004 at 02:33 UTC

    You're evaling the code on every execution of the sort block — that's terribly inefficient. Try something like this:

    my $sortCriteria = '$hr->{$b}->{"number"} <=> $hr->{$a}->{"number"}'; my $sortfunc = eval "sub { $sortCriteria }'; @sortedKeys = sort $sortfunc keys %$hr;

    That compiles the code just once, puts it in an anonymous function, and stores a reference to that function in $sortfunc. sort then simply calls upon this already-compiled function to compare two values.

    Wrapping evaled expressions in a sub { } like that is usually advisable when you're want to run the code passed in the string in a loop.

    Makeshifts last the longest.

      Thanks aristotle for your exemple. It got me to understand much better eval with sort. Here is how I solved my problem. I wanted to sort on column 0, then 1, then 2... variably depending on user input. Here is my test code maybe it will help others.
      push my @tableau,[222,234523676,26256,5645,634,56,3456,5345623]; push @tableau,[333,2346,2356457,347,7745,67,3462]; push @tableau,[3.3,2341,2356457,347,7745,67,3462]; push @tableau,[333,2349,2356457,347,7745,67,3462]; push @tableau,[777,67567,4568458,45845,48458,4578]; push @tableau,[999,67567,4568458,45845,48458,4578]; push @tableau,[444,67567,4568458,45845,48458,4578]; push @tableau,[111,67567,4568458,45845,48458,4578]; my $grandeur; foreach (@tableau){ my $compteur = 0 unless $compteur; $grandeur = $#{$tableau[$compteur]} if ($grandeur < $#{$tableau[$c +ompteur]}); $compteur++; } my @sorter; foreach (0..$grandeur){ my $sortCriteria = "\$tableau[\$b][$_] <=> \$tableau[\$a][$_]"; push @sorter,$sortCriteria; } $sorter = join " or ",@sorter; print "$sorter\n"; my $sortfunc = eval "sub { $sorter }"; @sortedKeys = sort $sortfunc 0..$#tableau; print "@{$tableau[$_]}\n" foreach @sortedKeys;
      Good luck.
Re: Sorting Hash: Sort Criteria Moved Into Eval
by Limbic~Region (Chancellor) on Aug 06, 2004 at 13:09 UTC
    mdog,
    Have you considered using Tie::Hash::Sorted? It allows you to dynamically change the sort routine which it seems you want to do. Additionally, it allows you to modify the hash arbitrarily and still maintain its "sortedness".
    #!/usr/bin/perl use strict; use warnings; use Tie::Hash::Sorted; my $custom_sort = sub { my $h = shift; [ sort {$h->{$b}{number} <=> $h->{$a}{number}} keys %$h ]; }; tie my %hash, 'Tie::Hash::Sorted', 'Sort_Routine' => $custom_sort; my @keys = qw(name number); @{ $hash{$_} }{ @keys } = ($_, '7.7') for qw(Me Chuck Zed); @{ $hash{Wife} }{ @keys } = qw(Wife 7.6); @{ $hash{Dad} }{ @keys } = qw(Dad 53); @{ $hash{Michael} }{ @keys } = qw(Michael 24); print "$hash{$_}{number}\t$hash{$_}{name}\n" for keys %hash; @{ $hash{Foo} }{ @keys } = qw(foo 42); $custom_sort = sub { my $h = shift; [ sort {$h->{$a}{name} cmp $h->{$b}{name}} keys %$h ]; }; tied( %hash )->Sort_Routine( $custom_sort ); print "$hash{$_}{name}\t$hash{$_}{number}\n" for keys %hash;

    Cheers - L~R

      Many, many thanks to all of you...When my script doesn't run the way I want it to, I think there is a problem with what I have done, not that Perl has a bug in it! :)

      Aristotle, I will definitely move the eval out so that it only compiles once and Limbic~Region, I'll investigate Tie::Hash::Sorted as it looks very interesting!

      mdog