user776942
user776942

Reputation:

Find the closest quarter-end for a date?

I am trying to find the quarter-end closest to a given date: e.g. the closest quarter-end for 5/27/2014 would be 6/30/2014 and for 2/2/2013 would be 12/31/2012. I have the following but it doesn't give me the expected output for a date like 8/15/2015:

import datetime

tester = datetime.datetime(2015, 8, 15)

calendar_date = datetime.datetime(tester.year - 1, 12, 31)
for dd in [(3, 31), (6, 30), (9, 30), (12, 31)]:
    diff = abs(datetime.datetime(tester.year, dd[0], dd[1]) - tester)
    if diff.days <= 45:
        calendar_date = datetime.datetime(tester.year, dd[0], dd[1])
        break

print tester, calendar_date

I've simplified by just assuming each quarter is 90 days and thus take 1/2 of that at 45 days (is there a better way???) but clearly that doesn't work for 8/16/2015 as it prints:

2015-08-15 00:00:00 2014-12-31 00:00:00

I was expecting 2015-09-30 00:00:00

Upvotes: 1

Views: 2290

Answers (2)

Padraic Cunningham
Padraic Cunningham

Reputation: 180481

You can get away with comparing just two dates, using ind = (dt.month-1) // 3 + 1 to get the index for the current quarter:

def find_qrt(dt):
    qrts = [date(dt.year - 1, 12, 31), date(dt.year, 3, 31),
            date(dt.year, 6, 30), date(dt.year, 9, 30),
            date(dt.year, 12, 31),
            ]
    ind = (dt.month-1) // 3 + 1
    curr_qr, last_qr = qrts[ind], qrts[ind-1]
    return curr_qr if abs(curr_qr - dt) < abs(last_qr - dt) else last_qr

If you want to return the later quarter in the case of a tie as per your example date we just need to use <=:

dt = datetime.date(2015, 8, 15)
from datetime import date

def find_qrt(dt):
    qrts = [date(dt.year - 1, 12, 31), date(dt.year, 3, 31),
            date(dt.year, 6, 30), date(dt.year, 9, 30),
            date(dt.year, 12, 31),
            ]
    ind = (dt.month-1) // 3 + 1
    curr_qr, last_qr = qrts[ind],qrts[ind-1]
    return curr_qr if abs(curr_qr - dt) <= abs(last_qr - dt) else last_qr

print(find_qrt(dt))

The first function will return 2015-06-30 because the earlier date breaks the tie, for the second we get 2015-09-30 as we take the current quarter in the event of a tie.

The first function

Upvotes: 1

dhke
dhke

Reputation: 15398

datetime.timedelta might be negative and diff.days <= 45 is always true for negative time deltas, hence the incorrect result.

You already had a simple solution with the candidates in place. These are

  1. Last quarter of year before target date
  2. All four quarters of the current year

datetime.timedelta objects have relative comparison operators, i.e. they form a total order, which means there's a minimum. As noted in the comments by Padraic Cunningham, you want the candidate with the minimum absolute distance to the target date:

def get_closest_quarter(target):
    # candidate list, nicely enough none of these 
    # are in February, so the month lengths are fixed
    candidates = [
        datetime.date(target.year - 1, 12, 31),
        datetime.date(target.year, 3, 31),
        datetime.date(target.year, 6, 30),
        datetime.date(target.year, 9, 30),
        datetime.date(target.year, 12, 31),
    ]
    # take the minimum according to the absolute distance to
    # the target date.
    return min(candidates, key=lambda d: abs(target - d))

The code here uses datetime.date for simplicity, but it should be easy to generalize to datetime.datetime if necessary.

Upvotes: 2

Related Questions