10
\$\begingroup\$

So this is my first ever attempt so it's obviously bad. There has to be a better way to do this. A big thing I haven't implemented is trying to guarantee the structure of the input(i.e making sure date is input as mm/dd/yyyy and not just something like 1/1/1), so would like feedback on that. But other than that I just want to see how you guys would do it! Tips, recommendations, have you broken it, etc.

def age_and_delta(b_tries=3):
    tday_day = datetime.date.today().strftime('%m/%d/%Y')[3:5]
    tday_month = datetime.date.today().strftime('%m/%d/%Y')[0:2]
    tday_year = datetime.date.today().strftime('%m/%d/%Y')[6:]
    int_tday_year = int(tday_year)
    int_tday_day = int(tday_day)
    int_tday_month = int(tday_month)
    age_pattern = re.compile("^[0-9/]+$")

    for i in range(b_tries):
        in_birthday = input("What is your birthday? (MM/DD/YYYY) ").strip()
        valid_bday = re.match(age_pattern, in_birthday)
        b_year = in_birthday[6:]
        b_month = in_birthday[0:2]
        b_day = in_birthday[3:5]
        int_b_year = int(b_year)
        int_b_month = int(b_month)
        int_b_day = int(b_day)
        u_age = int_tday_year - int_b_year
        if valid_bday:
            if int_b_month < int_tday_month:
                print(f"You are {u_age} years old.")
                return
            elif int_b_month >= int_tday_month and int_b_day > int_tday_day and int_b_year >= int_tday_year:
                print("You haven't even been born yet!")
                return
            elif int_b_month == int_tday_month and int_tday_day == int_b_day:
                print(f"Happy Birthday!! You are {u_age} years old today!")
                return
            elif int_b_month == int_tday_month and int_b_day <= int_tday_day:
                print(f"You are {u_age} years old.")
                return
            elif int_b_month == int_tday_month and int_b_day > int_tday_day:
                int_tday_year - 1
                print(f"You are {u_age} years old.")
                return
            elif int_b_month >= int_tday_month and int_b_day > int_tday_day:
                int_tday_year - 1
                print(f"You are {u_age} years old.")
                return
        print("Date is not valid")
    print("Maximum number of tries reached. Closing.")

age_and_delta()
\$\endgroup\$
4
  • 1
    \$\begingroup\$ You want feedback on not validating the inputs are real dates? My feedback is that you should do that - users are never to be trusted. \$\endgroup\$ Nov 4, 2022 at 10:32
  • \$\begingroup\$ @TobySpeight thanks for the comment. Ya, that's what I'm getting at. I want to ensure the date format provided is correct, that the month doesn't exceed 12, days are accurate to the given month, all inputs have the appropriate number of characters, and probably more. Does datetime provide the methods for the time aspect? What's a good direction to look in for the input format? All tips are helpful! \$\endgroup\$
    – malibooyah
    Nov 4, 2022 at 17:05
  • \$\begingroup\$ I use the parser functions of the dateutil library in order to convert text to date. You would have to install the library (e.g. with pip) because it is not part of the standard set of libraries you get with every python install. It accepts a wide variety of date formats and raises an exception on invalid input. That is not as useful if your purpose is to learn Python, but later when you are doing things to get a job done, it is good to know about the libraries. \$\endgroup\$
    – DMPalmer
    Nov 6, 2022 at 5:49
  • \$\begingroup\$ Also, you have to watch out for people from countries that use dd/mm/yyyy format, especially for the first 12 days of the month. If you print out the date you think they entered, using the name of the month (%b for 'Jan', %B for 'January') the user can more easily notice that the program expects an American style date. \$\endgroup\$
    – DMPalmer
    Nov 6, 2022 at 6:00

4 Answers 4

20
\$\begingroup\$

datetime provides all the methods you need.

try:
    birthday = datetime.datetime.strptime(date_text, '%Y/%m/%d')
except ValueError:
    raise ValueError("Incorrect data format, should be YYYY/MM/DD.")

now = datetime.date.today()
age = now - birthday

if age < 0:
    print("You haven't even been born yet!")
    return

if now.day == birthday.day and now.month == birthday.month:
    print(f"Happy birthday! You are now {age.years} years old.")
    return

print(f"You are {age.years} years old.")

In general, try searching for a solution before inventing a new one. For simple tasks like this there will most likely be a library that does it for you.


Try to avoid excessive nesting and else statements to make the logic of your code more clear. You can do this by replacing

if something is right:
   ...
   ...
   ...
else:
   ...

with

if something is wrong:
   ...
   return

...
...
...

This is called a guard clause / early return. Now we don't need to keep track of the condition as we dealt with it at the very beginning.

\$\endgroup\$
1
  • \$\begingroup\$ You should probably use raise ValueError("...") from None, otherwise Python will include the original traceback and print During handling of the above exception, another exception occurred: \$\endgroup\$
    – Jasmijn
    Nov 5, 2022 at 11:58
11
\$\begingroup\$

There's a problem here:

tday_day = datetime.date.today().strftime('%m/%d/%Y')[3:5]
tday_month = datetime.date.today().strftime('%m/%d/%Y')[0:2]
tday_year = datetime.date.today().strftime('%m/%d/%Y')[6:]

Most of the time, those three calls to today() will return the same result. Very occasionally, the date will change in between, resulting in a hard-to-reproduce bug. We should make a single call to today(), and use that result consistently throughout.

Using the real date makes for a function that can't easily be unit-tested with "difficult" inputs. Consider separating into two functions:

  1. a function that gets the current date and reads the user's birthdate, which calls
  2. one that accepts two dates and performs the age calculation, returning the message to be printed.

Then we can thoroughly test the calculation function, calling it with many combinations of birth and current dates.

\$\endgroup\$
7
\$\begingroup\$

To generalise @QuasiStellar's answer, most problems can be broken down into two steps: modelling the problem with right datastructures, and then solving it by applying logic. If you get the datastructure right, the solution is often easy: you only have to calculate things which actually need calculating. If you get the datastructure wrong, you end up doing lots of additional calculation to, in effect, create an ephemeral datastructure on top of the one you actually used.

In your original code you start with this:

tday_day = datetime.date.today().strftime('%m/%d/%Y')[3:5]
tday_month = datetime.date.today().strftime('%m/%d/%Y')[0:2]
tday_year = datetime.date.today().strftime('%m/%d/%Y')[6:]
int_tday_year = int(tday_year)
int_tday_day = int(tday_day)
int_tday_month = int(tday_month)

Quite aside from the race condition if today() changes between calls, what we do here is to take one datastructure which represents what you actually want (a date), cast it to another (a string representing a number) and then yet another (an int). Each of these is difficult. If you really wanted the year as an int, datetime.date.today().year is much easier than making strings, indexing them and then casting. The problem you created of getting the parts out of a string is also made harder than it need be: you've added in separators (/) and done nothing with them. That stage could have been written:

month, day, year = datetime.date.today().strftime("%m/%d/%Y").split("/")

Similar considerations apply all the way through.

As @QuasiStellar has pointed out, this problem is actually quite easy to solve if we represent dates as datetime.dates. It happens to be really easy because datetime is a nice library. But it would have been fairly easy to roll our own Date() type, either as a class, or as a namedtuple + some functions ('c style objects'). Doing so lets us encapsulate the hard things about dates (they're not just numbers) and focus on the logic. Our Date type needs to support a few things for this problem:

  • calculating the difference between two Dates (as what? Differences aren't dates (they can have '0' in any field) so we need a DateDifference)
  • getting today as a Date
  • representing the parts of a date (year, month and day)

Now we can write the problem in pseudo-code (a great exercise, particularly for beginners!):

  • represent user input (birthday) as a Date
  • calculate difference between today and birthday
  • (validate input)
  • print year component of difference.

This logic is very easy. It might look something like this:

birthday = as_date(input("Enter your birthday: "))
now = date_today()
age = date_difference(now, birthday)
if total_days(age) <= 0:
    print("You haven't been born yet!")
else:
    print(f"You are {age.years} years old")

Obviously, using datetime for this functionality allows you to do this in about five more lines. As an exercise it might be a good idea to write the functions I've given here (or a Date class if you're comfortable with classes).*

Encapsulation makes reasoning easy

By modelling the problem right, we can write code which tracks the problem, not the limits of our implementation. Of course, the other parts of the problem haven't gone away, which is why I encourage you to write them. But they have nothing to do with calculating birthdays: they're general date-manipulation problems, and they belong in date-manipulation code. As a bonus, that work can be re-used (hence datetime).

*The interface suggested here is not very pythonic and would be quite clunky to use. But it's probably quite easy to see how to write it, and asking how to make it better would be a great journey into python's idea of OOP and 'just works' operations (like comparing a datetime.timedelta with 0). If you do write it, I would use namedtuples for a Date and DateDifference object.

\$\endgroup\$
1
\$\begingroup\$

A lot has already been said, I wanna comment on your regex re.compile("^[0-9/]+$") for matching MM/DD/YYYY. That will match a lot of invalid data. The least you can do is this:

^[0-9]{2}/[0-9]{2}/[0-9]{4}$

You can get more fancy not allowing more than 0, 1 and 2 in thousands, 0 a 1 in tens of months. No more than 0, 1, 2 and 3 in tens of days, etc. But I wouldn't go too far in case you accidentally forbid some valid date since you are validating more later. This is probably still okay and not too convoluted to show possibilities:

^[0-3][0-9]/[0-1][0-9]/[0-2][0-9]{3}$

The other thing you can do is to actually create capture groups so that you extract the data from the string using the regex:

result = re.match("^([0-9]{2})/([0-9]{2})/([0-9]{4})$", your_input)

Now you can access those 3 groups using:

day = result.group(0)

Or even better to use named groups:

result = re.match("^(?P<day>[0-9]{2})/(?P<month>[0-9]{2})/([0-9]{4})$", your_input)
day = result.group("day")

That saves you from doing things like in_birthday[6:].

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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