reptile7's JavaScript blog
Friday, July 26, 2013
The Cards Are Alright
Blog Entry #296

As promised, in today's post we will revamp the checkCardNumber( ) and checkExpireDate( ) functions in the Scottish Gifts order.htm source; specifically, we will use regular expressions to carry out the functions' vetting operations.

checkCardNumber( )

The Issuer identification number (IIN) section of Wikipedia's "Payment card number" entry features a helpful table that lists the beginning digit sequences and number of digits for a variety of credit card types.
• At his site, Jan Goyvaerts has posted a "Finding or Verifying Credit Card Numbers" tutorial that provides regexp patterns for validating a variety of credit card types.

As detailed in the previous post, the checkCardNumber( ) function is divided into two parts vis-à-vis the Credit Card Info table:
(1) a for loop checks if the Credit Card # field has an all-digit value; and
(2) an if...else if cascade - the Credit Card Sanity Check - checks (a) if the Card Type selection and the Credit Card # value are out of sync and (b) if the Credit Card # value contains at least 12 digits.
As discussed below, we can integrate and refine these parts via the use of regular expressions.

There are four specific credit card types whose numbers we want to validate: Visa, MasterCard (M/C), Discover, and American Express (AMEX).

N.B. The validations of the Visa, MasterCard and American Express, and Discover subsections are based on Wikipedia's mid-2013 IIN information. As of February 2017:
(v) Visa card numbers can have 13, 16, or 19 digits;
(mc) MasterCard card numbers can also begin with 2221-2720; and
(d) Discover card numbers can have 16 or 19 digits.


We noted last time that Visa card numbers always begin with a 4. The aforecited Wikipedia IIN table specifies that Visa card numbers comprise 13 or 16 digits; however, in the text following the table, Wikipedia states, All 13-digit [Visa] account numbers have since been migrated to 16-digit account numbers, which is consistent with what this page and this document (sorry, this resource is no longer available) reports, so let's run with it and assume that 13-digit Visa card numbers are a thing of the past. A ^4\d{15}$ regexp pattern matches a 16-digit string beginning with a 4; here's how we might use the pattern in the checkCardNumber( ) function:

if (document.order.card_type[0].checked) { // If the Visa: radio button is checked
    if (/^4\d{15}$/.test(document.order.card_no.value)) { window.alert("Thanks, Visa user."); return false; }
    else { window.alert("Please enter a valid VISA number."); return true; } }

The test( ) method of the RegExp object is detailed here.

MasterCard and American Express

From the IIN table:
MasterCard card numbers begin with 51-55 and contain 16 digits.
American Express card numbers begin with 34 or 37 and contain 15 digits.

MasterCard card numbers are matched by a ^5[1-5]\d{14}$ regexp pattern; American Express card numbers are matched by a ^3[47]\d{13}$ regexp pattern.

if (document.order.card_type[1].checked) { // If the M/C: radio button is checked
    if (/^5[1-5]\d{14}$/.test(document.order.card_no.value)) { window.alert("Thanks, MasterCard user."); return false; }
    else { window.alert("Please enter a valid MasterCard number."); return true; } }
if (document.order.card_type[3].checked) { // If the AMEX: radio button is checked
    if (/^3[47]\d{13}$/.test(document.order.card_no.value)) { window.alert("Thanks, American Express user."); return false; }
    else { window.alert("Please enter a valid American Express number."); return true; } }


According to the IIN table, Discover card numbers begin with 6011, 622126-622925, 644-649, or 65, and contain 16 digits; it is not so easy to craft a regexp pattern that encompasses all of these criteria but it can be done:

var regexpDiscover = /^6(?:011\d{2}|5\d{4}|4[4-9]\d{3}|22(?:12[6-9]|1[3-9]\d|[2-8]\d{2}|9[01]\d|92[0-5]))\d{10}$/;
if (document.order.card_type[2].checked) { // If the Discover: radio button is checked
    if (regexpDiscover.test(document.order.card_no.value)) { window.alert("Thanks, Discover user."); return false; }
    else { window.alert("Please enter a valid Discover number."); return true; } }

• To speed things up, boolean alternatives in the regexpDiscover pattern are grouped with (?:x) non-capturing parentheses.

• The application of regular expressions to numeric ranges is covered here at the site. In order to match it with a regexp pattern, the 126-925 range must be broken up into five subranges:
(1) 126-129, which is matched by 12[6-9];
(2) 130-199, which is matched by 1[3-9]\d;
(3) 200-899, which is matched by [2-8]\d{2};
(4) 900-919, which is matched by 9[01]\d; and
(5) 920-925, which is matched by 92[0-5].

This is a case for which you might want to use String object methods to handle the various starting-digits requirements; more code is required but it's way more readable:

if (/^6\d{15}$/.test(document.order.card_no.value) &&
(document.order.card_no.value.substring(1, 4) == "011" ||
"22126" <= document.order.card_no.value.substring(1, 6) && document.order.card_no.value.substring(1, 6) <= "22925" ||
"44" <= document.order.card_no.value.substring(1, 3) && document.order.card_no.value.substring(1, 3) <= "49" ||
document.order.card_no.value.charAt(1) == "5")) { ... }

Somewhat complicating the picture, the text below the IIN table notes:
(1) Effective October 16, 2009, Diners Club [card numbers] beginning with 30, 36, 38, or 39 have been processed by Discover Card; these card numbers should contain 14 digits.
(2) China UnionPay cards are now treated as Discover cards and accepted on the Discover network. China UnionPay card numbers begin with 62 or 88 and contain 16-19 digits.
It is left to the reader to incorporate this information into the preceding code.

Removing spaces and dashes

The aforecited "Finding or Verifying Credit Card Numbers" tutorial puts forward a [ -]+ regexp pattern for flagging spaces and dashes in a user's credit card number entry.

var spaces_and_dashes = /[ -]+/g;
document.order.card_no.value = document.order.card_no.value.replace(spaces_and_dashes, "");

The replace( ) method of the String object is detailed here.

If we want to make use of this code, then we should increase the Credit Card # field's maxlength value to 19 (or maybe throw out the maxlength attribute altogether) - a 4444 4444 4444 4444 Visa card number entry would comprise 19 characters, for example - and remove the (no spaces allowed) text that lies below the field.

checkExpireDate( )

The original checkExpireDate( ) function OKs the submission of the order form if the user has entered something, anything - Hello, World!, 3.14159, whatever - into the Expiration Date field of the Credit Card Info table, and we don't want that, needless to say. In rectifying the situation:
(1) We need to let the user know what we expect with respect to the Expiration Date entry format. Entries deviating from that format must be intercepted.
(2) We also need to flag cards whose expiration dates are in the past - this is, after all, the whole point of an expiration date check.

Credit card expiration dates generally have a mm/yy format, e.g., 07/13 for July 2013, so our first order of business is to replace the MasterCard-Visa logo visamast.gif image below the Expiration Date field with a (mm/yy, e.g., 07/13) string.

We can match the 01-12 mm range with a (?:0[1-9]|1[0-2]) regexp pattern; a (?:1[3-9]|[2-9]\d) pattern will match yy years running from 2013 to 2099. Consequently, the following if statement catches pre-January 2013 expiration dates as well as Expiration Date values that do not conform to the mm/yy format:

if (!/^(?:0[1-9]|1[0-2])\/(?:1[3-9]|[2-9]\d)$/.test(document.order.expire_date.value))
    window.alert("Either your card is too old or your expiration date is not valid.");

Note that the mm/yy slash delimiter is a regexp metacharacter and must be literalized with a backslash.

Credit card expiration dates go into effect at the end of the mm month, e.g., a 07/13 card is good through 31 July 2013, so, this being July 2013, we need to
(1) approve cards with 07/13-and-later expiration dates and
(2) weed out cards with 01/13-06/13 expiration dates.
Towards this end, we will convert the user's mm/yy entry into an expDate Date object and then compare the expDate with a now Date object representing the present time:

var now = new Date( );
...expDate Date object creation code...
if (now < expDate) window.alert("You're good to go.");
else window.alert("Your card has expired.");

Because the JavaScript date is measured in milliseconds since midnight 01 January, 1970 UTC, a simple < comparison is all that is necessary to determine whether the expDate is later than now.

To create our expDate, we first extract the mm and yy parts of the user's mm/yy entry.

var mm = document.order.expire_date.value.substring(0, 2);
var yy = document.order.expire_date.value.substring(3, 5);

Several syntaxes for creating Date objects are available, and a

new Date(year, month, day [hour, minute, second, millisecond]);

constructor is the best choice for the expDate; however, we'll need to adjust the mm and yy extracts a bit before we can plug them into the constructor:

if (mm == "12") { // If mm is for December, go to January of the following year
    mm = 0;
    yy = Number("20" + yy) + 1; }
else {
    mm = Number(mm);
    yy = Number("20" + yy); }
var expDate = new Date(yy, mm, 1);

A credit card effectively expires at the very beginning of the mm+1 month. Index-wise, the mm extract maps onto the desired expDate month (e.g., 07 maps onto August) for all months except December, for which mm must be set to 0 (January). For non-December months, the mm string is numberified via the Number( ) function; it's not strictly necessary to do this (string-to-number type conversion is automatically carried out by the JavaScript engine) but I thought, "Why not?"

For the expDate year, we want to prepend 20 to yy and also push 20yy to the next year if mm is 12. The 20yy year must be numberified prior to incrementing it but can be left alone for non-December mms.

Future mischief

According to this site, Expiration dates can be in the range of two to 10 years, but are typically three to five. The code below will reject credit card expiration dates that are more than 10 years into the future:

var futureCutoff = new Date(now.getFullYear( ) + 10, now.getMonth( ) + 1, 1);
if (futureCutoff < expDate) window.alert("Your expiration date is too far into the future.");


In the div below:
(1) Choose a Card Type.
(2) Enter a value (valid or invalid) into the Credit Card # field and then click outside the field.
(3) Enter a value (valid or invalid) into the Expiration Date field and then click outside the field.
A button is provided for your convenience.

Credit Card Info:
Card Type:

Spaces/dashes between number blocks are OK.

(mm/yy, e.g., 07/13)

We're actually not done with the order.html page yet. Loading the order.html page calls a refresh_ship_details( ) function whereas unloading the page calls an add_ship_details( ) function, and we'll go through these functions in the following entry; moreover, it's time for us to set the shopping cart's cookie machinery in motion, so we'll get to that too.

Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)