CPAN modules for checking credit card numbers (LUHN check)

other reviews

Neil Bowers

2012-08-10

This is a review and comparison of 9 CPAN modules that can be used to do a LUHN check on a number.

If you don't want to read the whole review, or even the conclusion, then I'd recommend you use Business::CreditCard.

The LUHN algorithm is a simple checksum formula used to check whether a number is valid. It is used in a number of contexts, the most well-known of which is credit-cards, where in a 16-digit card number the rightmost digit is a check digit.

I'm not going to explain the LUHN algorithm here — there are plenty of resources for that online, including the wikipedia page linked above. Here are the five modules:

Module Doc Version Author # bugs # users Last update
Acme::Tools CPAN 0.13 Kjetil Skotheim 0 0 2010-10-13
Algorithm::CheckDigits CPAN v1.2.0 Mathias Weidner 0 0 2012-06-08
Algorithm::LUHN CPAN 1.00 Tim Ayers 1 1 2002-03-26
Business::BankCard CPAN 0.01 Steven Haryanto 0 1 2012-07-06
Business::CardInfo CPAN 0.12 Simon Elliott 0 0 2011-12-15
Business::CCCheck CPAN 0.05 Michael Robinton 1 0 2012-08-06
Business::CreditCard CPAN 0.31 Ivan Kohler 1 9 2009-10-20
Regexp::Common CPAN 2011121001 Abigail 17 139 2011-12-10
Regexp::Common::_support CPAN 2010010201 Abigail 17 139 2011-12-10

The following modules also provide a LUHN check, but not for general usage, so they're not included in this review:

For each module I'll present a SYNOPSIS style code example, and briefly outline any other features. For these code examples I use one of the test card numbers. I then present results from comparing and benchmarking the modules, followed with a recommendation for which module to use when.

Acme::Tools

Acme::Tools provides a large collection (60!) of utility functions, including ccn_ok() which implements the LUHN check:

use Acme::Tools ();

if (Acme::Tools::ccn_ok('5555555555554444')) {
    print "valid card number!\n";
} else {
    print "bogus card number.\n";
}

ccn_ok() returns undef if the argument is missing digits, 0 (zero) if the number doesn't pass the LUHN check, and either returns 1 or a string identifying the issuer of the card number.

Acme::Tools exports all 60 functions by default, which is why I used:

use Acme::Tools ();

and then explicitly call Acme::Tools::ccn_ok().

Given there are other modules which provide LUHN functionality, without bringing along 59 other functions, I can't see this often being the right choice.

Algorithm::CheckDigits

Algorithm::CheckDigits is the front-end to a large collection of modules for working with check digits. The LUHN check is provided by Algorithm::CheckDigits::M10_001, but you don't explicitly use it:

use Algorithm::CheckDigits;

$card = CheckDigits('visa');

if ($card->is_valid('5555555555554444')) {
    print "valid card number!\n";
} else {
    print "bogus card number.\n";
}

It's slightly confusing that you have to pass 'visa' to the CheckDigits() function, as it's not doing anything specific to VISA.

In addition to the is_valid() method, the module supports 3 other methods:

If you're working with a multiple data types that include check digits, then I can imagine this is exactly the one for you — it looks like a solid piece of work. But if you're just after a LUHN check, then probably not.

Algorithm::LUHN

Algorithm::LUHN is a good example of why a module doesn't need to have a lot of releases, or even recent releases, to be considered mature. Version 1.00 was released in March 2002, and another release hasn't been needed since then.

use Algorithm::LUHN qw(is_valid);

if (is_valid('5555555555554444')) {
    print "valid card number!\n";
} else {
    print "bogus card number.\n";
}

The module also provides two other functions:

This module has exactly the right name, and provides exactly and only the functions you might need. When looking for LUHN modules, my first search turned up just this module.

Business::BankCard

This module doesn't have an implementation yet — it's a placeholder for the prolific Steven Haryanto, which looks like it will provide some form of LUHN check.

Business::CardInfo

Business::CardInfo provides an OO interface, and performs a LUHN check when you specify the number, for example by passing it to the constructor:

use Business::CardInfo;

eval { $card = Business::CardInfo->new(number => '5555555555554444'); };
if (!$@) {
    print "valid card number, issued by ", $card->type, "\n";
} else {
    print "bogus card number.\n";
}

Business::CardInfo uses Moose, and defines a type constraint on the number. If you pass a number that doesn't pass the LUHN check, it dies. So you need to wrap this in an eval and check whether the eval died. On the plus side, it will strip spaces from the number.

The type() method returns the name of the issuer, or 'Unknown'. The documentation mentions a country() method, but this always returns 'UK'.

The documentation is a bit thin, and a bit confusing (it doesn't explain about the type constraint, but suggests there's a validate() method). It's also much slower than the other modules here (see the comparison below).

Business::CCCheck

Business::CCCheck provides a number of functions for dealing with credit card numbers, including CC_digits(), which returns false if the passed number fails the LUHN check:

use Business::CCCheck qw(CC_digits);

$result = CC_digits('5555555555554444');
if ($result) {
    print "valid card number ($result)!\n";
} else {
    print "bogus card number.\n";
}

If the number passed is valid, then CC_digits() returns a string which identifies the card issuer.

There's currently a problem with the CC_digits() function: the documentation suggests it only returns false if the number passed doesn't satisfy the LUHN check. But in fact you'll only get a true value back if the number passes the LUHN check and is in a valid number range for a known issuer. I've raised this with the author, and may end up getting co-maint to help release an update, as he's currently swamped with work.

The module provides other functions related to handling credit cards, such as CC_clean(), which removes blanks and dashes, returning the clean number if no unexpected characters were seen, or false otherwise.

Business::CreditCard

Business::CreditCard provides three functions:

use Business::CreditCard;

if (validate('5555555555554444')) {
    print "valid card number!\n";
} else {
    print "bogus card number.\n";
}

A couple of minor niggles about this module: it exports all functions by default. I think it would be better to let the user decide which functions to import. And validate() is a bit of a generic name.

Business::CreditCard::Object provides an OO interface to this module:

use Business::CreditCard::Object;

$card = Business::CreditCard::Object->new('5555555555554444');

if ($card->is_valid) {
    print "valid card number, issued by ", $card->type, "\n";
} else {
    print "bogus card number.\n";
}

Regexp::Common

Regexp::Common provides a range of commonly used regular expressions in an extensible framework. It supports combined LUHN and issuer checking. For example, to check whether a number is a valid card number issued by MasterCard, you might write:

use Regexp::Common qw(CC);

$number = '5555555555554444';

if ($number =~ /^$RE{CC}{Mastercard}/) {
    print "valid card number!\n";
} else {
    print "bogus card number.\n";
}

It only supports checking for the following issuers: Mastercard, Amex, Diners Club, and Discover.

I guess this might be useful if you're creating an interface where the user has to select the card type and then enter the number, but it's not really a general purpose LUHN interface (though the underlying function is: see the section on Regexp::Common::_support).

Regexp::Common::_support

Regexp::Common::_support is billed as providing support functions for Regexp::Common. At the moment it only provides one (documented) function, luhn(), which returns true if the argument passes the luhn checksum test:

use Regexp::Common::_support qw(luhn);

if (luhn('5555555555554444')) {
    print "valid card number!\n";
} else {
    print "bogus card number.\n";
}

You probably shouldn't rely on this module, as the documentation contains the following

Subroutines from the module may disappear without any notice, or their meaning or interface may change without notice.

You might wonder why I've included this module? My rule is that I have to include any modules that someone can find by searching CPAN using metacpan or search.cpan.org.

Comparison

Performance

The following table shows the results of benchmarking the seven modules that can be used to do a LUHN check. I ran one million random 16-digit numbers past each module, only checking validity (where modules provide separate functions for checking validity and card issuer).

ModuleTime (s)
Business::CCCheck2.66
Regexp::Common::_support9.34
Business::CreditCard11.29
Algorithm::LUHN20.94
Algorithm::CheckDigits21.89
Acme::Tools30.42
Business::CardInfo1123.42

Business::CardInfo is much slower than the other modules; I'm guessing this is down to Moose.

The following table shows the results of looking up the issuer, for the modules that support it. For two of the modules, that's the same function as used for the above test.

For this test I also used one million random 16-digit numbers.

ModuleTime (s)
Business::CCCheck2.59
Business::CreditCard4.82
Acme::Tools28.26
Business::CardInfo2583.22

So Business::CCCheck is the fastest in both cases; a shame about the quirk with CC_digits().

Correctness

Obviously a key thing you want to know about a LUHN implementation is that it's correct. Always. It's a well-known and relatively simple algorithm, so I wasn't expecting to find errors, but I still need to check.

One approach would be for me to reimplement the algorithms and check that all modules agree with mine. But apart from the obvious flaw that I may introduce bugs in my implementation, with enough implementations I can just test them against each other.

I ran one million random 16-digit numbers past the seven modules that can do a LUHN check, to see whether they all agreed. This was how I discovered the issue with CC_digits() from Business::CCCheck (described above). Apart from that issue, all of the modules were in agreement.

Next I looked at the three modules which identify the issuer, and checked to see whether they agree on the issuer, again for one million random 16-digit numbers. They were all in agreement.

Conclusion

There is no standout module here. Business::CCCheck is the fastest, but has some quirks with the API. Business::CreditCard isn't much slower, but provides a useful set of functions, even if they're not so well named.

Overall I think Business::CreditCard is the best module. It provides a separate LUHN function and another function for identifying a card's issuer.

If you want to perform a LUHN check on identifiers that contain characters other than digits, then Algorithm::LUHN looks like the best bet. Caveat: I haven't actually tested that functionality.

I wonder if there's an opportunity for Algorithm::LUHN::XS, or Business::CreditCard::XS here? There are plenty of fast LUHN implementations in C out there, and I can't imagine it would take much for an experienced XS developer (I'm not one of those).

comments powered by Disqus