18

I've built a function to get the first and last day of the current quarter but it's a bit long winded. I was wondering, is there a more succinct way of accomplishing this?

I understand that pandas has a QuarterBegin() function, but I couldn't implement it in a more concise way.

import datetime as dt
from dateutil.relativedelta import relativedelta     

def get_q(first=None,last=None):

    today = dt.date.today()

    qmonth = [1, 4, 7, 10]

    if first:
        for i,v in enumerate(qmonth):
            if (today.month-1)//3 == i:
                return dt.date(today.year,qmonth[i],1).strftime("%Y-%m-%d")

    if last:
        firstday = dt.datetime.strptime(get_q(first=True),"%Y-%m-%d") 
        lastday = firstday + relativedelta(months=3, days=-1)
        return lastday.strftime("%Y-%m-%d")

EDIT: Please let me know if this would be better suited to Code Review

Community
  • 1
  • 1
Charon
  • 2,344
  • 6
  • 25
  • 44

6 Answers6

21

Why roll your own?

import pandas as pd

quarter_start = pd.to_datetime(pd.datetime.today() - pd.tseries.offsets.QuarterBegin(startingMonth=1)).date()
John Redford
  • 459
  • 5
  • 6
  • 2
    And how to calculate current quarter end date? – yasin mohammed Jul 11 '17 at 12:33
  • 8
    Change QuarterBegin to QuarterEnd and add instead of subtract – John Redford Jul 19 '17 at 19:25
  • 4
    Thanks made one change also in starting month = 3 – yasin mohammed Jul 24 '17 at 10:12
  • 4
    The above code now produces this warning: `FutureWarning: The pandas.datetime class is deprecated and will be removed from pandas in a future version. Import from datetime module instead.`. As such, I believe that the below achieves the same thing without relying on the pandas.datetime class: ```import pandas as pd (datetime.today() - pd.tseries.offsets.QuarterBegin(startingMonth=1)).date()``` – followingell Jul 13 '21 at 09:18
12

Why so complicated :-)

from datetime import date
from calendar import monthrange

quarter = 2
year = 2016
first_month_of_quarter = 3 * quarter - 2
last_month_of_quarter = 3 * quarter
date_of_first_day_of_quarter = date(year, first_month_of_quarter, 1)
date_of_last_day_of_quarter = date(year, last_month_of_quarter, monthrange(year, last_month_of_quarter)[1])
oxidworks
  • 1,563
  • 1
  • 14
  • 37
5

You can do it this way:

import bisect
import datetime as dt

def get_quarter_begin():
    today = dt.date.today()

    qbegins = [dt.date(today.year, month, 1) for month in (1,4,7,10)]

    idx = bisect.bisect(qbegins, today)
    return str(qbegins[idx-1])

This solves the "first" case; I'm leaving the "last" case as an exercise but I suggest keeping it as an independent function for clarity (with your original version it's pretty strange what happens if no arguments are passed!).

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • it would make the function more general if you did return return a date object instead of a string. – jfs Apr 23 '16 at 09:22
  • @J.F.Sebastian: I totally agree--I sort of modeled this after what the OP did. But yes, removing the `str()` call in the return statement would be preferable to me too. – John Zwinck Apr 23 '16 at 11:43
  • unrelated: for a small `N` (such as in this case) a linear search could be used instead of the binary search: `return next(date(today.year, month, 1) for month, next_month in zip(quarter_months, quarter_months[1:]) if today.month < next_month)` where `quarter_months = [1, 4, 7, 10, 13]`. Though there might be no point in micro-optimizations here—any readable solution will do. – jfs Apr 23 '16 at 11:54
  • @JohnZwinck - this is a really interesting and elegant answer - thanks for posting. I had never heard of the `bisect` module before. I think I can figure out how to accomplish and equivalent for 'last' :) Just out of interest, what's the problem with having the two capabilities in one function, and also what's the problem if my function returns nothing when no args are passed? – Charon Apr 26 '16 at 20:32
  • @Charon: A function that takes parameters to produce totally different functionality is usually not as clear as two separate functions. For example Python has built-in `min()` and `max()`, not `minormax(min=False, max=False)`. – John Zwinck Apr 27 '16 at 02:32
  • @JohnZwinck - Thanks, that makes sense. I suppose that removing the parametrisation will also shorten the code when the function is called. – Charon Apr 27 '16 at 07:54
5

You shouldn't need to use unnecessary loops or large libraries like pandas to do this. You can do it with simple integer division / arithmetic and just the datetime library (although using dateutil results in cleaner code).

import datetime

def getQuarterStart(dt=datetime.date.today()):
    return datetime.date(dt.year, (dt.month - 1) // 3 * 3 + 1, 1)

# using just datetime
def getQuarterEnd1(dt=datetime.date.today()):
    nextQtYr = dt.year + (1 if dt.month>9 else 0)
    nextQtFirstMo = (dt.month - 1) // 3 * 3 + 4
    nextQtFirstMo = 1 if nextQtFirstMo==13 else nextQtFirstMo
    nextQtFirstDy = datetime.date(nextQtYr, nextQtFirstMo, 1)
    return nextQtFirstDy - datetime.timedelta(days=1)

# using dateutil
from dateutil.relativedelta import relativedelta

def getQuarterEnd2(dt=datetime.date.today()):
    quarterStart = getQuarterStart(dt)
    return quarterStart + relativedelta(months=3, days=-1)

Output:

>>> d1=datetime.date(2017,2,15)
>>> d2=datetime.date(2017,1,1)
>>> d3=datetime.date(2017,10,1)
>>> d4=datetime.date(2017,12,31)
>>> 
>>> getQuarterStart(d1)
datetime.date(2017, 1, 1)
>>> getQuarterStart(d2)
datetime.date(2017, 1, 1)
>>> getQuarterStart(d3)
datetime.date(2017, 10, 1)
>>> getQuarterStart(d4)
datetime.date(2017, 10, 1)
>>> getQuarterEnd1(d1)
datetime.date(2017, 3, 31)
>>> getQuarterEnd1(d2)
datetime.date(2017, 3, 31)
>>> getQuarterEnd1(d3)
datetime.date(2017, 12, 31)
>>> getQuarterEnd1(d4)
datetime.date(2017, 12, 31)
>>> getQuarterEnd2(d1)
datetime.date(2017, 3, 31)
>>> getQuarterEnd2(d2)
datetime.date(2017, 3, 31)
>>> getQuarterEnd2(d3)
datetime.date(2017, 12, 31)
>>> getQuarterEnd2(d4)
datetime.date(2017, 12, 31)
4
  • avoid Yo-Yo code. Don't convert a date object to a string only to parse it back into a date object again immediately like if last branch does in the question. Instead, return a date object and convert to string only when necessary (it is easier to access object attributes such as .year, .month than to parse its string representation to extract the same info)
  • avoid mutually exclusive boolean keywords arguments (first, last). It is an error-prone interface and it breeds code duplication. It is easy to return both results here and access corresponding attributes such as .first_day later. Or if there are (unlikely) performance issues; you could create two functions such as get_first_day_of_the_quarter() instead
  • to simplify algorithm, you could add a little redundancy to the input data e.g., see quarter_first_days in the code below (1 is mentioned twice in the list of months)—it allow to use i+1 unconditionally:
#!/usr/bin/env python
from collections import namedtuple
from datetime import MINYEAR, date, timedelta

DAY = timedelta(1)
quarter_first_days = [date(MINYEAR+1, month, 1) for month in [1, 4, 7, 10, 1]]    
Quarter = namedtuple('Quarter', 'first_day last_day')

def get_current_quarter():
    today = date.today()
    i = (today.month - 1) // 3 # get quarter index
    days = quarter_first_days[i], quarter_first_days[i+1] - DAY
    return Quarter(*[day.replace(year=today.year) for day in days])

MINYEAR+1 is used to accommodate - DAY expression (it assumes MINYEAR < MAXYEAR). The quarter index formula is from Is there a Python function to determine which quarter of the year a date is in?

Example:

>>> get_current_quarter()
Quarter(first_day=datetime.date(2016, 4, 1), last_day=datetime.date(2016, 6, 30))
>>> str(get_current_quarter().last_day)
'2016-06-30'
Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • Thanks for posting this answer. I can see that it would be better to simply work with `date` objects and then convert them to strings only as and when necessary. I think I will combine your answer with the one above to make my function. – Charon Apr 26 '16 at 20:37
0

Use Pandas:

import pandas as pd
# current quarter start
pd.date_range(end=pd.Timestamp.now(), periods=1, freq='QS')

# current quarter end
pd.date_range(start=pd.Timestamp.now(), periods=1, freq='Q')
ScottC
  • 3,941
  • 1
  • 6
  • 20
Weiwei
  • 11