If you're going to do a lot of work with elements in a particular
order, it's more efficient to sort once and work from that:
@sorted_employees = sort { $a->name cmp $b->name } @employees;
foreach $employee (@sorted_employees) {
print $employee->name, " earns \$", $employee->salary, "\n";
}
# load %bonus
foreach $employee (@sorted_employees) {
if ( $bonus{ $employee->ssn } ) {
print $employee->name, " got a bonus!\n";
}
}
use User::pwent qw(getpwent);
@users = ( );
# fetch all users
while (defined($user = getpwent)) {
push(@users, $user);
}
@users = sort { $a->name cmp $b->name } @users;
foreach $user (@users) {
print $user->name, "\n";
}
We can have more than simple comparisons, or combinations of simple
comparisons. This code sorts a list of names by comparing the
second letters of the names. It gets the second
letters by using substr:
@sorted = sort { substr($a,1,1) cmp substr($b,1,1) } @names;
and here we sort by string length:
@sorted = sort { length $a <=> length $b } @strings;
The sort function calls the code block each time
it needs to compare two elements, so the number of comparisons grows
dramatically with the number of elements we're sorting. Sorting 10
elements requires (on average) 46 comparisons, but sorting 1,000
elements requires 14,000 comparisons. A time-consuming operation like
a split or a subroutine call for each comparison
can easily make your program crawl.
Let's apply
map-sort-map
to the sorting by string length example:
@temp = map { [ length $_, $_ ] } @strings;
@temp = sort { $a->[0] <=> $b->[0] } @temp;
@sorted = map { $_->[1] } @temp;
The first line creates a temporary array of strings and their
lengths, using map. The second line sorts the
temporary array by comparing the precomputed lengths. The third line
turns the sorted temporary array of strings and lengths back into a
sorted array of strings. This way, we calculate the length of each
string only once.
Because the input to each line is the output of the previous line
(the @temp array we make in line 1 is fed to
sort in line 2, and that output is fed to
map in line 3), we can combine it into one
statement and eliminate the temporary array:
@sorted = map { $_->[1] }
sort { $a->[0] <=> $b->[0] }
map { [ length $_, $_ ] }
@strings;
The operations now appear in reverse order. When you meet a
map-sort-map,
you should read it from the bottom up to determine the function:
- @strings
-
The last part is the data to be sorted. Here it's just an array, but
later we'll see that this can be a subroutine or even backticks.
Anything that returns a list is fair game.
- map
-
The map closer to the bottom builds the temporary
list of anonymous arrays. This list contains the precomputed fields
(length $_) and also records
the original element ($_) by storing both in an
anonymous array. Look at this map line to find out
how the fields are computed.
- sort
-
The sort line sorts the list of anonymous arrays
by comparing the precomputed fields. It won't tell you much, other
than whether the list is sorted in ascending or descending order.
- map
-
The map at the top of the statement turns the
sorted list of anonymous arrays back into a list of the sorted
original elements. It will generally be the same for every
map-sort-map.
Here's a more complicated example, which sorts by the first number
that appears on each line in @fields:
@temp = map { [ /(\d+)/, $_ ] } @fields;
@sorted_temp = sort { $a->[0] <=> $b->[0] } @temp;
@sorted_fields = map { $_->[1] } @sorted_temp;
The regular expression mumbo jumbo in the first line extracts the
first number from the line being processed by map.
We use the regular expression /(\d+)/ in a list
context to extract the number.
We can remove the temporary arrays in that code, giving us:
@sorted_fields = map { $_->[1] }
sort { $a->[0] <=> $b->[0] }
map { [ /(\d+)/, $_ ] }
@fields;
This final example compactly sorts colon-separated data, as from
Unix's passwd file. It sorts the file
numerically by the fourth field (group id), then numerically by the
third field (user id), and then alphabetically by the first field
(username).
print map { $_->[0] } # whole line
sort {
$a->[1] <=> $b->[1] # gid
||
$a->[2] <=> $b->[2] # uid
||
$a->[3] cmp $b->[3] # login
}
map { [ $_, (split /:/)[3,2,0] ] }
`cat /etc/passwd`;