How to get the week number of the current quarter in Python?

旧城冷巷雨未停 提交于 2021-02-10 14:54:26

问题


I've been through every question and every third party library trying to figure out a way to do this where I don't have to manually map dates.

I'm trying to get the week number of the current fiscal quarter. Each quarter starts on the 1st of either January, April, July or October.

Given a date (string or object, it doesn't matter), I need to be able to calculate the week number of the fiscal quarter that it's in.

To make matters a little more complicated, the Fiscal year starts in April.

So for example, today, July 9th 2020 is week 2 of this Fiscal quarter (Q2), because the quarter starts in April. Similarly, the 29 and 30th of June 2020 are week 14 of quarter 1.

Most time formatting libraries and even the standard library ones have methods like ISO date where I can extract the week number fine. But it's the week number from the 1st day of the year.

I can't use arithmetic to simply remove the number of weeks to the current date as there are a different number of weeks in each quarter. Quarters can have 12, 13 or 14 weeks depending on the year.

The closest I've gotten is using the FiscalYear library which is great as it has a Fiscal Quarter class with it. Unfortunately, the inherited method isoformat() doesn't apply to it. Only the FiscalDate class, which doesn't give me the quarter which I need.

Has anyone run into this? Can someone point me in the right direction?

I'd post code snippets but it's just the 100 ways there are in Python to get the current week number (as of today, that's 28).

I've tried using rrules and deltas in dateutils but the closest I can get is the week number of the 1st quarter using offsets. The second quarter, it falls apart.

I'm happy to use pandas or any other 3rd party library if it will help me avoid hard coding the quarter dates or, god forbid, the week number to dates mappings.

Any help in the right direction would be very much appreciated.


Edit: All three answers below solved this issue for me in different ways. I struggled with which one to give the correct answer to, but I gave it to @Paul's answer as it was the one that I could follow most as someone who isn't a senior. It was also the answer that fit in with my personal use case (that I didn't mention), which was receiving a datetime object and getting the results. So that gave it the edge. Sorry to the others who provided amazing answers. I'm thrilled to have gotten the code what all I was hoping for was a nudge in the right direction. Thank you all.


回答1:


Unless this is a very common way to reckon week numbering, I don't know if you are going to find a library that will do this exactly for you, but it's easy enough to accomplish using dateutil's relativedelta and a little logic. Here's a simple implementation that returns a tuple (quarter, week). Since you said that Q1 starts April 1st, I am assuming that the period from January 1st to April 1st is called Q0:

from datetime import date, datetime, timedelta
import typing

from dateutil import relativedelta

NEXT_MONDAY = relativedelta.relativedelta(weekday=relativedelta.MO)
LAST_MONDAY = relativedelta.relativedelta(weekday=relativedelta.MO(-1))
ONE_WEEK = timedelta(weeks=1)


def week_in_quarter(dt: datetime) -> typing.Tuple[int, int]:
    d: date = dt.date()
    year = d.year

    # Q0 = January 1, Q1 = April 1, Q2 = July 1, Q3 = October 1
    quarter = ((d.month - 1) // 3)
    quarter_start = date(year, (quarter * 3) + 1, 1)
    quarter_week_2_monday = quarter_start + NEXT_MONDAY

    if d < quarter_week_2_monday:
        week = 1
    else:
        cur_week_monday = d + LAST_MONDAY
        week = int((cur_week_monday - quarter_week_2_monday) / ONE_WEEK) + 2

    return quarter, week

Which returns:

$ python week_in_quarter.py 
2020-01-01: Q0-W01
2020-02-01: Q0-W05
2020-02-29: Q0-W09
2020-03-01: Q0-W09
2020-06-30: Q1-W14
2020-07-01: Q2-W01
2020-09-04: Q2-W10
2020-12-31: Q3-W14

If I've misunderstood the first quarter of the calendar year, and it's actually the case that January 1–April 1 of year X is considered Q4 of year X-1, then you can change the return quarter, week line at the end to this (and change the return type annotation):

if quarter == 0:
    year -= 1
    quarter = 4

return year, quarter, week

Which changes the return values to:

$ python week_in_quarter.py 
2020-01-01: FY2019-Q4-W01
2020-02-01: FY2019-Q4-W05
2020-02-29: FY2019-Q4-W09
2020-03-01: FY2019-Q4-W09
2020-06-30: FY2020-Q1-W14
2020-07-01: FY2020-Q2-W01
2020-09-04: FY2020-Q2-W10
2020-12-31: FY2020-Q3-W14

If this is something that is a speed bottleneck, it should probably be easy to write an optimized version of this that does not use dateutil.relativedelta, but instead calculates this based on day of week, day of year and whether or not this is a leap year (calendar calculations in Python usually go faster if you can turn it into integer operations as early in the process as possible), but I suspect that in most cases this version should be the easiest to read and understand.

If you would like to avoid the dependency on dateutil, you can replace NEXT_MONDAY and LAST_MONDAY with simple functions:

def next_monday(dt: date) -> date:
    weekday = dt.weekday()
    return dt + timedelta(days=(7 - weekday) % 7)

def last_monday(dt: date) -> date:
    weekday = dt.weekday()
    return dt - timedelta(days=weekday)

In which case you would assign the two _monday variables as quarter_week_2_monday = next_monday(quarter_start) and cur_week_monday = last_monday(dt), respectively.

As a note: if I were writing this function, I'd probably not have it return a bare tuple of integers, but instead use attrs or a dataclass to create a simple class for the purpose, like so:

import attr

@attr.s(auto_attribs=True, frozen=True, slots=True)
class QuarterInWeek:
    year: int
    quarter: int
    week: int

    def __str__(self):
        return f"FY{self.year}-Q{self.quarter}-W{self.week:02d}"

(Note that slots=True is optional, and I think not available if you use dataclasses.dataclass instead — it's just that this is a simple struct and I tend to use slots classes for simple structs).




回答2:


I think this does what you need (or at least a very good start):

import datetime as dt

def quarter(date):
    return (date.month-1)//3 + 1 
    
def week_in_q(d):
    year=d.year
    soq={1:dt.date(year,1,1),
         2:dt.date(year,4,1),
         3:dt.date(year,7,1),
         4:dt.date(year,10,1)}
    for i, sow in enumerate(soq[quarter(d)]+dt.timedelta(weeks=x) for x in range(5*3)):
        if sow>=d: 
            return i+1
date=dt.date(2020, 1, 1)    

for d in (date+dt.timedelta(weeks=x) for x in range(53)):
    print(f"date: {d}, quarter: {quarter(d)}, week in that quarter: {week_in_q(d)}")

Prints:

date: 2020-01-01, quarter: 1, week in that quarter: 1
date: 2020-01-08, quarter: 1, week in that quarter: 2
date: 2020-01-15, quarter: 1, week in that quarter: 3
date: 2020-01-22, quarter: 1, week in that quarter: 4
date: 2020-01-29, quarter: 1, week in that quarter: 5
date: 2020-02-05, quarter: 1, week in that quarter: 6
date: 2020-02-12, quarter: 1, week in that quarter: 7
date: 2020-02-19, quarter: 1, week in that quarter: 8
date: 2020-02-26, quarter: 1, week in that quarter: 9
date: 2020-03-04, quarter: 1, week in that quarter: 10
date: 2020-03-11, quarter: 1, week in that quarter: 11
date: 2020-03-18, quarter: 1, week in that quarter: 12
date: 2020-03-25, quarter: 1, week in that quarter: 13
date: 2020-04-01, quarter: 2, week in that quarter: 1
date: 2020-04-08, quarter: 2, week in that quarter: 2
date: 2020-04-15, quarter: 2, week in that quarter: 3
date: 2020-04-22, quarter: 2, week in that quarter: 4
date: 2020-04-29, quarter: 2, week in that quarter: 5
date: 2020-05-06, quarter: 2, week in that quarter: 6
date: 2020-05-13, quarter: 2, week in that quarter: 7
date: 2020-05-20, quarter: 2, week in that quarter: 8
date: 2020-05-27, quarter: 2, week in that quarter: 9
date: 2020-06-03, quarter: 2, week in that quarter: 10
date: 2020-06-10, quarter: 2, week in that quarter: 11
date: 2020-06-17, quarter: 2, week in that quarter: 12
date: 2020-06-24, quarter: 2, week in that quarter: 13
date: 2020-07-01, quarter: 3, week in that quarter: 1
date: 2020-07-08, quarter: 3, week in that quarter: 2
date: 2020-07-15, quarter: 3, week in that quarter: 3
date: 2020-07-22, quarter: 3, week in that quarter: 4
date: 2020-07-29, quarter: 3, week in that quarter: 5
date: 2020-08-05, quarter: 3, week in that quarter: 6
date: 2020-08-12, quarter: 3, week in that quarter: 7
date: 2020-08-19, quarter: 3, week in that quarter: 8
date: 2020-08-26, quarter: 3, week in that quarter: 9
date: 2020-09-02, quarter: 3, week in that quarter: 10
date: 2020-09-09, quarter: 3, week in that quarter: 11
date: 2020-09-16, quarter: 3, week in that quarter: 12
date: 2020-09-23, quarter: 3, week in that quarter: 13
date: 2020-09-30, quarter: 3, week in that quarter: 14
date: 2020-10-07, quarter: 4, week in that quarter: 2
date: 2020-10-14, quarter: 4, week in that quarter: 3
date: 2020-10-21, quarter: 4, week in that quarter: 4
date: 2020-10-28, quarter: 4, week in that quarter: 5
date: 2020-11-04, quarter: 4, week in that quarter: 6
date: 2020-11-11, quarter: 4, week in that quarter: 7
date: 2020-11-18, quarter: 4, week in that quarter: 8
date: 2020-11-25, quarter: 4, week in that quarter: 9
date: 2020-12-02, quarter: 4, week in that quarter: 10
date: 2020-12-09, quarter: 4, week in that quarter: 11
date: 2020-12-16, quarter: 4, week in that quarter: 12
date: 2020-12-23, quarter: 4, week in that quarter: 13
date: 2020-12-30, quarter: 4, week in that quarter: 14



回答3:


Here is a simple solution using python's isocalendar library to find the week number:

Note: Week starts on Monday.

from datetime import datetime

FISCAL_QUARTERS = [4, 7, 10, 1]  # April, July, October, January
FISCAL_PERIOD = 3

def _calc_quarter_week(day, month, year):
    fiscal_quarter = None
    # Find which quarter the given date falls in
    for fiscal_index in range(len(FISCAL_QUARTERS)):
        f_month = FISCAL_QUARTERS[fiscal_index]
        if month >= f_month and month < f_month + FISCAL_PERIOD:
            fiscal_quarter = fiscal_index + 1
            break

    quarter_start_day = datetime(
        year=year, month=FISCAL_QUARTERS[fiscal_quarter-1], day=1)
    # Quarter week number
    _, q_week_no, _ = quarter_start_day.isocalendar()

    given_date = datetime(year=year, month=month, day=day)
    # Given week number
    _, given_week_no, _ = given_date.isocalendar()

    return fiscal_quarter, given_week_no - q_week_no + 1


day, month, year = map(int, input('Day Month Year\n').strip().split())
fiscal_quarter, week_count = _calc_quarter_week(day, month, year)
print('Fiscal quarter: {}, Week: {}'.format(fiscal_quarter, week_count))

Outputs:

Day Month Year
29 6 2020
Fiscal quarter: 1, Week: 14
Day Month Year
9 7 2020
Fiscal quarter: 2, Week: 2


来源:https://stackoverflow.com/questions/62819334/how-to-get-the-week-number-of-the-current-quarter-in-python

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!