http://qs321.pair.com?node_id=147528

I was just looking at the about.com Perl site, and it has this article about finding if a number is a valid Luhn number, which, apparently, tells you it's a valid credit card number.

  1. double the value of every other digit, counting backward from the second-last
  2. add all the digits of the resulting numbers together (not the numbers, but the digits of the numbers, so if you've got 16, you add it as one plus six)
  3. add the leftover numbers together
  4. add them to the sum of the other lot of numbers

If what you've got is divisible by ten, it's a valid credit card number.

Their solution is at http://perl.about.com/library/weekly/aa080600h.htm, and of course it's quite long, because it's a teaching exercise.

I got thinking about it and how to do it more compactly, in the spirit of "Regular Expressions -- is there anything they can't do?" and tried to make it as short as I could.

Here's an attempt, but I'd love to see you make it shorter:

$n = '4564123800603607'; # this is not my credit card number ($o = reverse($n)) =~ s/.(.)/($1*2)/ge; # every other number in it, times 2 # (reversed because then it will work # for numbers of different lengths, as # some cards are 13, not 16). ($e = reverse($n)) =~ s/(.)./$1/ge; # the other numbers, not times 2 $o =~ s/(.)/$x+=$1/ge; $e =~ s/(.)/$y+=$1/ge; # add the digits printf("%sValid.",(($x+$y)%10 == 0?'':'Not ')); # print the result

Replies are listed 'Best First'.
Re: Luhn Number Golf
by japhy (Canon) on Feb 26, 2002 at 13:57 UTC
    This code assumes the number comes as an argument to our function. It comes in at 68 (updated to allow for odd-digited credit cards).
    sub luhn { #234567890123456789012345678901234567890123456789012345678901234567 $a=0;$_=reverse pop;s/(.)(.?)/$a+=$1+($2&&(($2*2-1)%9)+1)/ge;1>$a%10 }

    _____________________________________________________
    Jeff[japhy]Pinyan: Perl, regex, and perl hacker, who'd like a (from-home) job
    s++=END;++y(;-P)}y js++=;shajsj<++y(p-q)}?print:??;

Re: Luhn Number Golf
by jmcnamara (Monsignor) on Feb 26, 2002 at 15:27 UTC

    82 chars (updated). Returns 1 for valid, undef otherwise. Your solution seems to be out by one for input that has an odd number of digits.
    sub luhn { $r=$t=0; split//,pop; $r.=$_[$_]*(1+($_+$#_)%2)for+0..@_; $r=~s/(.)/$t+=$1/eg; !chop$t }
    Update: Here is another non-reverse variation at 72 chars. Still a mile behind chipmunk however.
    sub luhn2 { $_=$i=0; split//,pop; $_.=(1+$i++%2)*pop while@_; s/(.)/+$1/g; $_=eval; !chop }

    --
    John.

Re: Luhn Number Golf
by demerphq (Chancellor) on Feb 26, 2002 at 15:24 UTC
    Well this was fun.

    Since you werent too specific about the rules I made mine robust. It has no side effects. It runs under strictures and warnings with no complaints and it doesn't care about non digits in the passed string (ie spaces or hyphens between the groups are ignored).

    It comes in at 101 characters.

    sub demerphq_luhn{ my($r,$x,$d)="".reverse$_[0];$r=~s/(\d)\D*(\d)?/$x+=$1;$2&&($d.=$2*2)/ +ge;$d=~s/(\d)/$x+=$1/ge;$x%10<1 }

    Yves / DeMerphq
    --
    When to use Prototypes?

Re: Luhn Number Golf
by chipmunk (Parson) on Feb 26, 2002 at 19:57 UTC
    This is 60 characters. I've verified it by comparing the results to the referenced code from http://perl.about.com/library/weekly/aa080600h.htm.
    sub luhn { # 1 2 3 4 5 6 #23456789012345678901234567890123456789012345678901234567890 my$s;$_=reverse@_;s/(.)(.)/$1.$2*2/ge;$s+=$_ for/./g;!chop$s }
      ++. You can eliminate reverse and shorten it to 55.
      sub luhn { # 1 2 3 4 5 #234567890123456789012345678901234567890123456789012345 my$s;$_=pop;~s/(.)(.)/$2.$1*2/ge;$s+=$_ for/./g;!chop$s }
      and, if you don't care for strict-ness, shave another 5 chars, by eliminating my$s;

      /prakash

        Unfortunately, you can't simply eliminate reverse. Cody Pendant explained why in the comments in his solution; if the number has an odd number of digits, you'll end doubling the wrong digits. For example, your solution would return true for 548979844, which is not a Luhn number.

        You shouldn't eliminate my$s; either. That's not there for strict-ness, but to allow the subroutine to be called more than once. Without it, $s would keep its values between calls, throwing off all the subsequent answers.

      52 chars:
      sub luhn { $_=reverse@_;s/(.)(.)/$1.$2*2/ge;s/\B/+/g;eval=~/0$/ }


        p
Re: Luhn Number Golf
by PrakashK (Pilgrim) on Feb 26, 2002 at 17:45 UTC
    Here's a sub with 62 characters. Expects the number as an argument and returns 1 or 0.

    sub luhn { #2345678901234567890123456789012345678901234567890123456789012 for(pop=~/(.)(.)/g){($a+=$1)+=$_ for map{split//}2*$2}!($a%10) }
    /prakash
122 Char Sub - Re: Luhn Number Golf
by metadoktor (Hermit) on Feb 26, 2002 at 13:29 UTC
    Removed some whitespace, added default variable and placed the code in a subroutine.
    #!/usr/bin/perl -w if (luhn()==0) { print "valid\n"; } else { print "not valid\n"; } sub luhn { 1 2 3 4 5 6 +7 8 9 10 11 12 0123456789012345678901234567890123456789012345678901234567890123456789 +01234567890123456789012345678901234567890123456789012 $n='4564123800603607';$o=$_=reverse$n;$o=~s/.(.)/$1*2/ge;s/(.)./$1/ge; +$o=~s/(.)/$x+=$1/ge;s/(.)/$y+=$1/ge;return($x+$y)%10; }

    metadoktor

    "The doktor is in."