2
\$\begingroup\$

its a practice project from "Automate the Boring stuff with Python" book. i am an intermediate level Python programmer and i tried to solve this problem with less code as possible. This code will not take any wrong date into consideration eg: 29-02-2002 will not be selected because 2002 is not a leap year and only leap years have 29th of feb. i did not add code to also detect dates with months written in words, i could do that too but i want to keep things simple for now and i also did not use pyperclip module to detect dates from copied text to clipboard because i dont want to confuse any beginner who also want to learn from watching my code. I want master programmers to review my code and if their is another way possible to detect dates then please post your solutions. Also i would appreciate any advice and positive criticism, so i know where i am standing right now and what i need to improve. Thanks. Code is as follows:

    import re


    def date_detector(text):
        date_pattern = re.compile('''
        ([12][0-9]|3[0-1]|0?[1-9])             # to detect days from 1 to 31
        ([./-])                                # to detect different separations
        (1[0-2]|0?[1-9])                       # to detect number of months
        ([./-])                                # to detect different seperations
        (2?1?[0-9][0-9][0-9])                  # to detect number of years from 1000-2999 years
         ''', re.VERBOSE)

        days = []
        months = []
        years = []
        dates = []
        for date in date_pattern.findall(text):
            days.append(int(date[0]))
            months.append(int(date[2]))
            years.append(int(date[4]))

        for num in range(len(days)):

        # appending dates in a list that dont need any filtering to detect wrong dates
            if months[num] not in (2, 4, 6, 9, 11):
                dates.append([days[num], months[num], years[num]])

        # detecting those dates with months that have only 30 days
            elif days[num] < 31 and months[num] in (4, 6, 9, 11):
                dates.append([days[num], months[num], years[num]])

        # filtering leap years with Feb months that have 29 days
            elif months[num] == 2 and days[num] == 29:
                if years[num] % 4 == 0:
                    if years[num] % 100 == 0:
                        if years[num] % 400 == 0:
                            dates.append([days[num], months[num], years[num]])
                    else:
                        dates.append([days[num], months[num], years[num]])

        # appending Feb dates that have less than 29 days
            elif months[num] == 2 and days[num] < 29:
                dates.append([days[num], months[num], years[num]])

        if len(dates) > 0:
            for date in dates:
                print(date)


data = '30-06-2012, 31-12-2012, 15-02-2002, 29-02-2004, 29-02-2002, 31-02-2004, 31-06-2012'

date_detector(data)
```
\$\endgroup\$
2
\$\begingroup\$

I suggest some minor improvements in the regular expression:

  • make sure that the same separator is used between day and month and between month and year with a backreference (?P=sep),
  • replace numbered capture groups with named, and make non-needed groups, if there wer any, non-capturing with ?:. Consequently, finditer and groupdict are used, and the day is obtained from the match with int(date['day']), etc. This will make the code somewhat more human.

More importantly, I suggest that you get rid of days, months and years lists altogether. These data can be stored in dictionaries in dates list and filtered prior to appending to dates.

As a consequence, you won't need a loop over range(len(days)).

The validation conditions can be OR'ed together without losing clarity, and I propose to make it a separate function date_is_valid(day: int, month: int, year: int) -> bool.

Also, the only parametre in date_detector can be made typed: def date_detector(text: str):.

To sum suggested modifications up:

import re

def date_is_valid(day: int, month: int, year: int) -> bool:
    return (month not in (2, 4, 6, 9, 11)   # 31 days in month (Jan, Mar, May, Jul, Aug, Oct, Dec).
    or day < 31 and month in (4, 6, 9, 11)  # 30 days in month (Feb, Apr, Jun, Sep, Nov).
    or month == 2 and day == 29 and year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
                                            # February, 29th in a Gregorian leap year.
    or month == 2 and day < 29)             # February, 1st-28th.

def date_detector(text: str):
    date_pattern = re.compile('''
    (?P<day>[12][0-9]|3[0-1]|0?[1-9])   # to detect days from 1 to 31
    (?P<sep>[./-])                      # to detect different separations
    (?P<month>1[0-2]|0?[1-9])           # to detect number of months
    (?P=sep)                            # to detect different seperations
    (?P<year>2?1?[0-9][0-9][0-9])       # to detect number of years from 1000-2999 years
     ''', re.VERBOSE)

    dates = []
    for match in date_pattern.finditer(text):
        date = match.groupdict()                            # convert Match object to dictionary.
        del date['sep']                                     # we don't need the separator any more.
        date = {key: int(val) for key, val in date.items()} # apply int() to all items.
        
        if date_is_valid(date['day'], date['month'], date['year']):
            dates.append(date)

    if len(dates) > 0:
        for date in dates:
            print(date)

data = '30-06-2012, 31-12-2012, 15-02-2002, 29-02-2004, 29-02-2002, 31-02-2004, 31-06-2012'

date_detector(data)
```
\$\endgroup\$
2
\$\begingroup\$

I know this is a part of an exercise, but it feels like a lot of wheel reinventing where you can leverage builtin Python capabilities for date validation:

from datetime import date

>>> date(2020, 2, 29) # leap year date works
datetime.date(2020, 2, 29)

>>> date(2002, 2, 29) # non-leap year will raise ValueError
ValueError: day is out of range for month

>>> date(2002, 9, 31) # 31th day will raise ValueError
ValueError: day is out of range for month
  • instead of creating 3 separate lists for years, months and days, you can create only one list, since you always access these parts at the same index. That also simplifies the for loop which gives you the values directly instead of giving you an index you want to access in these lists.

  • Python is a dynamic language where empty collections are evaluated to False, so when you want to check if a list has any items, you don't have to do it explicitly via if len(list) > 0, but you can do if list:. For purpose of printing items in list, you can take it one step further and omit the condition completely as iterating through an empty list wont print anything. Snippet before/after:

# before
if len(dates) > 0:
    for date in dates:
        print(date)

# after
for date in dates:
    print(date)

all suggestion applied:

import re
from datetime import date

def date_detector(text):
    date_pattern = re.compile('''
    ([12][0-9]|3[0-1]|0?[1-9])             # to detect days from 1 to 31
    ([./-])                                # to detect different separations
    (1[0-2]|0?[1-9])                       # to detect number of months
    ([./-])                                # to detect different seperations
    (2?1?[0-9][0-9][0-9])                  # to detect number of years from 1000-2999 years
        ''', re.VERBOSE)

    # use only one list for storing all parts of match together
    parsed = []
    for match in date_pattern.findall(text):
        # year, month, day for easier passing to date()
        parsed.append([ int(match[4]), int(match[2]), int(match[0])] )

    valid = [] 
    for item in parsed:
        try:
            # pass list of [year, month, day] to date() and let it check its validity for us
            date(*item)
        except ValueError as e:
            pass # invalid date, dont do anything
        else:
            valid.append(item)

    for item in valid:
        print(item)


data = '30-06-2012, 31-12-2012, 15-02-2002, 29-02-2004, 29-02-2002, 31-02-2004, 31-06-2012'

date_detector(data)
  • this can be simplified further by merging both for loops together, so you it does not iterate through the collection of data twice needlessly.
\$\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.