Perl 5 to Perl 6 Rewrite

My coworker Wes asked me if there could be a nice refactor for the following function which checks CAS Numbers to ensure their validity. After struggling for 30 minutes I gave up trying to make it a little bit nicer with reduce.

sub cas_old {
  my $cas = shift;
  if ($cas =~ /\d{1,8}-\d\d-\d/) {
    my @ary = grep { $_ ne '-' } split(//, $cas);
    my $check = pop @ary;
    my $count = @ary;
    my $sum;
    for (@ary){
      $sum += $_ * $count--;
    return $sum % 10 == $check;

Let’s take a look at this and figure it out. The crunchy bit is the for loop, so I’ll go through that. Basically we are summing each item times a weight that is inversely proportional to it’s location in the list. Or to be more explicit, let’s do an example on the board (7732-18-5.) 5 is the check digit.

$_ $count $_ * $count $sum
7 6 42 42
7 5 35 77
3 4 12 89
2 3 6 95
1 2 2 97
8 1 8 105

So basically we are making a special summation. The thing that’s unusual is that we have a decrementing counter along with it. If I had the control structure which I am about to show you in my mind already the solution might have jumped out sooner.

So I asked about it in #perl6 and it turns out there is a very nice Perl 6 version. It takes advantage of the mystical hyperoperator (>>infix op<<); that is, it takes two lists and performs an operation on each element together. Think SIMD. It also uses reduce ([infix op]) which I have mentioned before. Check it out!

sub cas(Str $cas) {
    if $cas ~~ /(\d ** 1..8)\-(\d\d)\-(\d)/ {
        my @digits = $0~$1.split '';
        my $check = $2;
        return ([+] @digits.reverse
           (1..@digits)) % 10 == $check;
    return Bool::False;

This does the same thing as above. Or to put it in English, we take our digits, reverse them, and then multiply each digit (hyperoperator, >>*<<) by the respective integer in the other list, that is, 1 to the size of the list. We then sum (reduce, [+]) that new list, and get the modulus 10 of it.

Very elegant, no?

Update: Turns out there is also a very elegant version in p5, according to mst. Check it out!

sub cas_old {
  use List::Util 'sum';
  my $cas = shift;
  if ($cas =~ /(\d{1,8})-(\d\d)-(\d)/) {
    my @digits = split(//, $1.$2);
    my $count = @digits;
    my $check = $3;
    return (sum map $_ * $digits[-$_],
       1 .. $count) % 10 == $check;

It’s very similar to the p6 version, just using fewer generalized operators, so you should be able to follow it fairly well.

Posted Fri, May 15, 2009