1
\$\begingroup\$

Date Detection:

Write a regular expression that can detect dates in the DD/MM/YYYY format.

Assume that the days range from 01 to 31, the months range from 01 to 12, and the years range from 1000 to 2999. Note that if the day or month is a single digit, it’ll have a leading zero.

Then store these strings into variables named month, day, and year, and write additional code that can detect if it is a valid date.

April, June, September, and November have 30 days, February has 28 days, and the rest of the months have 31 days. February has 29 days in leap years. Leap years are every year evenly divisible by 4, except for years evenly divisible by 100, unless the year is also evenly divisible by 400. Note how this calculation makes it impossible to make a reasonably sized regular expression that can detect a valid date.

import re
def check_date(day, month, year):
    # April, June, September, November = 30 days/ February = 28 days, unless leapyear so 29/ rest has 31 days
    month_dict = {4: 30, 6: 30, 9: 30, 11: 30, 2: 28}
    day_bound = month_dict.get(month, 31)

    # month is february
    if day_bound == 28:
        # checks if the year is a leap year
        if year % 4 == 0:
            if year % 100 == 0:
                if year % 400 == 0:
                    day_bound = 29
            else:
                day_bound = 29

    # if the day is in the range of possible days
    if day <= day_bound:
        return True
    return False
        


# DD/MM/YYYY
string = "31/02/2020"
date_regex = re.compile(r"([0-2]\d|3[01])/(0\d|1[0-2])/([12]\d{3})")
match = date_regex.search(string)
valid = False
if match:
    day = int(match.group(1))
    month = int(match.group(2))
    year = int(match.group(3))
    valid = check_date(day, month, year)

if valid:
    print(f'Day: {day}, Month: {month}, Year: {year}')
else:
    print('Invalid Date!')

\$\endgroup\$
2
\$\begingroup\$

Docstrings / Type hints

These allow you to describe how your code works in a pythonic manner. Docstrings allow IDE's and other documentation tools to see what your function/class does. Type hints allow you to show what types of parameters are accepted, and what types of values are returned.

check_date

Instead of calculating a leap year yourself, you can use calendar.isleap from the calendar module.

Return comparisons, not raw booleans

Instead of

if day <= day_bound:
    return True
return False

do this

return day <= day_bound

Does the exact same thing, but looks a lot nicer.

Split code into functions

You've done a good job splitting your code into functions, but I think you could use one more. Instead of parsing the date in the "main" code, put that code in another function, and pass it the date string.

def get_date_values(...) -> ...:
   ...

With all these changes, your final code would look something like this:

import re
import calendar
from typing import Tuple, Union

def check_date(day: int, month: int, year: int) -> bool:
    """
    Returns a bool based on if the date passed is a valid date.

    :param int day: Day.
    :param int month: Month.
    :param int year: Year.

    :return: True if a valid date, False otherwise.
    """
    # April, June, September, November = 30 days/ February = 28 days, unless leapyear so 29/ rest has 31 days
    month_dict = {4: 30, 6: 30, 9: 30, 11: 30, 2: 28}
    day_bound = month_dict.get(month, 31)

    if day_bound == 28:
        if calendar.isleap(year):
            day_bound = 29

    return day <= day_bound


def get_date_values(date: str) -> Union[Tuple[int, int, int], None]:
    """
    Returns a tuple containing the day, month, and year of the passed date.

    :param str date: Date to parse and retrieve values.

    :return: Either a Tuple, or for an invalid date, None.
    """
    date_regex = re.compile(r"([0-2]\d|3[01])/(0\d|1[0-2])/([12]\d{3})")
    match = date_regex.search(date)
    if match:
        return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
    return None


if __name__ == "__main__":
    date = "31/02/2020" #DD/MM/YYYY
    if check_date(*get_date_values(date)):
        print('Valid Date!')
    else:
        print('Invalid Date!')

I'll explain a bit more since I made some changes I haven't mentioned already.

Unpacking

if check_date(*get_date_values(date)):

This line unpacks each item from the tuple returned by get_date_values and passes them to the function. Since the tuple has three values, and check_date accepts three parameters, the * unpacks the tuple and passes each value to the function.

\$\endgroup\$
1
  • \$\begingroup\$ unpacking needs an existing tuple. this is an error not present in the OPs code \$\endgroup\$ – stefan Jun 18 '20 at 12:44
2
\$\begingroup\$

Just in case there might be a restriction on using standard date functions, the leap year logic can be reduced to one conditional block:

if day_bound == 28 and ((year % 4 == 0 and year % 100 != 0)
                        or year % 400 == 0):
    day_bound = 29
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Or even better, separate this in its own function \$\endgroup\$ – Maarten Fabré Jun 18 '20 at 6:26
2
\$\begingroup\$

There are some problems in your code structure and testability.

Provide a testable function that covers the given task completely

If you want to test your code against the requirements you will need a function

def is_valid_date_string(s):
    #[...]

that you can use in testing, e. g.

assert is_valid_date_string("31/02/2020") == True

that function shall not contain I/O (other than logging). So we restructure your main code like

def is_valid_date_string(string):
    # DD/MM/YYYY
    date_regex = re.compile(r"([0-2]\d|3[01])/(0\d|1[0-2])/([12]\d{3})")
    match = date_regex.search(string)
    if match:
        day = int(match.group(1))
        month = int(match.group(2))
        year = int(match.group(3))
        return check_date(day, month, year)
    return False

if __name__ == '__main__':
    string = "31/02/2020"
    if is_valid_date_string(string):
        print(string)
    else:
        print('Invalid Date!')

Now we can introduce more tests

if __name__ == '__main__':
    assert True == is_valid_date_string("01/01/2020")

    # false
    assert False == is_valid_date_string("00/01/2020")
    assert False == is_valid_date_string("01/00/2020")
    assert False == is_valid_date_string("01/01/0000")
    assert False == is_valid_date_string("31/04/2020")
    assert False == is_valid_date_string("30/02/2020")
    assert False == is_valid_date_string("31/02/2020")

    # leap
    assert False == is_valid_date_string("29/02/2001")
    assert True == is_valid_date_string("29/02/2004")
    assert False == is_valid_date_string("29/02/2100")
    assert True == is_valid_date_string("29/02/2400")

    # format
    assert False == is_valid_date_string("asdf")
    assert False == is_valid_date_string("1/2/2020")

We see two cases failing. Which part is responsible? Regex or check_date? check_date does not check any date but the upper limit of days only. Hence either the name is wrong or the implementation. check_date does silently assume its parameters are somewhat correct which may! not be enforced by the usage. This real danger! We also cannont test the function properly and we definitely shall not expose it. A user might get date as integer triple from somewhere and use your function to verify like

check_date(5, 90, 999)

We also cannot test it as we do not know the contracts

assert False == check_date(5, 90, 999)
assert False == check_date(35, 9, 999)

One fails, the other test is successful.

We shall incorporate the code into our is_valid_date_string function directly (or as private function).

Bottom line - do not mess with date/time manually, use a library

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.