Select to view content in your preferred language

Calclulate dates more efficiently

3308
6
Jump to solution
09-12-2023 02:26 PM
clt_cabq
Occasional Contributor III

I have a function that creates a start and end date which I pass into a couple of other procedures when running a monthly update. The start and end dates demark a one year period, starting on the first of the  month of last year, and ending on the last day of the prior month this year - so if I am running the update today the start/ends would be 9/1/2022 and 8/31/2023, respectively. I feel my approach, though it works, is a bit ham fisted and thought I'd ask if there is a more elegant way to accomplish this:

import datetime, calendar
def calc_dates():
    thisday = datetime.date.today() # gets todays date, source for finding current and last month and year
    thismonth = thisday.month
    lastmonth = thisday.month -1
    thisyear = thisday.year
    lastyear = thisyear - 1
    last_m_days = calendar.monthrange(thisyear,lastmonth)[1] #gets the number of days in the previous month from when the script is run
    enddate = f'{lastmonth}/{last_m_days}/{thisyear}'
    startdate = f'{thismonth}/1/{lastyear}'
    dates = [enddate,startdate]
    return dates
Tags (2)
1 Solution

Accepted Solutions
HaydenWelch
Occasional Contributor II

The python datetime module has a timedelta object that is what you're looking for, just specify a delta of 365 days and add it to your start date.

datetime.timedelta 

 

def calc_dates(date:datetime=datetime.now()):
    this_year = date - timedelta(days=date.day-1)
    one_year = this_year + timedelta(days=364)
    return (this_year.strftime("%m/%d/%y"), one_year.strftime("%m/%d/%y"))

 

That function is pretty elegant and returns good data so far. To test it you can run this bit of code:

 

for i in range(52):
    print(calc_dates(datetime.now()-timedelta(weeks=i)))

 

Which will give you the paired start/end dates for the past year.

 Here's a version that takes leap years into account:

 

from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

def calc_dates(date:datetime=datetime.now()):
    this_year = date - timedelta(days=date.day-1)
    one_year = this_year + relativedelta(years=1) - timedelta(days=1)
    return (this_year.strftime("%m/%d/%y"), one_year.strftime("%m/%d/%y"))

 

View solution in original post

6 Replies
DanPatterson
MVP Esteemed Contributor

Not much you can do, but have a look at timetuple

def calc_dates():
    # gets todays date, source for finding current and last month and year
    tt = datetime.date.today().timetuple()
    thisyear, thismonth, thisday = tt[:3]
    lastmonth = thisday -1
    lastyear = thisyear - 1
    #gets the number of days in the previous month from when the script is run
    last_m_days = calendar.monthrange(thisyear,lastmonth)[1] 
    enddate = f'{lastmonth}/{last_m_days}/{thisyear}'
    startdate = f'{thismonth}/1/{lastyear}'
    dates = [enddate,startdate]
    return dates
    

calc_dates()
Out[16]: ['11/30/2023', '9/1/2022']

... sort of retired...
HaydenWelch
Occasional Contributor II

The python datetime module has a timedelta object that is what you're looking for, just specify a delta of 365 days and add it to your start date.

datetime.timedelta 

 

def calc_dates(date:datetime=datetime.now()):
    this_year = date - timedelta(days=date.day-1)
    one_year = this_year + timedelta(days=364)
    return (this_year.strftime("%m/%d/%y"), one_year.strftime("%m/%d/%y"))

 

That function is pretty elegant and returns good data so far. To test it you can run this bit of code:

 

for i in range(52):
    print(calc_dates(datetime.now()-timedelta(weeks=i)))

 

Which will give you the paired start/end dates for the past year.

 Here's a version that takes leap years into account:

 

from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

def calc_dates(date:datetime=datetime.now()):
    this_year = date - timedelta(days=date.day-1)
    one_year = this_year + relativedelta(years=1) - timedelta(days=1)
    return (this_year.strftime("%m/%d/%y"), one_year.strftime("%m/%d/%y"))

 

BlakeTerhune
MVP Regular Contributor

You could also use dateutil.relativedelta for clearer looking adjustments and datetime.strftime to format the dates as strings.

 

import datetime
from dateutil.relativedelta import relativedelta
import calendar

def calc_dates():
    thisday = datetime.date.today()
    lastmonth = thisday - relativedelta(months=1)
    lastyear = thisday - relativedelta(years=1)
    last_m_days = calendar.monthrange(lastmonth.year, lastmonth.month)[1]
    startdate = datetime.datetime(lastyear.year, lastyear.month, 1)
    enddate = datetime.datetime(lastmonth.year, lastmonth.month, last_m_days)
    dates = (startdate.strftime('%m/%d/%Y'), enddate.strftime('%m/%d/%Y'))
    return dates

 

I also recommend a tuple as your return instead of a list so it's immutable. Just seems more semantically correct.

clt_cabq
Occasional Contributor III

Thanks everyone for your responses, great suggestions and perspectives! @BlakeTerhune - I had considered using a tuple for the reason you suggest but just defaulted to a list as i was coding, I think you are correct though that this would be a better practice. @HaydenWelch This is a great solution, I was interested to see how you embedded the today() function as an argument to the function. @DanPatterson I want to look closer at the time tuple module now because that looks like its pretty useful, thanks for pointing it out.

HaydenWelch
Occasional Contributor II

One thing I just noticed with some of the other solutions is that they fail in January. When you subtract 1 from the first month you get the 0th month:

 

IllegalMonthError                         Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_5260\2484461742.py in <cell line: 15>()
     14 
     15 for i in range(50):
---> 16     print(calc_dates(datetime.today()-timedelta(days=i)))

~\AppData\Local\Temp\ipykernel_5260\2484461742.py in calc_dates(date)
      7     lastyear = thisyear -1
      8     #gets the number of days in the previous month from when the script is run
----> 9     last_m_days = calendar.monthrange(thisyear,lastmonth)[1]
     10     enddate = f'{lastmonth}/{last_m_days}/{thisyear}'
     11     startdate = f'{thismonth}/1/{lastyear}'

c:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\lib\calendar.py in monthrange(year, month)
    122        year, month."""
    123     if not 1 <= month <= 12:
--> 124         raise IllegalMonthError(month)
    125     day1 = weekday(year, month, 1)
    126     ndays = mdays[month] + (month == February and isleap(year))

IllegalMonthError: bad month number 0; must be 1-12

 

Something to be aware of if you use that method. The one I submitted is good until you hit the max year. I ran it up to 10000 days and it ran in about 1ms.

@BlakeTerhune's solution gets around this by using the relativedelta object which prevents you from subtracting numbers from months and getting invalid months. Their solution is also very fast and works for 10000 days out with no issues in about the same amount of time

One more update, my leap year calculation misses out on some years. Running it up to 2050 puts the dates on the same day one year apart, here's the function with the updated leapyear modulo operation : (I switched to relativedelta)

 

from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

def calc_dates(date:datetime=datetime.now()):
    this_year = date - timedelta(days=date.day-1)
    one_year = this_year + relativedelta(years=1) - timedelta(days=1)
    return (this_year.strftime("%m/%d/%y"), one_year.strftime("%m/%d/%y"))

for i in range(10000):
    print(calc_dates(datetime.now()+timedelta(days=i)))

 

 

clt_cabq
Occasional Contributor III

@HaydenWelch thanks for the update.. I see the logical issue with calculating the month, that should have been obvious I suppose. I haven't implemented any of this just yet but will take all this into consideration when I do.