4.15. Sorting a List by Computable FieldProblemYou want to sort a list by something more complex than a simple string or numeric comparison. This is common when working with objects ("sort by the employee's salary") or complex data structures ("sort by the third element in the array that this is a reference to"). It's also applicable when you want to sort by more than one key, for instance, sorting by birthday and then by name when multiple people have the same birthday. Solution
Use the customizable comparison routine in @ordered = sort { compare() } @unordered; You can speed this up by precomputing the field. @precomputed = map { [compute(),$_] } @unordered; @ordered_precomputed = sort { $a->[0] <=> $b->[0] } @precomputed; @ordered = map { $_->[1] } @ordered_precomputed; And, finally, you can combine the three steps: @ordered = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [compute(), $_] } @unordered; DiscussionThe use of a comparison routine was explained in Recipe 4.14 . As well as using built-in operators like <=>, you can construct more complex tests: @ordered = sort { $a->name cmp $b->name } @employees;
You often see foreach $employee (sort { $a->name cmp $b->name } @employees) { print $employee->name, " earns \$", $employee->salary, "\n"; } If you're going to do a lot of work with elements in a particular order, it's more efficient to sort it 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"; } }
We can put multiple comparisons in the routine and separate them with @sorted = sort { $a->name cmp $b->name || $b->age <=> $a->age } @employees;
This first considers the names of the two employees to be compared. If they're not equal, Let's look at a real-life example of sorting. Here we fetch all users, as User::pwent objects. Then we sort them by name and print the sorted list: 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 @sorted = sort { substr($a,1,1) cmp substr($b,1,1) } @names; and here we sort by length of the strings: @sorted = sort { length $a <=> length $b } @strings;
The
Fortunately, we can remove this bottleneck by running the operation once per element prior to the sort. Use
Let's apply @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
Because the input to each line is the output of the previous line (the @sorted = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ length $_, $_ ] } @strings;
The operations now appear in reverse order. When you meet a
Here's a more complicated example, which sorts by the first number that appears on each line in @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 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 fourth field (group id), then numerically by the third field (user id), and then alphabetically by the first field (user name). 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`;
This compact, See Also
The |
|