The ordering operators

Perl has two operators, cmp and <=>, which are basically never seen outside of sort blocks.

That doesn’t mean you can’t use them elsewhere, though. Certainly sort and these operators were designed to work seamlessly together but there isn’t anything sort-specific about the operators per se, and in some contexts they can be the most appropriate solution.

An example would be some code that used to go (almost exactly) like this:

my ( $maj, $min, $rel ) = split /[.]/, $version;
my $version_ok = $maj > 3 || (
    $maj == 3 && ($min > 6 || (
        $min == 6 && $rel >= 8
    ))
);

Basically, this is a check that the version number of something with a three-component dotted version is at least 3.6.8. As expressed in the code, for both the major and minor version there are two cases where they might be accepted: if the component is above a given threshold then the whole version is acceptable with no further questions, but if equal to that threshold, the version might still be acceptable depending on the value(s) of the lesser component(s). At the same time, if the value is below that threshold, the version is unacceptable, no further questions necessary. So there are three cases, two of which can succeed, and two of which require no further checks.

This is exactly what the <=> operator offers: if you ask $maj <=> 3 you will get 1 if the major version is above 3 (immediate success), -1 if it is below 3 (immediate failure), and 0 if it is equal to 3 (success to be determined from the lesser components). Because 0 is false, you can chain such comparisons with logical-or, until one of them gives an immediate success/fail – as of course we are used to doing in sort blocks.

And so this is what the code looks like now:

my ( $maj, $min, $rel ) = split /[.]/, $version;
my $version_ok = ( $maj <=> 3 || $min <=> 6 || $rel <=> 8 ) != -1;

The expression in parentheses has each component of the actual version on the left and the respective component of version 3.6.8 on the right, and will stop at the first comparison that gives a definite answer. The final != -1 then checks that the answer was not “the right side is greater” at the final comparison – i.e. “the given version is not below version 3.6.8”.

7 Comments

I've been Perling since 1997 and I don't think I ever thought about using the comp operators outside of sort() before today. TIL.

Padding numbers and doing comparison would be simpler and straightforward but less clever.

Neat. I do like a nice logical op chain.

I also use it sometimes when there are three different actions based on a comparison, turning something like

for my $x (1, 2, 3) {
    for my $y (1, 2, 3) {
        print "$x $y ";
        if ($x == $y) {
            print 'same';
        } elsif ($x < $y) {
            print 'less';
        } else {
            print 'greater';
        }
        print "\n";
    }
}

into

for my $x (1, 2, 3) {
    for my $y (1, 2, 3) {
        print "$x $y ", qw( same greater less )[$x <=> $y], "\n";
    }
}

I would certainly prefer the original for readability in inline code, but I would be OK with the chained comparison operators in a function. Here are couple other version-comparison functions:

sub vcmp1 {
  my ($v1, $v2) = @_;
  my @v1 = split /[.]/, $v1;
  my @v2 = split /[.]/, $v2;
  my $fmt;
  $fmt .= "%0@{[max length $v1[$_], length $v2[$_]]}d" for 0..2;
  return sprintf($fmt, @v1) cmp sprintf($fmt, @v2);
}

sub vcmp2 {
  my ($v1, $v2) = @_;
  my @v1 = split /[.]/, $v1;
  my @v2 = split /[.]/, $v2;
  do { return $v1[$_] <=> $v2[$_] if $v1[$_] <=> $v2[$_] } for 0..2;
  return 0;
}

Leave a comment

About Aristotle

user-pic Waxing philosophical