# Validating Credit Card Numbers

This is one of the puzzles I was trying to solve, where the question is to validate the credit card number with following characteristics:

• It must contain exactly 16 digits.
• It must only consist of digits (0-9).
• It may have digits in groups of 4 , separated by one hyphen "-".
• It must NOT use any other separator like ' ' , '_', etc.
• It must NOT have 4 or more consecutive repeated digits.

I have go through some of the solutions here and here as well, but the characteristics for validating are different from what I was working on, hence posting this.

total_count = int(raw_input())
numbers_list = []
#refernece list to check the starting series
start_list = [4,5,6]

for count in range(total_count):
numbers_list.append(raw_input())

#condition 1, validates the starting series
def val_start(num):
if int(num[0]) in start_list:
return True
else:
return False

#to check if individial elements of the list are of length=4
#4321-5555-67899-9991, splitted to ["4321","5555","67899","991"] which is invalid
def val_group(num):
for val in num.split("-"):
if len(val) != 4:
return False

return True

#condition 2, validates the length of the number
def val_len(num):
check = num
check2 = True
if "-" in num:
check = "".join(num.split("-"))
check2 = val_group(num)

if ((len(check) == 16) and check2):
return True
else:
return False

#condition 3, validates if input consists only number
def val_isdigit(num):
if not num.isdigit():
for ch in num:
if not (ch.isdigit() | (ch == "-")):
return False
return True

#condition 4, validates the repetition of any number for 4 consective times
def val_rep(num):
res = "".join(num.split("-"))
for i in range(len(res)):
try:
if (res[i] == res[i+1]):
if (res[i+1] == res[i+2]):
if (res[i+2] == res[i+3]):
return False
except IndexError:
pass
return True

for num in numbers_list:
#returns all the values into a list
result = [val_start(num), val_len(num),val_isdigit(num), val_rep(num)]
if False in result:
print("Invalid")
else:
print("Valid")


I am looking for an optimised way for the function val_rep(), which validates the repetition of any number for 4 consecutive times.

First of all, you should encapsulate your functionality in a function, unless you really want to use your code only once. At the moment you would have to duplicate your code somewhat just to check a second credit card. So instead let us write a function with a doc string:

def is_valid_card_number(sequence):
"""Returns True' if the sequence is a valid credit card number.

A valid credit card number
- must contain exactly 16 digits,
- must only consist of digits (0-9) or hyphens '-',
- may have digits in groups of 4, separated by one hyphen "-".
- must NOT use any other separator like ' ' , '_',
- must NOT have 4 or more consecutive repeated digits.
"""



However, credit card numbers follow a fixed pattern, and therefore are a perfect candidate for regular expressions:

import re

PATTERN='^([456][0-9]{3})-?([0-9]{4})-?([0-9]{4})-?([0-9]{4})$'  This might seem arcane, so let us have a look at that pattern. Anything inside parentheses is considered a group. So we have four groups: ([456][0-9]{3}) and three times ([0-9]{4}). The hyphens in between have ? and are therefore optional. If they exist, they must occur once, so -- is not allowed. The […] indicates are character class. Only characters inside that class are allowed at that point. The ^ indicates that we're describing the start of our string, not some point in the middle. So we have to start with 456. That requriement is now met. We're followed by 3 ({3}) digits. Our first group therefore consists of four digits, and so do the others. The $ makes sure that there is nothing left: our string has to stop there.

We now have met most of our conditions:

• we have 16 digits
• we have groups of four
• we only have digits or hyphens

We're missing the four consecutive digits. This is now tricky, since it's not clear whether the hyphens are part of the run. If they are only invalid four 4 character groups, we can simply check whether the current character * 4 is the complete group:

import re

PATTERN='^([456][0-9]{3})-?([0-9]{4})-?([0-9]{4})-?([0-9]{4})$' def is_valid_card_number(sequence): """Returns True' if the sequence is a valid credit card number. A valid credit card number - must contain exactly 16 digits, - must start with a 4, 5 or 6 - must only consist of digits (0-9) or hyphens '-', - may have digits in groups of 4, separated by one hyphen "-". - must NOT use any other separator like ' ' , '_', - must NOT have 4 or more consecutive repeated digits. """ match = re.match(PATTERN,sequence) if match == None: return False for group in match.groups: if group[0] * 4 == group: return False return True  If the repeated digits may also not span hyphens/groups it gets a little bit more complicated, but not too much. This is left as an exercise, though. That being said: make sure to handle multiple numbers, not a single one. That way you will be able to check your code easier. But that depends. If it's a single use script, you probably don't have a need for a function. • Just to note, your code says 1122-2233-4455-6677 or 1122223344556677 is valid, however I don't think that passes the "must NOT have 4 or more consecutive repeated digits" requirement – Peilonrayz Jul 18 '17 at 9:01 • @Peilonrayz noted right below the code: "If the repeated digits may also not span hyphens/groups it gets a little bit more complicated, but not too much. This is left as an exercise, though." – Zeta Jul 18 '17 at 9:02 • Ah yes, I missed that. :) – Peilonrayz Jul 18 '17 at 9:05 • Can't you just do PATTERN='^([456][0-9]{3})(-?([0-9]{4}){3})$', or am I missing something? – Stephen S Jul 18 '17 at 13:00
• @StephenS Now we have one match.groups too many. You would have to ignore the second one. (?:…) might work, but I'm not sure about the latter capturing in Python at the moment. – Zeta Jul 18 '17 at 13:02

You can do most of this with regex. Going through your requirements:

• It must only consist of digits (0-9).

And so we can use [0-9] for each digit.

• It must contain exactly 16 digits.

And so we can use something like [0-9]{16}. However, that only checks that it contains sixteen digits, so 1234567890123456abc would be valid. So we need something like ^[0-9]{16}$ • It must start with a 4,5 or 6 And so we can change the first match. ^[456][0-9]{15}$

• It may have digits in groups of 4 , separated by one hyphen "-".

This makes the regex a bit longer, but it still stays quite simple. The only complex thing is it's all or nothing it seems, so you can either separate or not. This check would be best in Python. However that means splitting up the groups with -?

^[456][0-9]{3}-?[0-9]{4}-?[0-9]{4}-?[0-9]{4}$ • It must NOT use any other separator like ' ' , '_', etc. This works the same as the above. If it however wanted us to allow those, we'd have to change the separators to groups such as [\- _]. • It must NOT have 4 or more consecutive repeated digits. We can't do this easily in pure regex, so it's best to do this in Python. And so we'd want to use capture groups to get the numbers. ^([456][0-9]{3})-?([0-9]{4})-?([0-9]{4})-?([0-9]{4})$


And so if it matches the above regex, you just have to check that no four consecutive digits are the same.

To improve val_rep I'd use a modified version on the itertools pairwise recipe.

def quadwise(iterable):
"s -> (s0,s1,s2,s3), (s1,s2,s3,s4), (s2,s3,s4,s5), ..."
a, b, c, d = tee(iterable, 4)
next(b, None)
next(c, None)
next(c, None)
next(d, None)
next(d, None)
next(d, None)
return zip(a, b, c, d)


Which can allow for a simple check, such as:

def val_rep(num):
if all(head == item for item in tail):
return False
return True


Which could be 'simplified' to:

def val_rep(num):
return not any(
all(head == item for item in tail)
)


Which in Python 2 would have to be something like:

def val_rep(num):
return not any(
all(head == item for item in (a, b, c))
)


And so I'd recommend something like:

REGEX = r'^([456][0-9]{3})-?([0-9]{4})-?([0-9]{4})-?([0-9]{4})$' def quadwise(iterable): "s -> (s0,s1,s2,s3), (s1,s2,s3,s4), (s2,s3,s4,s5), ..." a, b, c, d = itertools.tee(iterable, 4) next(b, None) next(c, None) next(c, None) next(d, None) next(d, None) next(d, None) return zip(a, b, c, d) def val_rep(num): return not any( all(head == item for item in (a, b, c)) for head, a, b, c in quadwise(num) ) def valid_creditcard(card): groups = re.match(REGEX, card) if not groups: return False if card.count('-') not in (0, 3): return False return val_rep(''.join(groups.groups())) if __name__ == '__main__': total_count = int(raw_input()) cards = [ raw_input() for count in range(total_count) ] for card in cards: if valid_creditcard(card): print('Valid') else: print('Invalid')  • You've talked about -?, but you're missing it in your code. Typo? Oh, and we can do the 4-digit-consecutive check in regular expressions, but the expression will be… ugly. – Zeta Jul 18 '17 at 8:55 • (Your last non-python code snippet is still missing -?) – Zeta Jul 18 '17 at 8:57 • @Zeta Oops, thank you for that. :) I noticed I was reading it wrong half way through writing the answer. – Peilonrayz Jul 18 '17 at 8:57 • If you use ^$ in your REGEX, you don't need the card.count('-'). – Zeta Jul 18 '17 at 9:05
• I think the quadwise approach is overkill compared to something like any(len(group) >= 4 for _, group in itertools.groupby(num)). – Gareth Rees Jul 18 '17 at 16:51

Your val_rep function can also be simplified with regular expressions by using backreferences (see the \number syntax). Here \1 will be replaced by the first matched number.

import re

REP_PATTERN=r"([0-9])(?:-?\1){3}"

def val_rep(sequence):
return not re.search(REP_PATTERN, sequence)


You can even combine all the tests into a single regular expression using a negative lookahead (?!...):

import re

PATTERN=r"^(?!.*([0-9])(?:-?\1){3})[456][0-9]{3}(-?)[0-9]{4}\2[0-9]{4}\2[0-9]{4}\$"

def is_valid_card(sequence):
return re.search(PATTERN, sequence)


Note that in the second pattern we are using a backreference to reject card numbers such as 4123-45678910-1234 where hyphens are inconsistent (\2 will match either a hyphen or an empty sequence)

You can massively simplify this by using regular expressions. Python includes an efficient module for that, named re (short for regular expressions).

In this case, check if a given string is a valid credit card number (CC.py):

import re

PATTERN = "([4-6]{1})([0-9]{3}-?)([0-9]{4}-?){2}([0-9]{4})"

def is_valid_creditcard(sequence):
"""Check if a sequence is a valid credit card number.
Rules for sequences to qualify as credit card numbers:

Sequences must:

-Contain exactly 16 digits;
-Only consist of digits (0-9).

Sequences may:
-Have digits in groups of 4, separated by one hyphen.

Sequence must not:
-Use any other separator;
-Have 4 or more consecutive repeated digits.
"""
for i, n in enumerate(sequence):
try:
if (sequence[i],
sequence[i+1],
sequence[i+2],
sequence[i+3]
) == (n, n, n, n):
return False
except IndexError:
pass
return bool(re.match(PATTERN, sequence))


Examples:

>>> import CC
>>> CC.is_valid_creditcard("0000-1111-2222-3333")
False
>>> # Starts with "0", not "4"/ "5" / "6"
>>> CC.is_valid_creditcard("4444-5555-6666-777")
False
>>> # Incorrectly grouped
>>> CC.is_valid_creditcard("4444-3333-2222-XXXX")
False
>>> # Contains non-numerical characters
>>> CC.is_valid_creditcard("444-55555-6666-7777")
False
>>> # Incorrectly grouped
>>> CC.is_valid_creditcard("4567:8901:2345:6789")
False
>>> # Illegal seperators
>>> CC.is_valid_creditcard("4444-5555-6666-7777")
False
>>> # Contains 4 or more consecutive repeated digits
>>> CC.is_valid_creditcard("4011-7505-1047-1848")
True
>>> CC.is_valid_creditcard("6015399610667820")
True


If you're new to regular expressions, you may want to check out this page.

• Your function fails both the hyphens (due to your any check) as well as the "4 consecutive repeated digits" requirement. Also, the length check is wrong. According to the OP, a sequence may contain hypens, therefore "4567-1234-1234-1234" is a valid sequence, but certainly does not have length 16. You should probably review (heh) your code and OP's requirements. – Zeta Jul 18 '17 at 8:09
• Your new regular expression misses the "no 4 consecutive digits part". – Zeta Jul 18 '17 at 8:27
• Still wrong. Now any credit card that contains four times the same digit is wrong, e.g. 4567-1234-4564-4534 has 6 4s, but is valid according to the spec. – Zeta Jul 18 '17 at 8:42
• I noticed that just now myself- I really need to get more creative. – Daniel Jul 18 '17 at 8:44