Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sorting an Array Reference to Hashes

After executing these lines in Perl:

my $data = `curl '$url'`;
my $pets = XMLin($data)->(pets);

I have an array reference that contains references to hashes:

$VAR1 = [
      {
        'title' => 'cat',
        'count' => '210'
      },
      {
        'title' => 'dog',
        'count' => '210'
      }
]

In Perl, how do I sort the hashes first by count and secondarily by title. Then print to STDOUT the count followed by the title on each newline.

like image 774
syker Avatar asked Jun 29 '10 01:06

syker


People also ask

Can hashes be sorted?

The data structures used for storing and sorting the data are matrices. There are a few versions of hash sort methods, primarily there are two : In-situ hash sort – In this method, both the storage and sorting of the values occur in the same data structure.

How do I sort an array of hashes in Perl?

Perl's built in sort function allows us to specify a custom sort order. Within the curly braces Perl gives us 2 variables, $a and $b, which reference 2 items to compare. In our case, these are hash references so we can access the elements of the hash and sort by any key we want.

Can you sort a hash by values?

##Hash#keys and Hash#values A simple way to sort a hash keys or values is to use the methods Hash#keys and Hash#values. Those 2 methods are simply returning an array of the keys or the values of your hash. So as soon as you get an array with only one of the type of values you want to work with, it's all good!

How do I sort hash keys?

Unfortunately, there is no direct way to sort the hash by keys. However, we can use the values method to fetch the hash values and sort them. Once we execute the above command, we should get the values of the hash sorted in array format.


1 Answers

Assuming you want counts in descending order and titles ascending:

print map join(" ", @$_{qw/ count title /}) . "\n",
      sort { $b->{count} <=> $a->{count}
                         ||
             $a->{title} cmp $b->{title} }
      @$pets;

That's compact code written in a functional style. To help understand it, let's look at equivalent code in a more familiar, imperative style.

Perl's sort operator takes an optional SUBNAME parameter that allows you to factor out your comparison and give it a name that describes what it does. When I do this, I like to begin the sub's name with by_ to make sort by_... ready more naturally.

To start, you might have written

sub by_count_then_title {
  $b->{count} <=> $a->{count}
              ||
  $a->{title} cmp $b->{title}
}

my @sorted = sort by_count_then_title @$pets;

Note that no comma follows the SUBNAME in this form!

To address another commenter's question, you could use or rather than || in by_count_then_title if you find it more readable. Both <=> and cmp have higher precedence (which you might think of as binding more tightly) than || and or, so it's strictly a matter of style.

To print the sorted array, a more familiar choice might be

foreach my $p (@sorted) {
  print "$p->{count} $p->{title}\n";
}

Perl uses $_ if you don't specify the variable that gets each value, so the following has the same meaning:

for (@sorted) {
  print "$_->{count} $_->{title}\n";
}

The for and foreach keywords are synonyms, but I find that the uses above, i.e., foreach if I'm going to name a variable or for otherwise, read most naturally.

Using map, a close cousin of foreach, instead isn't much different:

map print("$_->{count} $_->{title}\n"), @sorted;

You could also promote print through the map:

print map "$_->{count} $_->{title}\n",
      @sorted;

Finally, to avoid repetition of $_->{...}, the hash slice @$_{"count", "title"} gives us the values associated with count and title in the loop's current record. Having the values, we need to join them with a single space and append a newline to the result, so

print map join(" ", @$_{qw/ count title /}) . "\n",
      @sorted;

Remember that qw// is shorthand for writing a list of strings. As this example shows, read a map expression back-to-front (or bottom-to-top the way I indented it): first sort the records, then format them, then print them.

You could eliminate the temporary @sorted but call the named comparison:

print map join(" ", @$_{qw/ count title /}) . "\n",
      sort by_count_then_title
      @$pets;

If the application of join is just too verbose for your taste, then

print map "@$_{qw/ count title /}\n",
      sort by_count_then_title
      @$pets;
like image 134
Greg Bacon Avatar answered Oct 28 '22 01:10

Greg Bacon