-
-
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); |
// Further comments on utils.creditcard.validateName method:
It's not appropriate to test for strict equality (=== is a strict equality operator that doesn't convert types) such a different value types name === /^\s*$/, where name is expected to be string (JSDoc annotation states name parameter is expected to be a string) and /^\s*$/ is always object. Therefore, we can omit your first IF clause.
Read more about equality in JavaScript: === vs ==.
Regex pattern /^[a-z]{1,}(\s[a-z]{1,}){1,}$/i works well and passes all tests, so it could be taken as final. As I understand it requires string to begging with at least one letter followed by space and at least one letter occurred more than once at the end of the string.
I think it will be respectful to allow - (hyphen character). There might be coupled surnames containing 2 words separated by hyphen you mentioned before. And, maybe, shall we accept at least two letters instead of one for name/surname matches? Let's suppose there's no names/surnames of one letter.
console.log( utils.creditcard.validateName('n l') === false ); // Don't allow 1 letter names.
console.log( utils.creditcard.validateName('na li') === true );
Can you please apply suggested fixes and try to make function body contain just one line? Like you perfectly did with utils.creditcard.validateNumber and utils.creditcard.validateSecurityCode functions.
// Comments on utils.creditcard.validateDate method:
I cannot find any stable documentation on String.prototype.toArray method you use to split given string into an array. The only resource what I found on the topic is EcmaScript 6 string extras proposal from 2011, however it was not implemented. We shan't rely on proposals when building for use in production. Therefore, please replace your toArray function to an appropriate one.
As well would be nice if you simplify the function body trying to turn your IFELSE clauses into one line returnable boolean. Variables defined above IFELSE clauses are okay to keep.
Turning your 3 IFELSE clauses into one line returnable boolean is a good challenge, isn't it? :D
// 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:
- Optionally omit regex validation relying on JSDoc annotation above the function;
- Understand how programming language interpreter executes the code (values inside parenthesis are examined first, types are taken in mind with math operators);
- Learn how
parseIntfunction works and its required params; - Fix the logic of the inline IF clause being careful with AND
&&operator.
// 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
- Understand what
typeofvalue logical operators return; - 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).
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.
// Further comments on
utils.creditcard.validateNamemethod:Currently method passes all the required test cases, however doesn't follow validation logic being strictly tied to a given test cases. For example, imagine credit card owner is from Spain and his name contains 4 words: Jose Manuel Cordoba Puyol. Try this test use case to see how validation fails:
We shall simplify regex pattern and make validation more general allowing given name contains at least 2 words with spaces in between.
Also please review step 2 (contains a couple of clues) from the previous comment on this function.