Python Arena
Step 4 of 4

Checking input and replacing text

A program can't trust its input. A user types a time as 9.30, a phone number with spaces, or their name where a number should go. A pattern describes exactly what's acceptable, and re.fullmatch checks the whole input against it. A loop asks again until the input fits:

import re

while True:
    time = input("Time (HH:MM): ")
    if re.fullmatch(r"([01]\d|2[0-3]):[0-5]\d", time):
        break
    print("Please use the form HH:MM, such as 09:30.")
print("Alarm set for", time)
Input
9.30
24:00
07:45
Output
Time (HH:MM): 9.30
Please use the form HH:MM, such as 09:30.
Time (HH:MM): 24:00
Please use the form HH:MM, such as 09:30.
Time (HH:MM): 07:45
Alarm set for 07:45

Read the pattern in parts. The hour is either 0 or 1 followed by any digit, or 2 followed by a digit from 0 to 3. Then comes a colon. The minutes are a digit from 0 to 5 followed by any digit. A regular expression sees characters, not numbers, so a range of numbers such as 0 to 23 has to be spelled out digit by digit.

A check that you need in several places fits in a function that returns True or False:

def is_valid_time(text):
    return re.fullmatch(r"([01]\d|2[0-3]):[0-5]\d", text) is not None

Replacing with re.sub

re.sub(pattern, replacement, text) returns a new string in which every match of the pattern is replaced with the replacement. It's handy for tidying up text:

In the Python interpreter
>>> import re
>>> re.sub(r"\s+", " ", "too    many     spaces")
'too many spaces'
>>> re.sub(r"[^0-9]", "", "+1 (555) 010-0199")
'15550100199'
>>> re.sub(r"\d", "*", "PIN 4821, card 7734")
'PIN ****, card ****'
>>> re.sub(r"colou?r", "hue", "Color? Pick a colour.")
'Color? Pick a hue.'

The first line squeezes every run of whitespace into one space. The second removes everything that isn't a digit, by replacing it with the empty string. The last one leaves Color alone, since patterns are case sensitive. Like the string methods, re.sub never changes the original string: strings can't be changed, so it returns a new one.

Tidying and checking work well together. This function removes the spaces, brackets and dashes people like to type in phone numbers, and then checks what's left:

import re


def clean_phone(text):
    digits = re.sub(r"[ ()-]", "", text)
    if re.fullmatch(r"\+?\d{7,15}", digits):
        return digits
    return None


for number in ["+358 40 123 4567", "(555) 010-0199", "call me"]:
    print(clean_phone(number))
Output
+358401234567
5550100199
None

The class [ ()-] lists a space, two round brackets and a dash. Inside square brackets, round brackets are ordinary characters, and the dash is an ordinary dash because it comes last. In the middle of the class, as in [ -)], it would make a range from the space to ), which takes in ! and # too, and the dash itself would no longer be in the class.

Your task

Challenge 1 of 3

The program asks for a username until it gets one that is 3 to 12 characters long and has only small letters and digits. Fill in the two blanks: the re function that checks the whole text, and the quantifier. An example run:

Username: Ada
Use 3 to 12 small letters or digits.
Username: ada_l
Use 3 to 12 small letters or digits.
Username: ada99
Welcome, ada99!

Challenge 2

Write a function mask(card) that returns the card number with every digit except the last four replaced by *. Other characters, such as spaces, stay as they are. Use re.sub. For example,

print(mask("1234 5678 9012 3456"))

should print:

**** **** **** 3456

Challenge 3

Write a program that reads prices until the user enters an empty line. A valid price is one or more digits, optionally followed by a dot and exactly two digits: 12, 3.50 and 0.99 are valid, but 3.5, .50, 12.345 and ten are not. For each invalid line the program prints Not a price: and the line right away. At the end it prints the total of the valid prices, with two decimals. An example run:

Price: 3.50
Price: 12
Price: 3.5
Not a price: 3.5
Price: 0.99
Price:
Total: 16.49