-
-
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); |
Comment above absolutely equally applies to utils.creditcard.validateSecurityCode
method.
Very good you have learned the meaning of quantifier curly brackets {}
in the regular expressions (further regex) and \d
special character representing digits.
// Following comments on utils.creditcard.validateNumber
method:
Current revision utils.creditcard.validateNumber
method works great and with no mistakes. Here's why:
- First you verify given input (string) length with
cardNumber.length
IF clause; - In the second IF clause
/\d{16}/.test(cardNumber)
you verify if given input contains at least 16 digits following each other.
So, basically, if we test the second IF clause with an input longer than 16 characters containing non-digits, it will be passed too: console.log( /\d{16}/.test('abc1234567890123456abcdefg12345') );
. See, required 16 digits are there following each other (but there are others too).
However, your second IF clause would never receive an input with length not equals to 16, because the first IF clause makes sure only 16 characters are passed into. So you are safe and the method works with no mistakes.
Still this makes confusion for other developers (source code reviewers). Given input length is checked 2 times: first with cardNumber.length
, second with {16}
(number inside quantifier curly brackets in the regex pattern). Therefore, the logical question coming is: if we can build such a regex that would require exactly 16 digits (no more, no less)? And get rid of first IF clause at all then?
As from tests above, \d{16}
regex requires to have 16 digits following each other, but also accepts longer values with any characters before or after required 16 digits.
Here's the logic for improving our regex that gives a clue for you:
We wish regex require 16 digits\d{16}
match the beginning of the input and the end of the given input.
We already know that 16 digits representation is \d{16}
, so we only need to find out additional special characters in regular expression that would require the \d{16}
to be at the beginning and at the end of the input.
Challenge accepted? ;)
// Following comments on utils.creditcard.validateNumber
method:
Very good work on the function in the current revision. Regex pattern /^\d{16}$/
is absolutely correct and final.
But we can still act like a "Pro" and improve the function a bit more :D Sorry for such a detailed nit-picking, but I think it's valuable to follow excellence when appropriate.
The clue here is to learn what RegExp.prototype.test()
function returns trying to shorter the body of the function to a minimum possible. In our case, it is possible to shorter function body to just a single line.
The last test
utils.creditcard.validateNumber(4111111111111111) === false
is written incorrectly. If we sign functions, its parameters and returnable values with JSDoc annotations, everyone who executes the function have to follow the signature definitions. Otherwise what is documentation for? – right, to follow it.
Annotation above the function says:@param {string} cardNumber Credit card number.
. MeanscardNumber
parameter must be given as a string. In the last test, I am giving it as a number. So the problem is on my side – programmer who executes the function written by other (you in this case) without following its signature (JSDoc annotation).
// Following comments on utils.creditcard.validateNumber
method:
One question about /^\d{16}$/
regex pattern I wish we clarify before going further.
- If
\d
means digits. \d{16}
means 16 digits following one after each other (where even/\d{16}/.test('a1234567890123456abc231')
does match because contains 16 digits in a row even starts with letter "a").- And if
^\d{16}
means 16 digits at the beginning (where/^\d{16}/.test('a1234567890123456abc231')
doesn't match because begins with letter "a", but/^\d{16}/.test('1234567890123456abc231')
match because begins with 16 digits even ends with other letters and digits). - And if
\d{16}$
(without ^ at the beginning) means 16 digits at the end (where/\d{16}$/.test('abcde1234567890123456')
does match because ends with 16 digits in a row, but/\d{16}$/.test('abcde1234567890123456a')
doesn't match because ends with letter "a").
Why then ^\d{16}$
(final pattern) doesn't match /^\d{16}$/.test('1234567890123456abc1234567890123456')
? Where input begins with 16 digits in a row and ends with 16 digits in a row.
This is a very tricky question, but I think knowledge of why doesn't is a true wisdom understanding ^
and $
regex special characters. Think, learn, understand the logic and try to answer me the question.
your final check doesn't match the order of the final pattern, that's why it doesn't match. your string begins with a digit, ends with a digit, but first of all there are letters in the middle, and its not 16 char anymore.
How do you mean doesn't match the order?
There are letters in the middle, but in 3 /^\d{16}/.test('1234567890123456abc231')
there are also letters in the middle, and it is still longer than 16 digits, still matches.
zamorochil mne golovu svoimi voprosami. :)) pishu po russki, moj pervij otvet na tvoj vopros. vopros zvuchal tak. pochemu poslednij pattern ne sootvetstvuet /^\d{16}$/.test('1234567890123456abc1234567890123456')?
vot chto ja tebe napisal: tvoja poslednjaja proverka (t.e. /^\d{16}$/.test('1234567890123456abc1234567890123456')) ne sootvetstvuet porjadku finalnogo patterna kotorij slovami zvuchit kak "1. stroka nachinaeca s cifri, 2. ejo dlinna 16 cifr, 3. stroka zakanchivaeca na 16 cifre". a zdes /^\d{16}$/.test('1234567890123456abc1234567890123456') stroka ne zakonchilas na 16 cifre, za nej sledujut esho bukvi i cifri, vot tebe pervoe nesootvestvie, tozhe samoe nesootvetstvie mi vstretim esli nachnjom s konca stroki. simvol ^ v nashem sluchae ne nahodit cifru esli pered nej est' lubie simvoli, takzhe simvol $ ne nahodit cifru esli posle nejo est' lubie simvoli. tak chto vot chto oznachaet moj otvet "your final check doesn't match the order of the final pattern !
WindTamer commented on 2014-05-05:
- stroka nachinaeca s cifri,
- ejo dlinna 16 cifr,
- stroka zakanchivaeca na 16 cifre
4etko, molodec, razbirae6sja!
Izmeni pozaluista pattern obratno na nash finanljnij (/rabo4ij) i popitajsa ukrotitj funkciju do 1 linii:
manakor commented on 2014-05-04:
But we can still act like a "Pro" and improve the function a bit more :D Sorry for such a detailed nit-picking, but I think it's valuable to follow excellence when appropriate.
The clue here is to learn whatRegExp.prototype.test()
function returns trying to shorter the body of the function to a minimum possible. In our case, it is possible to shorter function body to just a single line.
// Comments on utils.creditcard.validateName
method:
For this task let's assume credit card names contains only English alphabet letters. Because anyway for other language names (Chinese, Arabic etc) we'd write a custom validation per language.
Our task is to make sure given input contains at least 2 words, what basically means there are ASCII characters with at least one space in between. To do that we can follow these logical steps (clue):
- Remove whitespace from both start and end of the give input;
- Build a proper regex that would match only English alphabet letters and spaces. We already know that space in regex is defined as
\s
, and that allowed characters range could be set using regex character set pattern[xyz]
with a hyphen inside:[a-e]
===[abcde]
. However, we need such a characters set that would match only ASCII characters that are English alphabet letters ignoring their case or including both capital and lowercase letters into the pattern.
// Further comments on utils.creditcard.validateName
method:
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:
console.log( utils.creditcard.validateName('Jose Manuel Cordoba Puyol.') === true );
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.
// 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
parseInt
function 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
typeof
value 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.
// Comments on
utils.creditcard.validateNumber
method:\D
regular expression special character checks whether value contains a non-digit character.\s
checks whether value contains a single white space.If so, when then JavaScript interpreter reaches the second part of your IF clause
… || /\s/.test(cardNumber)
? And if reaches ever? Maybe white space is also a non-digit character? ;)Let's try in the console:
console.log( /\D/.test(' ') );
.Your solution for the
utils.creditcard.validateNumber
method is almost brilliant. This means, it's a workable and would be accepted by software source code reviewers. So, congrats, very good job on this one.However, if you wish to make it just brilliant, here is a clue for you:
{}
);\d
(lowercase 'd') special character;Let me know if you will give it a try. Otherwise, I am going to provide my comments on the next method.