{"id":"regex-checking-input","slug":"checking-input","title":"Checking input and replacing text","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:\n\n```python\nimport re\n\nwhile True:\n    time = input(\"Time (HH:MM): \")\n    if re.fullmatch(r\"([01]\\d|2[0-3]):[0-5]\\d\", time):\n        break\n    print(\"Please use the form HH:MM, such as 09:30.\")\nprint(\"Alarm set for\", time)\n```\n\n```input\n9.30\n24:00\n07:45\n```\n\n```output\nTime (HH:MM): 9.30\nPlease use the form HH:MM, such as 09:30.\nTime (HH:MM): 24:00\nPlease use the form HH:MM, such as 09:30.\nTime (HH:MM): 07:45\nAlarm set for 07:45\n```\n\nRead 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.\n\nA check that you need in several places fits in a function that returns `True` or `False`:\n\n```python\ndef is_valid_time(text):\n    return re.fullmatch(r\"([01]\\d|2[0-3]):[0-5]\\d\", text) is not None\n```\n\n### Replacing with re.sub\n\n`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:\n\n```pycon\n>>> import re\n>>> re.sub(r\"\\s+\", \" \", \"too    many     spaces\")\n'too many spaces'\n>>> re.sub(r\"[^0-9]\", \"\", \"+1 (555) 010-0199\")\n'15550100199'\n>>> re.sub(r\"\\d\", \"*\", \"PIN 4821, card 7734\")\n'PIN ****, card ****'\n>>> re.sub(r\"colou?r\", \"hue\", \"Color? Pick a colour.\")\n'Color? Pick a hue.'\n```\n\nThe 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.\n\nTidying 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:\n\n```python\nimport re\n\n\ndef clean_phone(text):\n    digits = re.sub(r\"[ ()-]\", \"\", text)\n    if re.fullmatch(r\"\\+?\\d{7,15}\", digits):\n        return digits\n    return None\n\n\nfor number in [\"+358 40 123 4567\", \"(555) 010-0199\", \"call me\"]:\n    print(clean_phone(number))\n```\n\n```output\n+358401234567\n5550100199\nNone\n```\n\nThe 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.\n","task":"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:\n\n```\nUsername: Ada\nUse 3 to 12 small letters or digits.\nUsername: ada_l\nUse 3 to 12 small letters or digits.\nUsername: ada99\nWelcome, ada99!\n```\n","kind":"fill","seed":"import re\n\nwhile True:\n    name = input(\"Username: \")\n    if re.[[fullmatch]](r\"[a-z0-9][[{3,12}]]\", name):\n        break\n    print(\"Use 3 to 12 small letters or digits.\")\nprint(f\"Welcome, {name}!\")\n","solution":"import re\n\nwhile True:\n    name = input(\"Username: \")\n    if re.fullmatch(r\"[a-z0-9]{3,12}\", name):\n        break\n    print(\"Use 3 to 12 small letters or digits.\")\nprint(f\"Welcome, {name}!\")\n","hints":["The whole name has to fit the pattern, not just part of it, so use the function that checks the whole text.","The function is `re.fullmatch`. From 3 to 12 times is the quantifier `{3,12}`."],"tests":[{"name":"Test 1","stdin":"Ada\nada_l\nada99\n","expect":"Username: Ada\nUse 3 to 12 small letters or digits.\nUsername: ada_l\nUse 3 to 12 small letters or digits.\nUsername: ada99\nWelcome, ada99!","hidden":false},{"name":"Test 2","stdin":"xy\nthisnameiswaytoolong\nbob\n","expect":"Username: xy\nUse 3 to 12 small letters or digits.\nUsername: thisnameiswaytoolong\nUse 3 to 12 small letters or digits.\nUsername: bob\nWelcome, bob!","hidden":true},{"name":"Test 3","stdin":"grace1\n","expect":"Username: grace1\nWelcome, grace1!","hidden":true}],"require":[],"forbid":[],"more":[{"task":"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,\n\n```python\nprint(mask(\"1234 5678 9012 3456\"))\n```\n\nshould print:\n\n```\n**** **** **** 3456\n```\n","kind":"code","seed":"import re\n\n\ndef mask(card):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(mask(\"1234 5678 9012 3456\"))\n","solution":"import re\n\n\ndef mask(card):\n    return re.sub(r\"\\d\", \"*\", card[:-4]) + card[-4:]\n\n\nif __name__ == \"__main__\":\n    print(mask(\"1234 5678 9012 3456\"))\n","hints":["Split the job in two: the start of the text, `card[:-4]`, and its last four characters, `card[-4:]`.","Replace every digit of the start with `re.sub(r\"\\d\", \"*\", ...)`, and add the last four characters back unchanged."],"tests":[{"name":"print(mask(\"1234 5678 9012 3456\"))","stdin":"","call":"print(mask(\"1234 5678 9012 3456\"))\n","expect":"**** **** **** 3456","hidden":false},{"name":"print(mask(\"4000-1234-5678-9010\"))","stdin":"","call":"print(mask(\"4000-1234-5678-9010\"))\n","expect":"****-****-****-9010","hidden":true},{"name":"print(mask(\"98765\"))","stdin":"","call":"print(mask(\"98765\"))\n","expect":"*8765","hidden":true}],"require":[{"pattern":"\\bre\\.sub\\s*\\(","message":"Replace the digits with `re.sub`."}],"forbid":[]},{"task":"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:\n\n```\nPrice: 3.50\nPrice: 12\nPrice: 3.5\nNot a price: 3.5\nPrice: 0.99\nPrice:\nTotal: 16.49\n```\n","kind":"code","seed":"import re\n","solution":"import re\n\ntotal = 0\nwhile True:\n    line = input(\"Price: \")\n    if line == \"\":\n        break\n    if re.fullmatch(r\"\\d+(\\.\\d\\d)?\", line):\n        total += float(line)\n    else:\n        print(\"Not a price:\", line)\nprint(f\"Total: {total:.2f}\")\n","hints":["Read lines in a `while True` loop and `break` when the line is empty.","A price is `\\d+` followed by an optional group of a dot and two digits. The dot needs a backslash: `r\"\\d+(\\.\\d\\d)?\"`.","Check each line with `re.fullmatch`. Add the valid ones to the total with `float(line)`, and print `f\"Total: {total:.2f}\"` at the end."],"tests":[{"name":"Test 1","stdin":"3.50\n12\n3.5\n0.99\n\n","expect":"Price: 3.50\nPrice: 12\nPrice: 3.5\nNot a price: 3.5\nPrice: 0.99\nPrice:\nTotal: 16.49","hidden":false},{"name":"Test 2","stdin":"ten\n.50\n12.345\n7\n100.10\n\n","expect":"Price: ten\nNot a price: ten\nPrice: .50\nNot a price: .50\nPrice: 12.345\nNot a price: 12.345\nPrice: 7\nPrice: 100.10\nPrice:\nTotal: 107.10","hidden":true},{"name":"Test 3","stdin":"1.5\n\n","expect":"Price: 1.5\nNot a price: 1.5\nPrice:\nTotal: 0.00","hidden":true}],"require":[{"pattern":"\\bre\\.fullmatch\\s*\\(","message":"Check each price with `re.fullmatch`."}],"forbid":[]}]}