6.6 Sort & subroutines

The function sort use default the ASCIIbetical order. Thanks to two operators you can influence the sort order. In addition, combining sort with built-in or custom subroutines with these operators does the same. I use the following way of coding:


@list = qw();separate definition of @list, which gives a bit longer code

sort ( @list );
sort ( { ... } @list ); with block {...} before the list/array; the block can also contain subroutines
sort ( custom_function @list );
Here some examples.
Example 1

# ASCIIbetical order or ASCII#Character order
@list = qw( 0 1 2 3 5 8 13 21 34 55 89 144 233 377 610 );
@list_sorted = sort( @list ); 
print("@list_sorted\n");# Output: 0 1 13 144 2 21 233 3 34 377 5 55 610 8 89
Example 2

# this is probably what you want, thanks to the operator <=> and the global variables $a and $b
@list = qw/0 1 2 3 5 8 13 21 34 55 89 144 233 377 610/; 
@list_sorted = sort( { $a <=> $b } @list );
print("@list_sorted\n");# Output: 0 1 2 3 5 8 13 21 34 55 89 144 233 377 610
Example 3

# descending order: $b <=> $a
@list = qw/0 1 2 3 5 8 13 21 34 55 89 144 233 377 610/; 
@list_sorted = sort( { $b <=> $a } @list );
print("@list_sorted\n");# Output: 610 377 233 144 89 55 34 21 13 8 5 3 2 1 0
Example 4

# ASCIIbetical order or ASCII#Character order
@list = qw(c Z b Y a X);
@list_sorted = sort( @list );
print("@list_sorted\n");# Output: X Y Z a b c
Example 5

# probably what you want, thanks to the operator cmp and the global variables $a and $b
# the lc function (lowercase) is necessary to compare all characters
@list = qw(c Z b Y a X);
@list_sorted = sort( { lc($a) cmp lc($b) } @list );
print("@list_sorted\n");# Output: a b c X Y Z
Example 6

# here a combination with function substr: the last digit of a number determines the sort order
@list = qw(8 2 24 16 2345 6 1 64 133); 
@list_sorted = sort( { substr($a,-1,1) <=> substr($b,-1,1) } @list );
print("@list_sorted\n");# Output: 1 2 133 24 64 2345 16 6 8
Example 7

# here a combination with function length: the length determines the sort order
# in case the strings have the equal length, then the ASCIIbetical order determines the sort order
@list = qw(like Cappuccino YOU do Zen); 
@list_sorted = sort( { length($a) <=> length($b) } @list );
print("@list_sorted\n");# Output: do YOU Zen like Cappuccino
Example 8

# here the previous code again, but now in a custom subroutine mc_compare
# notice that this subroutine handles the global variables $a and $b and does not have any arguments
@list = qw(like Cappuccino YOU do Zen); 
sub mc_compare { length($a) <=> length($b) }
@list_sorted = sort( mc_compare @list );
print("@list_sorted\n");# Output: do YOU Zen like Cappuccino
Example 9

# here an array of hashes, sorted on the numerical value 'age', hence <=>
@name_age = ( {name => Daniel, age => 39}, 
              {name => Sebastian, age => 41}, 
              {name => Florence, age => 37} 
            );
@name_age_sorted = sort( { $a->{age} <=> $b->{age} } @name_age ); 
foreach (@name_age_sorted) {
 print($_->{name}, " ",  $_->{age} , "\n");
}

Florence 37
Daniel 39
Sebastian 41

Example 10

# here an array of hashes, sorted on the string value 'name', hence cmp
@name_age = ( {name => Daniel, age => 39}, 
              {name => Sebastian, age => 41}, 
              {name => Florence, age => 37} 
            );
@name_age_sorted = sort { $a->{name} cmp $b->{name} } @name_age; 
foreach (@name_age_sorted) {
 print($_->{name}, " ",  $_->{age}, "\n");
}

Daniel 39
Florence 37
Sebastian 41

Notice that the previous array of hashes examples can be writen easier with the __DATA__ construction!
Example 11

# here a combination with the split subroutine
use Text::Trim;

# Read and store data from __DATA__ section into an array
@data = <DATA>;
chomp(@data);

# Sort the array based on the second numerical column, hence <=>
@sorted_data = sort {
    (split /\s+/, $a)[1] <=> (split /\s+/, $b)[1]
} @data;

# print the sorted data
# the foreach loop and the trim subroutine for a better output
foreach (trim(@sorted_data)) {
 print($_ . "\n") if ($_);
}

__DATA__
apple 5
banana 2
orange 8
grape 1


Output: 
grape 1
banana 2
apple 5
orange 8

Example 12

# here a combination with split in a own subroutine
# if two numerial values are equal, one want to order by the names; that is what the logical operator || does 
use Text::Trim;

@data = <DATA>;
chomp(@data);

# create your own function
sub mc_compare {

	@a_value = (split /;/, $a);
	@b_value = (split /;/, $b);	
	
	$a_value[1] <=> $b_value[1] || $a_value[0] cmp $b_value[0];
}

# notice that this subroutine handles the global variables $a and $b and does not have any arguments here
@sorted_data = sort(mc_compare @data);

foreach ( trim(@sorted_data) ) {
 print($_ . "\n") if ($_);
}

__DATA__
apple;5
grape;2
orange;8
banana;2


Output:
banana;2  
grape;2
apple;5
orange;8
Now banana;2 before grape;2 thanks to the logical operator || in the subroutine mc_compare
Exit: the Schwarzian Transform (aka decorate, sort, undecorate)
The Schwartzian Transform is used for efficiently sorting a list of items by a computed property, without repeatedly recomputing that property. This is often used when the computation of the sorting key is expensive. This is the case in example 12: always different pairs of $a and $b need to be evaluated (of course, that is only observable in case of many data lines). The last example can be written with the Schwartzian Transform. But first an easy example as an introduction:
Example 13

# list of strings
@strings = ("apple", "banana", "orange", "grape", "kiwi");

# Schwartzian Transform
@sorted_strings =

  map { $_->[0] }# undecorate
  sort { $a->[1] <=> $b->[1] }# sort
  map { [$_, length($_)] } @strings;# decorate

# print the sorted list
print join(", ", @sorted_strings), "\n"; # output: kiwi, apple, grape, banana, orange
Let's break the code

  1. The map { [$_, length($_)] } @strings part ('decorate') creates an array of arrays, where each inner array contains the original string and its length.
  2. The sort { $a->[1] <=> $b->[1] } part ('sort') sorts these arrays based on the length (the second element of each inner array).
  3. The map { $_->[0] } part ('undecorate') extracts the original strings from the sorted array of arrays.
Example 14
Now the rewritten example 12

use Text::Trim;

@data = <DATA>;
chomp(@data);

sub mc_compare {

	$a->[1] <=> $b->[1] || $a->[0] cmp $b->[0];
}

# Schwartzian Transform
@sorted_data = 
               map { $_->[0] } 
               sort mc_compare 
               map { /;(\d+)/; [ $_, $1 ] } @data; # alternative: map { [ $_, substr($_,-1) ] } @data;
               
foreach (trim(@sorted_data)) {
 print($_ . "\n") if ($_);
}

__DATA__
apple;5
grape;2
orange;8
banana;2
Study the next example where the array of arrays ('decorate' part) is extended and notice that the sort part has been changed.
Example 15

use Text::Trim;

@data = <DATA>;
chomp(@data);

# Schwartzian Transform with multiple values using substr
@sorted_strings =
    map { $_->[0] }
    sort { $a->[2] <=> $b->[2] or $a->[1] <=> $b->[1] }
    map { [$_, substr($_, 0, 5), substr($_, -3)] } @data;

# print the sorted list
foreach (trim(@sorted_strings)) {
 print($_ . " ") if ($_); # output: apple123 grape123 kiwi345 banana456 orange789
}

__DATA__
apple123
banana456
orange789
grape123
kiwi345