Skip to content

Instantly share code, notes, and snippets.

@WindTamer
Forked from niksumeiko/exercises.creditcard.js
Last active September 23, 2015 12:10
Show Gist options
  • Select an option

  • Save WindTamer/11199270 to your computer and use it in GitHub Desktop.

Select an option

Save WindTamer/11199270 to your computer and use it in GitHub Desktop.
/**
* @fileoverview Utils Interface credit card validation methods.
* @author (Wind Tammer)
*/
utils = {};
/** @type {Object<Function>} */
utils.creditcard = {};
/**
* Checks whether credit card number is valid.
* @param {string} cardNumber Credit card number.
* @return {Boolean}
*/
utils.creditcard.validateNumber = function(cardNumber)
{
// Function has to return boolean identifying whether credit
// card number contains exactly 16 digits.
return /^\d{16}$/.test(cardNumber);
};
/**
* Checks whether credit card security code is valid.
* @param {string} securityCode Credit card security.
* @return {Boolean}
*/
utils.creditcard.validateSecurityCode = function(securityCode) {
// Function has to return boolean identifying whether credit
// card security code (CVV/CVC) contains exactly 3 digits.
return /^\d{3}$/.test(securityCode);
};
/**
* Checks whether credit card name is valid.
* @param {string} name Credit card owner name.
* @return {Boolean}
*/
utils.creditcard.validateName = function(name) {
// Function has to return boolean identifying whether name on
// the credit card contains at least 2 words with no digits.
return /^[a-z]{2,}(\s[a-z]{2,}){1,}$/i.test(name);
};
/**
* Checks whether credit card expiration date is valid.
* @param {string} expirationDate Credit card expiration date in
* YYYY-MM format.
* @return {Boolean}
*/
utils.creditcard.validateDate = function(expirationDate) {
// Function has to return boolean identifying whether credit
// card expiration date is at least next month. If today is April,
// expiration date has to be at least May.
var expirationDateArray = expirationDate.split('-', 2);
var today = new Date();
var thisMonth = today.getUTCMonth() + 1;
var thisYear = today.getFullYear();
var givenMonth = parseInt(expirationDateArray[1], 10);
var givenYear = parseInt(expirationDateArray[0], 10);
return(givenYear > thisYear) || (givenYear == thisYear) && (givenMonth > thisMonth);
};
/**
* @fileoverview Super simple Unit tests. If any outputs
* 'false' function is not working as planned.
* @author (Wind Tammer)
*/
console.log(utils.creditcard.validateNumber('4111111111111111') === true);
console.log(utils.creditcard.validateNumber('2312312312') === false);
console.log(utils.creditcard.validateNumber('411a343432dasd') === false);
console.log(utils.creditcard.validateNumber('4111 1111 1111 1111') === false);
console.log(utils.creditcard.validateNumber(4111111111111111) === false);
console.log(utils.creditcard.validateSecurityCode('09') === false);
console.log(utils.creditcard.validateSecurityCode('A12') === false);
console.log(utils.creditcard.validateSecurityCode('1 2 3') === false);
console.log(utils.creditcard.validateSecurityCode('347') === true);
console.log(utils.creditcard.validateSecurityCode(347) === false);
console.log(utils.creditcard.validateSecurityCode('347234234') === false);
console.log(utils.creditcard.validateName('Nik Sumeiko') === true);
console.log(utils.creditcard.validateName('NikSumeiko') === false);
console.log(utils.creditcard.validateName('Nik Sumeiko II') === true);
console.log(utils.creditcard.validateName('Nik Sumeiko 2') === false);
console.log(utils.creditcard.validateName(' ') === false);
console.log(utils.creditcard.validateDate('2014-04') === false);
console.log(utils.creditcard.validateDate('2014-07') === true);
console.log(utils.creditcard.validateDate('201404') === false);
console.log(utils.creditcard.validateDate(201407) === false);
console.log(utils.creditcard.validateDate('') === false);
@niksumeiko
Copy link

// Further comments on utils.creditcard.validateDate method:
As mentioned lots of comments before, please do not hesitate to rely on annotation describing the function located above it. Professional programmers are trying to write basic documentation for their code, therefore JSDoc annotations are invented. So if annotation says @param {string} expirationDate Credit card expiration date in YYYY-MM format, you can rely that the parameter given is going to be a YYYY-MM string and there's no additional code required to validate it once again. So testing given parameter to match regex pattern could be omitted. However, in your programmer carrier you will face many colleagues that avoid using annotations/documentation or carrying about value types. Just always remember, using these techniques really help make source code semantic, readable by other programmers and preventing unnecessary bugs in advance.

Going further. The use of parseInt function is wrong in the last part of the inline IF clause:

console.log( parseInt('02' + 1) );
// You wish it return 3, but "wow" it returns 21. Is that what you expected?

To understand why it happens, we shall know the order of how JavaScript (actually, any programming language) interpreter executes the code. First it looks into parenthesis, seeing '02' + 1 expression inside. Interpreter tries to understand types of the values to both sides of plus + operator. In math plus operator is supposed to sum numbers so does programming language interpreter, but only if both values are numbers typeof value === 'number'. If one of the values is NOT a number, interpreter just concatenates these values converting both to strings:

'02' + '123'; // Evaluates to '02123'.
'02' + 1; // Evaluates to '021';

Then interpreter executes parseIn function with a concatenated string '021' that evaluates to number 21.
So if given month is 02 (February) you basically compare 21 to the current month.

As well, the logic of the inline IF clause parseInt(expirationDateArray[0]) >= thisYear && parseInt(expirationDateArray[1] + 1) > thisMonth) is a bit wrong. What if given year is 2015, but given month is 2 what is less than current month (5):

console.log( 2015 >= 2014 && 3 > 5 ); // false
// Why it evaluates to false if given year is 2015 (next year)?
// Card is valid if its year is 2015, month doesn't matter.

Here's a workflow to perform to fix the issue:

  1. Optionally omit regex validation relying on JSDoc annotation above the function;
  2. Understand how programming language interpreter executes the code (values inside parenthesis are examined first, types are taken in mind with math operators);
  3. Learn how parseInt function works and its required params;
  4. Fix the logic of the inline IF clause being careful with AND && operator.

@niksumeiko
Copy link

// Further comments on utils.creditcard.validateDate method:
You have successfully fixed the logic of the validation, and the function works correctly as required. However, as said before, would awesome to inline your IFELSE clause to make the source a bit shorter.

To help you translate IFELSE clause into one line, I'd suggest to define 2 new variables above:

...
  var today = new Date(),
      thisMonth = today.getUTCMonth() + 1,
      givenMonth = parseInt(expirationDateArray[1], 10),
      thisYear = today.getFullYear(),
      givenYear = parseInt(expirationDateArray[0], 10);

  if (givenYear === thisYear && givenMonth > thisMonth) {
    return true;
  } else if (givenYear > thisYear) {
    return true;
  } else {
    return false;
  }

With 2 additional variables and thisMonth normalisation (+1) directly in the variable definition, IFELSE clause becomes more readable, so you can translate it into one line easier.

The clue is to

  1. Understand what typeof value logical operators return;
  2. Try to see your IFELSE clause as 3 different parts and try to rewrite each part as a separate inline returnable boolean; then when you have each part, combine them together using logical operator(s).
// This is first part of your IFELSE clause:
if (givenYear === thisYear) {
  return true;
}

// This is the second part:
if (givenYear > thisYear) {
  return true;
}

// This is the third part (already an inline returnable boolean):
return false;

The last part is already an inline returnable boolean.
But can you try to rewrite first and second parts as inline returnable booleans as well? And then try to group them using appropriate logical operator in between:
return (returnable boolean) logical operator (returnable boolean).

@niksumeiko
Copy link

I see you have finalised utils.creditcard.validateDate method. This means we have finished the exercise successfully. Congratulations!

Truly intelligent programmers reuse existing things a lot (even they are built by others). They invent new programs only when really required.

From now on you will be able to validate credit cards, when you have a project that requires credit card validation on the client. Keep your creations under control and reuse them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment