Custom search: Custom sort

  1. New fields
  2. Custom search
  3. Backquoting special characters

If you don’t get dereferencing, go back and take another look at it, because we’re going to do a different kind of dereferencing here. Arrays can have multiple dimensions. So far, all of the arrays we’ve used have had a single dimension: our simple arrays have been a list of single items, and our associative arrays have been simple sets of keys and values. But arrays can have rows and columns much like a spreadsheet; they can even mix simple arrays in one column with associative arrays in others.

Adding a sort switch is pretty easy. We’ll want to be able to sort on any valid field, so we can re-use @validFields for this purpose.

} elsif ($switch eq "sort") {
$sortby = shift;
if (!grep(/^$sortby$/, @validFields)) {
print "\nI can only sort by $validFields.\n\n";
help();
exit;
}
} else {

Because we want to sort the results, we can’t just print out each line as soon as we reach it. We’ll need to save it for later. Replace the print for raw format with:

$text = $_;

Replace the print for html format with:

$text = "<tr><td>$song</td><td>$album</td><td>$artist</td></tr>\n";

Replace the print for simple format with:

$text = "$song ($album, by $artist)\n";

As a test, you might run the script now; you should see nothing, because we aren’t printing anything anymore.

After the section the sets the text (and the used to print the text) add:

#store or print the display text and the sort text
if ($sortby) {
$matches[$#matches+1]{'text'} = $text;
$matches[$#matches]{'sort'} = $$sortby;
} else {
print $text;
}

So, if $sortby exists and has something in it we store the $text we just set for later sorting. If we aren’t going to be sorting it, we just print it out now. The interesting part is how we remember this text. We have to remember not only the text we want to display, but also the text we want to sort on.

$matches[$#matches+1]{'text'} = $text;
$matches[$#matches]{'sort'} = $$sortby;

The first line remembers the text. We’re setting up an @matches array that will contain this information. This will be a simple array: it will simply be a list of items that goes from 0 on up to however many we find. For a simple array, recall that $#arrayname is the current top item. This means that $#arrayname+1 is the next empty item. That’s what we’re setting right here: the next empty item in @matches is getting a new item.

That new item is, rather than a scalar variable, an associative array. The first association in that array will be between the word “text” and the display text we want to remember.

The second line remembers what we’re sorting by. Here, we only use $#matches, because the topmost item is the one we want: the previous line added a new item to @matches, and we want to add a new association to the associative array we put there.

We associate the word “sort” with the value of the field we want to sort by. This, again, is a symbolic dereference. If $sortby contains “genre”, $$sortby will be $genre.

So if the first matching song is “Sleeping Bag” by “ZZ Top” from the album “Afterburner”, and we are sorting by song, the first item in @matches ($matches[0]) will be an associative array associating “text” with “Sleeping Bag (Afterburner, by ZZ Top)” and associating “sort” with “Sleeping Bag”.

We’re almost ready to try it. Just to make sure we’re on the same page, here is the entire “if ($matched)” section:

if ($matched) {
$matches++;
if ($format eq "raw") {
$text = $_;
} elsif ($format eq "html") {
$text = "<tr><td>$song</td><td>$album</td><td>$artist</td></tr>\n";
} elsif ($format eq "summary") {
$artists{$artist}++;
} else {
$text = "$song ($album, by $artist)\n";
}

#store or print the display text and the sort text
if ($sortby) {
$matches[$#matches+1]{'text'} = $text;
$matches[$#matches]{'sort'} = $$sortby;
} else {
print $text;
}
}

All that’s left is to sort and display the matches. But in order to sort the matches, we need a subroutine that we can hand to sort, that knows how to sort matches.

sub byCustom {
return $$a{'sort'} cmp $$b{'sort'};
}

When sort calls a subroutine, it does not pass arrays. If the item it is passing is an array, it passes a hard reference to an array. Just as with symbolic references, we need to dereference a hard reference in order to get at its value.

Here, $a and $b are going to be hard references to an associative array, because each item in @matches is an associative array, and we want to sort @matches. Since we want to sort on the text that is associated with the word ‘sort’ in the associative array, we dereference each array and then ask for the value associated with “sort” in that array.

Remember that “cmp” is the text equivalent of “<=>”.

We can also dereference such a reference and get an associative array back by using %$a or %$b. For example, “%leftside = %$a” would make %leftside be a normal associative array that we could get keys from or pull values from as normal.

So, now we have our sort routine. We can finally sort and display our matches.

We already have a place outside of the while that is displaying stored information: there’s an “if (%artists)” block. At the end of that block, and an “elsif” block:

} elsif (@matches) {
@matches = sort byCustom @matches;
foreach $match (@matches) {
print $$match{'text'};
}
}

We sort @matches, assign the sorted array back to @matches, and then go through @matches for each item it contains. Arrays in Perl do not really contain arrays. They contain hard references to arrays. So we have to dereference that hard reference in order to get the value associated with “text” that we want to display.

./show --song shoes songs.txt

./show --song shoes --sort song songs.txt

./show --song shoes --sort artist songs.txt

The first one should show about ten songs that mention “shoes” in the title. The second one should show the same songs, but sorted by song title. The third shows the same songs sorted by artist name.

Try this:

./show --artist "Elton John" --sort song songs.txt

Looks like we’re not quite done yet. This is sorted by song, but it’s putting the upper-case songs first, and the lower-case songs second. First it sorts through A to Z and then a to z.

This is easy enough to fix. We need to make the comparison in the byCustom subroutine not care about upper or lower case. The easiest way to do this is to make the text be all lower case (or all upper case). There is a function for this: lc(“text”) will convert that text to all lower case. Change the byCustom subroutine to:

sub byCustom {
if ($sensitive) {
return $$a{'sort'} cmp $$b{'sort'};
} else {
return lc($$a{'sort'}) cmp lc($$b{'sort'});
}
}

Now, by default sorts will not care about case, but if we specify –case sorts will be case sensitive:

./show --artist "Elton John" --sort song songs.txt

./show --artist "Elton John" --sort song –case songs.txt

And add this to the help:

print "\t--sort <$validFields>: sort by specified field\n";

  1. New fields
  2. Custom search
  3. Backquoting special characters