ZetCode

Perl advanced sorting

last modified November 7, 2026

The sort function takes a block that compares two elements, available as $a and $b, and returns a negative number, zero, or a positive number. The basics, string and numeric order and sorting records by one field, are covered in the Perl sort article. This article goes further: sorting by several keys, writing reusable comparison subroutines, natural order, and the classic idioms for sorting by keys that are expensive to compute: the Schwartzian transform, the Orcish maneuver, and the Guttman-Rosler transform. A benchmark at the end compares them.

The examples were run with Perl 5.44 on Linux.

Sorting by several keys

To sort by more than one key, compare by the first key and fall back to the next one when the first comparison returns zero. The low-precedence or operator expresses this directly.

multikey.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @staff = (
    { name => 'Martin',  dept => 'sales', salary => 2400 },
    { name => 'Eva',     dept => 'dev',   salary => 3100 },
    { name => 'Adam',    dept => 'sales', salary => 2400 },
    { name => 'Zuzana',  dept => 'dev',   salary => 3800 },
    { name => 'Peter',   dept => 'ops',   salary => 2900 },
    { name => 'Lenka',   dept => 'sales', salary => 2750 },
);

my @sorted = sort {
       $a->{dept}   cmp $b->{dept}
    or $b->{salary} <=> $a->{salary}
    or $a->{name}   cmp $b->{name}
} @staff;

printf "%-6s %-7s %d\n", @$_{qw(dept name salary)} for @sorted;

The staff are sorted by department in ascending order, then by salary in descending order, then by name. The comparison operators cmp and <=> return 0 for equal values, so or moves on to the next comparison only for ties. Swapping $a and $b in one comparison reverses the direction of that key alone, as for the salary. Adam and Martin earn the same, so their names decide.

$ perl multikey.pl
dev    Zuzana  3800
dev    Eva     3100
ops    Peter   2900
sales  Lenka   2750
sales  Adam    2400
sales  Martin  2400

Named comparison subroutines

A comparison that is used more than once can be put in a named subroutine and passed to sort by name.

named.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @words = qw(banana Apple cherry apple Banana date);

sub by_name {
    fc($a) cmp fc($b) or $a cmp $b
}

sub by_length_desc {
    length($b) <=> length($a) or by_name()
}

say join ' ', sort by_name @words;
say join ' ', sort by_length_desc @words;
say join ' ', reverse sort by_name @words;

my $order = \&by_name;
say join ' ', sort $order @words;

The by_name subroutine sorts case-insensitively with fc, the case-folding function, and breaks ties with a case-sensitive comparison, so Apple always comes before apple. The sort subroutine reads the package variables $a and $b rather than arguments, which is why by_length_desc can call by_name() to break ties. Putting reverse in front of sort reverses the whole result; Perl recognises this combination and does not build an intermediate list. A variable holding a code reference can also be used in place of the name.

$ perl named.pl
Apple apple Banana banana cherry date
Banana banana cherry Apple apple date
date cherry banana Banana apple Apple
Apple apple Banana banana cherry date

Sort stability

A sort is stable when elements that compare equal keep their original order. Perl's sort uses a merge sort, which is stable.

stable.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @tasks = (
    [ 'write report', 2 ],
    [ 'call Peter',   1 ],
    [ 'fix bug 17',   3 ],
    [ 'order paper',  2 ],
    [ 'backup',       1 ],
    [ 'review PR',    2 ],
);

my @by_priority = sort { $a->[1] <=> $b->[1] } @tasks;

say "$_->[1] $_->[0]" for @by_priority;

The tasks are sorted by priority only. Tasks with the same priority stay in the order in which they were listed: write report, order paper, review PR. Stability allows sorting in several passes, from the least important key to the most important, although a single multi-key comparison is usually clearer.

$ perl stable.pl
1 call Peter
1 backup
2 write report
2 order paper
2 review PR
3 fix bug 17

Natural sort

String comparison puts file10 before file2, because the character 1 is smaller than 2. A natural sort compares the embedded numbers by value, as people expect.

natural.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @files = qw(file10.txt file2.txt File1.txt file1.txt file20.txt file3.txt img12.png img9.png);

sub natural_key {
    my $s = lc shift;
    $s =~ s/(\d+)/sprintf '%09d', $1/ge;
    return $s;
}

say 'plain:   ', join ' ', sort @files;
say 'natural: ', join ' ', map  { $_->[1] }
                           sort { $a->[0] cmp $b->[0] or $a->[1] cmp $b->[1] }
                           map  { [natural_key($_), $_] } @files;

The natural_key function lowercases the string and pads every run of digits with zeros to nine places, so that a plain string comparison orders the numbers correctly: file000000002.txt sorts before file000000010.txt. The keys are computed once per element and sorted together with the original strings, a technique explained in the next section. The tie-breaker keeps File1.txt and file1.txt, which have the same key, in a defined order.

$ perl natural.pl
plain:   File1.txt file1.txt file10.txt file2.txt file20.txt file3.txt img12.png img9.png
natural: File1.txt file1.txt file2.txt file3.txt file10.txt file20.txt img9.png img12.png

The Schwartzian transform

A sort block runs many times for each element, roughly n log n comparisons in total. If the sort key must be extracted from the element, for example parsed from a string, the extraction is repeated in every comparison. The Schwartzian transform, named after Randal Schwartz, computes each key only once.

schwartzian.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @events = (
    'Concert;14.03.2027',
    'Exhibition;02.11.2026',
    'Marathon;28.09.2027',
    'Workshop;02.11.2026',
    'Festival;30.06.2027',
);

my @sorted =
    map  { $_->[0] }
    sort { $a->[1] cmp $b->[1] or $a->[0] cmp $b->[0] }
    map  {
        my ($d, $m, $y) = (split /;/)[1] =~ /(\d+)\.(\d+)\.(\d+)/;
        [ $_, "$y$m$d" ];
    } @events;

say for @sorted;

The pipeline reads from bottom to top. The first map turns each event into a pair of the original string and a key, here the date rearranged into the sortable form YYYYMMDD. The sort compares the precomputed keys, falling back to the string for events on the same day. The last map drops the keys and returns the original strings. The cost is the creation of one small array per element, which pays off as soon as the key is anything more than a hash lookup.

$ perl schwartzian.pl
Exhibition;02.11.2026
Workshop;02.11.2026
Concert;14.03.2027
Festival;30.06.2027
Marathon;28.09.2027

The Orcish maneuver

The Orcish maneuver, a pun on or-cache, keeps the ordinary sort block but caches the keys in a hash. The //= operator computes a key the first time it is needed and reuses it afterwards.

orcish.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $calls = 0;

sub score {
    my $word = shift;
    $calls++;
    return scalar(() = $word =~ /[aeiou]/gi);
}

my @words = qw(perl sort orcish maneuver schwartzian transform key cache);

$calls = 0;
my @plain = sort { score($b) <=> score($a) or $a cmp $b } @words;
say "plain:  $calls calls";

$calls = 0;
my %cache;
my @orcish = sort {
    ($cache{$b} //= score($b)) <=> ($cache{$a} //= score($a))
        or $a cmp $b
} @words;
say "orcish: $calls calls";

say "$_ ($cache{$_})" for @orcish;

The score function counts the vowels in a word and counts its own calls. The plain sort calls it 30 times for 8 words; with the cache, it is called exactly once per word. The cache is a hash keyed by the elements, so the technique works only for strings or for elements whose string form is unique; the Schwartzian transform has no such restriction. The original form of the idiom used ||=, which recomputes keys that are 0 or empty; the //= operator avoids that.

$ perl orcish.pl
plain:  30 calls
orcish: 8 calls
maneuver (4)
schwartzian (3)
cache (2)
orcish (2)
transform (2)
key (1)
perl (1)
sort (1)

The Guttman-Rosler transform

The Guttman-Rosler transform goes one step further: it packs the key and the element into a single string, so the sort needs no block at all. Perl's default string sort, without a comparison block, runs entirely in C and is the fastest sort available.

grt.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my @people = ('Kovac 1987', 'Novak 2001', 'Horvath 1987', 'Balaz 1995', 'Novak 1979');

my @sorted =
    map  { substr $_, 4 }
    sort
    map  { my ($name, $year) = split; pack('N', $year) . $_ } @people;

say for @sorted;

The year is packed with pack('N', ...) into four bytes in big-endian order, which sort correctly as a string, and is prepended to the original element. After the default sort, substr removes the first four bytes. People born in the same year are ordered by the rest of the string, so Horvath comes before Kovac. The transform is fast but demands care: every key must be encoded so that its byte order matches the desired order, which is easy for non-negative integers and fixed-width strings and harder for negative or floating-point numbers and descending order.

$ perl grt.pl
Novak 1979
Horvath 1987
Kovac 1987
Balaz 1995
Novak 2001

sort, map, and grep pipelines

Since sort, map, and grep all take and return lists, they combine into pipelines that filter, transform, and order data in a single expression.

pipeline.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $text = <<'END';
the quick brown fox jumps over the lazy dog the fox
sleeps and the dog barks at the quick fox
END

my %freq;
$freq{$_}++ for split ' ', $text;

my @top = (sort { $freq{$b} <=> $freq{$a} or $a cmp $b } keys %freq)[0 .. 4];
say "$_: $freq{$_}" for @top;

my %seen;
say join ' ', sort grep { !$seen{$_}++ } map { length } split ' ', $text;

say join ' ', map  { "$_->[0]=$_->[1]" }
              sort { $b->[1] <=> $a->[1] or $a->[0] cmp $b->[0] }
              grep { $_->[1] > 1 }
              map  { [$_, $freq{$_}] } keys %freq;

The first pipeline sorts the keys of a frequency hash by value and takes the top five with a list slice. The second extracts the distinct word lengths: map computes the lengths, grep with a %seen hash removes duplicates, and sort orders them; the default string sort is fine here because all lengths have one digit. The third builds pairs, keeps the words that occur more than once, sorts them by count and name, and formats the result. As in the Schwartzian transform, such pipelines are read from right to left.

$ perl pipeline.pl
the: 5
fox: 3
dog: 2
quick: 2
and: 1
2 3 4 5 6
the=5 fox=3 dog=2 quick=2

Benchmarking the techniques

The core Benchmark module measures how fast pieces of code run. We sort 5,000 dates in the DD.MM.YYYY format with each of the four approaches.

bench.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Benchmark qw(cmpthese);

srand 42;

my @dates = map {
    sprintf '%02d.%02d.%04d', 1 + int rand 28, 1 + int rand 12, 1990 + int rand 40
} 1 .. 5000;

sub key_of {
    my ($d, $m, $y) = $_[0] =~ /(\d+)\.(\d+)\.(\d+)/;
    return "$y$m$d";
}

my %subs = (
    naive => sub {
        my @s = sort { key_of($a) cmp key_of($b) } @dates;
        return @s;
    },
    orcish => sub {
        my %c;
        my @s = sort { ($c{$a} //= key_of($a)) cmp ($c{$b} //= key_of($b)) } @dates;
        return @s;
    },
    schwartzian => sub {
        my @s = map { $_->[1] } sort { $a->[0] cmp $b->[0] } map { [key_of($_), $_] } @dates;
        return @s;
    },
    grt => sub {
        my @s = map { substr $_, 8 } sort map { key_of($_) . $_ } @dates;
        return @s;
    },
);

my @expected = sort { key_of($a) cmp key_of($b) } @dates;

for my $name (sort keys %subs) {
    my @got = $subs{$name}->();
    die "$name sorts differently\n" unless "@got" eq "@expected";
}

cmpthese(-2, \%subs);

Before measuring, the program checks that all four subroutines produce the same order as the straightforward sort. The cmpthese function runs each subroutine for at least two CPU seconds and prints a table of rates and relative speeds. Each subroutine assigns the result to an array. This matters: cmpthese does not call the code in list context, and the behaviour of sort outside list context is undefined. Written as return sort { ... } @dates, the naive and Orcish variants did no sorting at all and reported impossibly high rates.

The naive sort extracts the key with a regular expression in every comparison and is about eight times slower than the Orcish maneuver. The Schwartzian transform is a bit faster than the cache, and the Guttman-Rosler transform, which sorts without a comparison block, is the fastest. The exact numbers depend on the machine and the data, but with an expensive key, any of the three techniques beats the naive sort by an order of magnitude.

$ perl bench.pl
              Rate       naive      orcish schwartzian         grt
naive       14.2/s          --        -88%        -89%        -92%
orcish       116/s        714%          --        -13%        -39%
schwartzian  133/s        836%         15%          --        -30%
grt          189/s       1227%         63%         42%          --

Best practices

In this article we have sorted by several keys, written named comparison subroutines, relied on sort stability, implemented a natural sort, used the Schwartzian transform, the Orcish maneuver, and the Guttman-Rosler transform, combined sort with map and grep, and compared the techniques with Benchmark.

Source

perlfunc: sort · perlfaq4: How do I sort an array by (anything)? · Benchmark

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Perl tutorials.