如何在Python中获取当前季度的周数?

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

我已经解决了每一个问题和每一个第三方图书馆,试图找出一种方法来做到这一点,而我不必手动映射日期。

我正在尝试获取当前财政季度的周数。 每个季度从 1 月、4 月、7 月或 10 月的 1 日开始。

给定一个日期(字符串或对象,无关紧要),我需要能够计算出它所在的财政季度的周数。

让事情变得有点复杂,财政年度从 4 月开始。

例如,今天 2020 年 7 月 9 日是本财政季度 (Q2) 的第 2 周,因为该季度从 4 月开始。同样,2020 年 6 月 29 日和 30 日是第 1 季度的第 14 周。

大多数时间格式化库甚至标准库都有像 ISO 日期这样的方法,我可以在其中提取周数。但它是从一年的第一天开始的周数。

我不能使用算术简单地删除到当前日期的周数,因为每个季度的周数不同。季度可以有 12、13 或 14 周,具体取决于年份。

我得到的最接近的是使用 FiscalYear 库,它很棒,因为它有一个财政季度 class。不幸的是,继承的方法 isoformat() 不适用于它。只有 FiscalDate class,它没有提供我需要的季度。

有人 运行 参与其中吗?有人能指出我正确的方向吗?

我会 post 代码片段,但这只是 Python 中获取当前周数(截至今天,为 28)的 100 种方法。

我试过在 dateutils 中使用 rrules 和 deltas,但我能得到的最接近的是使用偏移量的第一季度的周数。第二季,分崩离析

我很乐意使用 pandas 或任何其他第 3 方库,如果它能帮助我避免对季度日期或周数到日期映射进行硬编码的话。

任何正确方向的帮助将不胜感激。


编辑:以下所有三个答案都以不同的方式为我解决了这个问题。我纠结于要给出正确答案,但我把它给了@Paul 的答案,因为它是我作为非大四学生最能理解的答案。这也是适合我个人用例(我没有提到)的答案,它正在接收日期时间对象并获得结果。所以这给了它优势。对提供惊人答案的其他人表示抱歉。我很高兴得到代码,我所希望的只是朝着正确的方向轻推。谢谢大家

我认为这可以满足您的需求(或者至少是一个很好的开始):

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)}")

打印:

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

这是使用 python 的 isocalendar 库查找周数的简单解决方案:

注意:一周从星期一开始。

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))

输出:

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

除非这是计算周编号的一种非常常见的方法,否则我不知道您是否会找到一个可以完全为您执行此操作的库,但使用 dateutil 很容易完成的 relativedelta 和一点逻辑。这是一个简单的实现,return 是一个元组 (quarter, week)。既然你说 Q1 从 4 月 1 日开始,我假设从 1 月 1 日到 4 月 1 日的时间段称为 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

哪个 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

如果我误解了日历年的第一季度,实际上 X 年的 1 月 1 日至 4 月 1 日被认为是 X-1 年的第 4 季度,那么您可以更改 return quarter, week 最后一行(并更改 return 类型注释):

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

return year, quarter, week

将 return 值更改为:

$ 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

如果这是速度瓶颈,那么编写一个不使用 dateutil.relativedelta 的优化版本应该很容易,而是根据星期几、一年中的某一天来计算以及这是否是闰年(Python 中的日历计算通常会更快,如果你能尽早将其转换为整数运算),但我怀疑在大多数情况下这个版本应该是最容易阅读和理解。

如果你想避免对dateutil的依赖,你可以用简单的函数替换NEXT_MONDAYLAST_MONDAY

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)

在这种情况下,您可以将两个 _monday 变量分别指定为 quarter_week_2_monday = next_monday(quarter_start)cur_week_monday = last_monday(dt)

注意:如果我正在编写这个函数,我可能不会 return 一个纯整数元组,而是使用 attrs or a dataclass 创建一个简单的 class 的目的,像这样:

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}"

(请注意,slots=True 是可选的,我认为如果您改用 dataclasses.dataclass 则不可用 — 只是这是一个简单的结构,我倾向于使用插槽 class es 用于简单结构)。