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

Hello,
I was advised by a member of Perl Guru to ask at Perl Monks whether it would be possible or not to perform a type of sort i'm trying to achieve. I would rather not perform this sort outside of the sort block, as I will later choose the type of sort from a hash lookup of subroutine refs.
Here is my code:
foreach my $colour ( sort { if ($products{'Cat'}{'Pro'}{$a}{'Qua'} <= 0) {return 1;} elsif ($products{'Cat'}{'Pro'}{$b}{'Qua'} <= 0) {return -1;} } sort {$a <=> $b || $a cmp $b} keys %{$products{'Cat'}{'Pro'}} ) {

Here is its output (example):
Colour Quantity 70 (Grey brown mix) 3 72 (Green mix) 10 74 (Fuschia mix) 8 76 (Mauve mix) 11 77 (Blue mix) 6 90 (Beige pink mix) 12 91 (Peach cream mix) 8 92 (Silver cream mix) 10 93 (Beige blue mix) 9 78 (Black white mix) 0 75 (Red mix) 0 73 (Aqua mix) 0 71 (Beige mix) 0

As you can see, colours with a quantity of more than 0 are displayed above those with a quantity of 0. However, each group must then be sorted in alphabetical order, and although close, products with a quantity of 0 are being undesirably sorted in reverse.
I can see why the code doesn't work as desired, but i'm unable see a way to adjust it to suit my full requirements.
Is it possible without breaking out of the sort block?
Chris

Replies are listed 'Best First'.
Re: Complex conditional sort
by Limbic~Region (Chancellor) on Mar 09, 2011 at 00:19 UTC
    Zhris,
    If I had to do this, I would probably use a Schwartzian Transform. First, the explanation: Prepend each color with a 1 character value depending on if it is above 0 or not. Then, perform a normal ASCIIbetical sort on the list. Finally, remove the leading character. *Untested*
    my @list = map {substr($_, 1)} sort map {$lookup{$_}{val} > 0 ? "A$_" : "B$_"} @list;

    Update: You asked if this could be done without breaking out of a sort block because you would be choosing the block from a hash. You could just as easily use a subroutine instead of a sort block but you could do the same trick in the block itself:

    sort { my $lhs = $lookup{$a}{val} > 0 ? "A$a" : "B$a"; my $rhs = $lookup{$b}{val} > 0 ? "A$b" : "B$b"; $lhs cmp $rhs }

    Cheers - L~R

Re: Complex conditional sort
by GrandFather (Saint) on Mar 09, 2011 at 00:29 UTC

    So you need to condition the sort test result on quantity and colour.

    use strict; use warnings; my %cat; while (<DATA>) { next if ! /^(\d+)\s+(\d+)$/; $cat{$1} = $2; } my @order = sort { $cat{$a} && $cat{$b} ? $a <=> $b : $cat{$a} <=> $cat{$b} || $a <=> $b } keys %cat; printf "%-3d %-3s\n", $_, $cat{$_} for @order; __DATA__ 70 3 72 10 74 8 76 11 77 6 90 12 91 8 92 10 93 9 78 0 75 0 73 0 71 0

    Prints:

    71 0 73 0 75 0 78 0 70 3 72 10 74 8 76 11 77 6 90 12 92 10 93 9

    The ternary operator ?: selects compare by colour if neither count is 0, or $cat{$a} <=> $cat{$b} || $a <=> $b which first compares by count then (if both counts are the same) compares by colour.

    True laziness is hard work
Re: Complex conditional sort
by wind (Priest) on Mar 09, 2011 at 00:43 UTC
    Do a Schwartzian Transform to capture the color code and a boolean to indicate whether there is a count, then sort, and finally unpack and print.
    #!/usr/bin/perl -w use strict; print map {$_->[2]} sort {$b->[1] <=> $a->[1] || $a->[0] <=> $b->[0]} map {/(\d+).*?(\d+)/ ? [$1, !!$2, $_] : ()} <DATA>; 1; __DATA__ Colour Quantity 70 (Grey brown mix) 3 72 (Green mix) 10 74 (Fuschia mix) 8 76 (Mauve mix) 11 77 (Blue mix) 6 90 (Beige pink mix) 12 91 (Peach cream mix) 8 92 (Silver cream mix) 10 93 (Beige blue mix) 9 78 (Black white mix) 0 75 (Red mix) 0 73 (Aqua mix) 0 71 (Beige mix) 0
    Outputs
    70 (Grey brown mix) 3 72 (Green mix) 10 74 (Fuschia mix) 8 76 (Mauve mix) 11 77 (Blue mix) 6 90 (Beige pink mix) 12 91 (Peach cream mix) 8 92 (Silver cream mix) 10 93 (Beige blue mix) 9 71 (Beige mix) 0 73 (Aqua mix) 0 75 (Red mix) 0 78 (Black white mix) 0

    Update: L~R pointed out that I did not use the initial data structure that you described, and therefore my result wasn't as helpful. Unfortunately, you do not state what actually are the keys of the third level hash. Assuming they are the colour codes and the names are inside the hash with Qua, then the following would work:

    #!/usr/bin/perl -w use strict; my %products; $products{Cat}{Pro} = { 70 => {Name => 'Grey brown mix', Qua => 3}, 72 => {Name => 'Green mix', Qua => 10}, 74 => {Name => 'Fuschia mix', Qua => 8}, 76 => {Name => 'Mauve mix', Qua => 11}, 77 => {Name => 'Blue mix', Qua => 6}, 90 => {Name => 'Beige pink mix', Qua => 12}, 91 => {Name => 'Peach cream mix', Qua => 8}, 92 => {Name => 'Silver cream mix', Qua => 10}, 93 => {Name => 'Beige blue mix', Qua => 9}, 78 => {Name => 'Black white mix', Qua => 0}, 75 => {Name => 'Red mix', Qua => 0}, 73 => {Name => 'Aqua mix', Qua => 0}, 71 => {Name => 'Beige mix', Qua => 0}, }; foreach my $colour ( sort {!!$products{Cat}{Pro}{$b}{Qua} <=> !!$products{Cat}{Pro}{$a} +{Qua} || $a <=> $b} keys %{$products{Cat}{Pro}} ) { printf "%-2s %-22s %s\n", $colour, "($products{Cat}{Pro}{$colour}{ +Name})", $products{Cat}{Pro}{$colour}{Qua}; } 1; __END__
    - Miller
      I'd recommend against using the negation operator to return a positive value for true, not so much because there no guarantee that it will continue to do so, but because the reader probably won't know it returns positive for true. (Especially since some languages use a negative value for true!) It's not like $x && 1 or $x ?1:0 would be a burden to use instead of !!$x.

        Fair enough ikegami.

        I like using the !!$test construct to force a boolean to 1 or (''/0). However, if that isn't clear enough, I'd probably choose using the >0 test.

        sort {$products{Cat}{Pro}{$b}{Qua}>0 <=> $products{Cat}{Pro}{$a}{Q +ua}>0 || $a <=> $b}

        I suppose it still requires some level of understanding of the possible return values of conditionals, but that's one of the most important things to be familiar with when dealing with sorting in my opinion. Especially the <=> and cmp operators.

      Thank you everyone for your suggestions. I tried most of your examples, although a couple didn't work as I desired (unless I implemented them incorrectly). I have used Limbic~Region's second example, its tidy and functions well:
      foreach my $col ( sort { my $aa = $products{$a}{Q} > 0 ? "A$a" : "B$a"; my $bb = $products{$b}{Q} > 0 ? "A$b" : "B$b"; ($aa <=> $bb || $aa cmp $bb) } keys %products ) {
      Thanks again, Chris

        Why did you reintroduce ($aa <=> $bb || $aa cmp $bb)? First, it goes against what you said ("each group must then be sorted in alphabetical order"). Secondly, it implies that you have colour names that start with numbers, and that doesn't make much sense. Maybe you are doing a cheap "natural sort", but you'd would at least need a no warnings; to accompany it.

        Update: Expanded to clarify.

Re: Complex conditional sort
by ikegami (Patriarch) on Mar 09, 2011 at 00:12 UTC

    [ I'm assuming the key is the colour's name (e.g. "Grey brown mix"). ]

    Compare quantities, breaking ties by comparing names:

    sort { $products{'Cat'}{'Pro'}{$a}{'Qua'} <=> $products{'Cat'}{'Pro'}{$b} +{'Qua'} || $a cmp $b } keys %{ $products{'Cat'}{'Pro'} }

    Repeating $products{'Cat'}{'Pro'} all over the place is a bit silly, though.

    my $pro_products = $products{Cat}{Pro}; sort { $pro_products->{$a}{Qua} <=> $pro_products->{$b}{Qua} || $a cmp $b } keys %$pro_products
      ikegami,
      Perhaps I am missing something, but I don't see how this solves the problem. The colors must be sorted alphabetically in two groups. The first group should be those with quantities above 0 and the second group should be 0. As you have described it, they will be ordered first by quantity (correctly putting all colors with quantities in front of those without) but within the groups you will have the wrong alphabetical order.

      Cheers - L~R

        Oops, I thought he wanted to group by quantity. To group by presence/absence (presence first), it's simplest to solidify the concept into values that can be compared with <=> or cmp. Let's pick zero for presence and one for absence (since zero comes before one).

        my $pro_products = $products{Cat}{Pro}; sort { $pro_products->{$a}{Qua} ?0:1 <=> $pro_products->{$b}{Qua} ?0:1 || $a cmp $b } keys %$pro_products

        Update: Down below, L~R picked "A" and "B" instead of 0 and 1. Same idea.

Re: Complex conditional sort
by Zhris (Initiate) on Mar 09, 2011 at 04:32 UTC
    Hello, I have now had the time to look at the code properly and have a clear understanding of how a Schwartzian Transform works. Its so simple, yet works so well in my case. My initial reply was rushed, while I quickly tested every potential solution, during a busy time. I have adjusted the sort routine code to improve on "critisms" made. Thank you very much everyone for your input. I am very surprised at the high level of knowledge and skill here, but to be honest I didn't expect anything less from monks! Chris
Re: Complex conditional sort
by salva (Canon) on Mar 09, 2011 at 09:54 UTC
    use Sort::Key::Multi 'is_keysort'; # 'is' stands for an integer and a string keys my @sorted = is_keysort { ($products{Cat}{Pro}{$_}{Qua} ? 0 : 1), $_ } keys %{$products{'Cat'}{'Pro'}};