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

Hi, This is probably going to be simple... mind not working today..

So I have have a array of hash references, firstly I want to sort it on price order and then I want to sort it dependent on having a particular key.. So lets say I have this

[ { 'vtype_ug' => 1 + 'total_rate' => 400, }, { 'total_rate' => 220, }, { 'total_rate' => 700, 'vtype_ug' => 1, }, { 'total_rate' => 300, }, { 'total_rate' => 400, }, { 'total_rate' => 250, }, ]
I want it to be sorted like
[ { 'vtype_ug' => 1 + 'total_rate' => 400, }, { 'total_rate' => 700, 'vtype_ug' => 1, }, { 'total_rate' => 220, }, { 'total_rate' => 250, }, { 'total_rate' => 300, }, { 'total_rate' => 400, }, ]
I tried this... it almost works but not quite...
my @sorted = sort { (exists $a->{vtype_ug}) ? -1 : 0} sort { $a->{total_rate} <=> $b->{total_rate} }
I know its probably just my little brain not working very well.. but any help appreciated.

Replies are listed 'Best First'.
Re: Sorting on Exists
by davorg (Chancellor) on Sep 08, 2006 at 10:13 UTC

    You're doing two separate sorts. Which sorts by the first criteria and then resorts by the second criteria (completely ignoring the first).

    If you want to sort by two criteria then you need to do both comparisons in the same sorting block.

    use strict; use warnings; my @data = ( { vtype_ug => 1, total_rate => 400, }, { total_rate => 220, }, { total_rate => 700, vtype_ug => 1, }, { total_rate => 300, }, { total_rate => 400, }, { total_rate => 250, }, ); my @sorted = sort { -(exists $a->{vtype_ug} <=> exists $b->{vtype_ug}) || $a->{total_rate} <=> $b->{total_rate} } @data; use Data::Dumper; print Dumper @sorted;

    Note: The negation on the first comparison ensures that elements with "vtype_ug" come before those who don't (without it the order is reversed).

    --
    <http://dave.org.uk>

    "The first rule of Perl club is you do not talk about Perl club."
    -- Chip Salzenberg

      You're doing two separate sorts. Which sorts by the first criteria and then resorts by the second criteria (completely ignoring the first).
      No, that's not true. His first (last executed) sort code block is wrong, but in a modern perl, i.e. one with a table sort, the concept is valid: you can sort items according to one field, while keeping items that compare to the same value in their original order. Just like sgifford shows in Re: Sorting on Exists.
Re: Sorting on Exists
by holli (Abbot) on Sep 08, 2006 at 09:18 UTC
    This works, but is maybe not as effective as it could be:
    my @sorted = sort { $a->{total_rate} <=> $b->{total_rate} } @array; @sorted = ( (grep { defined $_->{vtype_ug} } @sorted), (grep { not defined $_->{vtype_ug} } @sorted) );
    Note that the round brackets here are necessary.

    Update:
    Another way:
    my @sorted = map { $_->{total_rate} *= -1 if $_->{vtype_ug}; $_ } sort { $a->{total_rate} <=> $b->{total_rate} } map { $_->{total_rate} *= -1 if $_->{vtype_ug}; $_ } @array ;


    holli, /regexed monk/
Re: Sorting on Exists
by artist (Parson) on Sep 08, 2006 at 11:25 UTC
    Benchmarking:
                    Rate  holi_updated holi_original        davorg
    holi_updated  19608/s            --          -14%          -35%
    holi_original 22676/s           16%            --          -25%
    davorg        30211/s           54%           33%            --
    
    --Artist
      holli, that is!


      holli, /regexed monk/
Re: Sorting on Exists
by sgifford (Prior) on Sep 08, 2006 at 16:14 UTC
    Assuming a recent version of Perl, I think your original code would work if the first sort were:
    sort { exists $b->{vtype_ug} <=> exists $a->{vtype_ug} }
    (untested, but I think it's the right idea)

    A sort sub should return positive if $a is greater than $b, 0 if they are equal, and negative if $b is greater than $a. So if you want items with vtype_ug to always sort before items that don't have it, you need to return -1 if you find it only in $a, 1 if you find it only in $b, or 0 if it is in both or neither.

    According to sort's documentation, in Perl 5.7 and newer sorts are "stable", which means that if two items are equal they'll stay in the same order. This should ensure that two sorts work. However, as others have mentioned, you can expect better performance and better portability if you use one sort sub that does both comparisons:

    sort { (exists $b->{vtype_ug} <=> exists $a->{vtype_ug}) || ($a->{total_rate} <=> $b->{total_rate}) }

    Update: bart is right on all points, and I've corrected them above. Thanks!

      Assuming a recent version of Perl, I think your original code would work if the first sort were:
      sort { (exists $a->{vtype_ug}||0) <=> (exists $b->{vtype_ug}||0) }
      You've got the compare function giving the opposite result than desired, but yes, it does work (in 5.8.8, at least), with this code:
      @sorted = sort { (exists $b->{vtype_ug}||0) <=> (exists $a->{vtype_ug} +||0) } sort { $a->{total_rate} <=> $b->{total_rate} } @arr;
      Actually, there's no need for that || 0 as exists returns a boolean, thus, that's safe to use as 0 when it returns false. So the next works too, without any warnings:
      @sorted = sort { exists $b->{vtype_ug} <=> exists $a->{vtype_ug} } sort { $a->{total_rate} <=> $b->{total_rate} } @arr;