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

Hello Monks...

This seems like it should be easy, but I'm drawing a blank and I'm not sure what to type in Super Search to get more help.

I have an array whose elements look like this:

Item1 - 2 foo, 2 bar
Item2 - 0 foo, 1 bar
Item3 - 1 foo, 3 bar
Item4 - 1 foo, 2 bar


and I would like to sort them in descending order by bar:

Item3 - 1 foo, 3 bar
Item1 - 2 foo, 2 bar
Item4 - 1 foo, 2 bar
Item2 - 0 foo, 1 bar


I'm not certain how to approach this problem: split or regex? hash or sort()? Any hints or advice would be welcome.
  • Comment on Sort array according to a value in each element?

Replies are listed 'Best First'.
Re: Sort array according to a value in each element?
by BrowserUk (Patriarch) on May 24, 2004 at 18:00 UTC

    A GRT will do it.

    #! perl -slw use strict; my @array = split '\n', <<'EOA'; Item1 - 2 foo, 2 bar Item2 - 0 foo, 1 bar Item3 - 1 foo, 3 bar Item4 - 1 foo, 2 bar EOA my @sorted = map{ substr $_, 5; } sort map{ sprintf '%05d%s', $_ =~ m[,\s+(\d+)], $_; } @array; print for @sorted; __END__ Item2 - 0 foo, 1 bar Item1 - 2 foo, 2 bar Item4 - 1 foo, 2 bar Item3 - 1 foo, 3 bar

    Update: Limbic~Region pointed out that you wanted descending not ascending. so substitute sort { $b cmp $a } for  sort or use reverse on the results.


    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "Think for yourself!" - Abigail
      I gotta like two working solutions with examples demonstrating the Schwartzian Transform and Guttman Rosler Transform...

      Now, having hardly any background in Computer Science-- bubble sorts are about my speed =)-- I have to woodshed for a while to figure out *why* they work-- and how to clean up a couple of warnings.

      Thanks L~R and BrowserUK!
        This Q&A might help enlighten you on how they work. They're not sort algorithms like bubblesort, they just manipulate the data so that the sort comparison looks at the right thing. The Schwartzian transform is explained there. The GRT just prefixes the data with the sorting information, then strips it off at the end. Essentially, it's a Schwartzian using a scalar as the array.

        The PerlMonk tr/// Advocate
Re: Sort array according to a value in each element?
by Limbic~Region (Chancellor) on May 24, 2004 at 17:58 UTC
    McMahon,
    I am too tired to think of a Fast, Flexible, Stable Sort solution, so here is a Schwartzian Transform. You will have to modify this depending on your real data.
    #!/usr/bin/perl use strict; use warnings; my @array = ( 'Item1 - 2 foo, 2 bar', 'Item2 - 0 foo, 1 bar', 'Item3 - 1 foo, 3 bar', 'Item4 - 1 foo, 2 bar', ); my @sorted = map { $_->[0] } # $b <=> $a for descending is better than reverse sort sort { $b->[2] <=> $a->[2] || $b->[1] <=> $a->[1] } map { [$_, / (\d+) [^\d]+(\d+)/] } @array; print "$_\n" for @sorted;
    Cheers - L~R

      Even doing the extra work that wasn't asked for (sorting by "foo" the records where "bar" is equal), it isn't particularly hard. I'll sort "bar" descending and then "foo" ascending to illustrate the techniques a bit better:

      my @list= <DATA>; @list= @list[ map { unpack "N", substr($_,-4) } sort map { # Only these two lines had to be written: my( $foo, $bar )= $list[$_] =~ / (\d+) /g; ~pack("N",$bar) . pack("N",$foo) . pack "N", $_ } 0..$#list ]; print @list; __END__ Item1 - 2 foo, 2 bar Item2 - 0 foo, 1 bar Item3 - 1 foo, 3 bar Item4 - 1 foo, 2 bar Item4 - 3 foo, 2 bar Item4 - 0 foo, 2 bar

      Yes, this screams to be put into a module... (:

      Rather than ~pack"N", you can also use ~sprintf"%09d" or even ~(length($n).$n)."\0" if you are dealing with non-negative integers, but pack"N" is nice in that it handles negative integers and rather large numbers and is fast.

      - tye        

Re: Sort array according to a value in each element?
by jZed (Prior) on May 24, 2004 at 18:52 UTC
    I'm not certain how to approach this problem: split or regex? hash or sort()?
    I'd add 'databases' to that list of possible solutions although for most circumstances the other answers in this thread are better.
    #!perl -w use strict; use DBI; my $aryref = [ ['Item1','2 foo','2 bar'] , ['Item2','0 foo','1 bar'] , ['Item3','1 foo','3 bar'] , ['Item4','1 foo','2 bar'] ]; my $dbh=DBI->connect('dbi:AnyData:'); $dbh->ad_import('tmp','ARRAY',$aryref,{col_names=>'c1,c2,c3'}); printf "%s\n",join ' ',@$_ for @{ $dbh->selectall_arrayref( 'SELECT * FROM tmp ORDER BY c3 DESC, c2 DESC' )}; __END__
      jZed, my first thought was "this would be easy if it were SQL".

      But the example data is really brain-dead; the real data is interspersed throughout a gigantic text file, and normalizing it for a database isn't really an option.

      Update: jZed's reply is right on. I didn't read closely enough. In fact, I didn't realize DBI was capable of treating an arrayref as a database and getting to it via SQL.

      I'm knee-deep in the Guttman/Rosler paper at the moment (good information there), but this is definitely worth exploring. Soon.
        the real data is interspersed throughout a gigantic text file, and normalizing it for a database isn't really an option.
        I'm sure you're right. But look at my example again. No file or external database is created and no normalizing is done at all. It just takes the same $arrayref you started with and creates a temporary-in-memory table and allows you to query the $arrayfef as if it were a database.
Re: Sort array according to a value in each element?
by Art_XIV (Hermit) on May 24, 2004 at 20:25 UTC

    Although the sort function is un-glamorous and not very fast, there are times when it is adequate for the task at hand:

    use strict; use warnings; my @array = ( 'Item1 - 2 foo, 2 bar', 'Item2 - 16 foo, 8 bar', 'Item3 - 0 foo, 1 bar', 'Item4 - 1 foo, 3 bar', 'Item5 - 4 foo, 12 bar', 'Item6 - 1 foo, 2 bar', ); @array = sort {get_bar($b) <=> get_bar($a)} @array; print "$_\n" for @array; sub get_bar { my ($line) = @_; my ($bar) = $line =~ /, (\d+) bar/; return $bar; }

    There is no doubt that one of the transform methods would be much faster, though.

    Hanlon's Razor - "Never attribute to malice that which can be adequately explained by stupidity"
Re: Sort array according to a value in each element?
by dimar (Curate) on May 25, 2004 at 03:53 UTC

    I have an array whose elements look like this:
    Yes, on the surface its an array, but conceptually, it seems like you are working with a three column table, with dashes and commas as your field separators.

    If my inference is correct, then you will probably want the ability to sort on *any* column, not just the 'bar' column. With this in mind, the following code offers a way to manage your data more like a table. You can sort it, format it, select for individual fields and do all the other stuff people always ask for, but didn't realize they wanted at the beginning.

    If my inference is wrong, you can ignore this post.

    ### INIT pragma use strict; use warnings; ### INIT vars my $data_table = []; my @aLines = (<DATA>); ### MUNGE raw data into a table @{$data_table} = map{ my @aFlds = split /\s*-\s*|\s*,\s*/,$_; if(scalar @aFlds != 3){die"ERROR: bad data"} chomp$aFlds[2]; {fld0 => $aFlds[0], fld1 => $aFlds[1], fld2 => $aFlds[2],}; }@aLines; ### SORT the table or do whatever else you want ### See Also 4.15. Sorting a List by Computable Field, Perl Cookbook ### By Tom Christiansen & Nathan Torkington; ISBN 1-56592-243-3 my @ordered = sort { $a->{fld2} cmp $b->{fld2} } @{$data_table}; map{print "$_->{fld0} ;;; $_->{fld1} ;;; $_->{fld2}\n"}@ordered; __DATA__ Item3 - 1 foo, 3 bar Item1 - 2 foo, 2 bar Item4 - 1 foo, 2 bar Item2 - 0 foo, 1 bar
Re: Sort array according to a value in each element?
by ishnid (Monk) on May 25, 2004 at 10:57 UTC
    Perhaps you could use Sort::Fields?
    #!/usr/bin/perl -w use strict; use Sort::Fields; my @array = ('Item1 - 2 foo, 2 bar', 'Item2 - 0 foo, 1 bar', 'Item3 - 1 foo, 3 bar', 'Item4 - 1 foo, 2 bar' ); my @sorted = fieldsort ['-5n', '-3n'], @array; print "$_\n" for @sorted;
Re: Sort array according to a value in each element?
by Anonymous Monk on May 24, 2004 at 20:19 UTC
    @arr = (); $arr[0][0]=2; $arr[0][1]=2; $arr[1][0]=0; $arr[1][1]=1; $arr[2][0]=1; $arr[2][1]=3; $arr[3][0]=1; $arr[3][1]=2; foreach $val (sort {$b->[1] <=> $a->[1]} @arr) { print join("|", @{$val}) . "\n"; }

    You can also implement this using hashes.

    Edit by castaway, swapped pre tags for code tags

      At first I thought this was an attempt at symbolic references, but upon further inspection it doesn't even compile. Either you're completely clueless about Perl's array syntax, or you've misused the markup tags somehow.
Re: Sort array according to a value in each element?
by deibyz (Hermit) on May 25, 2004 at 10:43 UTC
    If array data is like you show, you only have to sort it starting from the end.
    You can do it like that:
    my @items= ( 'Item3 - 1 foo, 3 bar', 'Item1 - 2 foo, 2 bar', 'Item4 - 1 foo, 2 bar', 'Item2 - 0 foo, 1 bar' ); my @sorted = reverse sort map{scalar reverse $_}@items; print scalar reverse $_ . "\n" foreach @sorted;
    I know the rest of the options are better, faster and more flexible, but I think this is simplier.
Re: Sort array according to a value in each element?
by Anonymous Monk on May 25, 2004 at 15:46 UTC
    I'm sorry, I just had to do it in one line. ;-)

    my @array = ( 'Item1 - 1 foo, 2 bar', 'Item2 - 16 foo, 8 bar', 'Item3 - 0 foo, 1 bar', 'Item4 - 1 foo, 3 bar', 'Item5 - 4 foo, 12 bar', 'Item6 - 2 foo, 2 bar', ); @sortedArray = sort {($b=~/(\d+) bar/)[0] <=> ($a=~/(\d+) bar/)[0]} @array; $"="\n"; print "@sortedArray\n"; # sort by two fields @sortedArray = sort { ($b=~/(\d+) bar/)[0] <=> ($a=~/(\d+) bar/)[0] || ($b=~/(\d+) foo/)[0] <=> ($a=~/(\d+) foo/)[0] } @array; print "\n@sortedArray\n";