如何快速给数据库内生成工作日历?Python生成中国节假日工作表

本文最后更新于 2024年8月25日 上午

我们处理一些业务,比如:计算员工请假的时间工作日;就需要数据库内存在一张工作日历,记录调休和节假日。这个时候如何进行操作呢?

嘿嘿

实际上,是有很多的公共接口。但是很多情况下,我们需要在内网环境下使用,这个时候就需要在数据库内生成工作日历表,如果使用频繁,甚至考虑缓存到中间件Redis内。

那么,如何在数据库内生成一个工作日历表呢?

离线日历库

如果只是简单的日历,那么其实系统自带的日历功能,也足够我们使用;比如iOS自带的日历,可以轻松滑动到300年后:

也不知道300年后,我在哪(╯`□′)╯

关键我们需要的是完整的放假表,例如: 2024年的9月14日,因为中秋节调休,周六要上班。所以,我们肯定需要一个工作日历的数据来源。

万恶的调休……

对于中国的节假日,最准确的肯定是中国政府网每年下半年发布次年的节假日和调休表(每次都是第一时间关注又要调休几次、最多要连续上几天的班╳╳○○),比如: 2024年的放假安排

如何获取一个离线的日历库呢?推荐以下几个项目:

项目vsme/chinese-days是有提供调休的JSON文件的:chinese-days.json,内部主要用两个部分:

  • holidays: 放假的日期;
  • workdays: 因节日而调休的日期。

理论上,你可以直接解析这个JSON文件,或者直接使用项目封装好的功能:

1
2
3
4
5
6
7
8
9
10
11
12
<script src="https://cdn.jsdelivr.net/npm/chinese-days/dist/index.min.js"></script>
<script>
// 检查某个日期是否为工作日
console.log(isWorkday('2023-01-01')); // false
// isHoliday 检查某个日期是否为节假日
console.log(isHoliday('2023-01-01')); // true
// 检查某个日期是否为调休日(in lieu day)
// 检查 2024-05-02 返回 `true` 则表示是一个调休日。
console.log(isInLieu('2024-05-02')); // true
// 检查 2024-05-01 返回 `false` 则表示不是一个调休日。
console.log(isInLieu('2024-05-01')); // false
</script>

当然,为了可以生成SQL脚本,我们这里使用LKI/chinese-calendar库,接下来我们就来演示。

演示网站

为了更会演示什么是中国节假日工作表;我利用本文的思路,搭建了一个在线演示网站:

在线演示网站-界面

在线演示网站-结果演示

比如,我们查询20240916,那么可以得到信息,这一天虽然是周一

  • 2024-09-16 是休息日ヾ(≧≦)〃;当天正在因为中秋而休息
  • 目标日期的节气是: 白露,与此同时是该节气的第10天~
  • 农历日期: 二零二四年八月十四日; 该日属于甲辰年癸酉月癸未日

网站使用的是腾讯云的轻量应用服务器进行搭建,另外也给大家申请到的福利:

另外,也可以考虑:

使用服务器的专属连接,享受超低折扣( ◔ ڼ ◔ )

支持创作

制作教程不易,如果热心的小伙伴,想支持创作,可以加入我们的电圈(还可以解锁远程协助、好友位😃):

WebChart Recognise

当然,也欢迎在B站或YouTube上关注我们:

更多:

节假日库选择

项目概要

LKI/chinese-calendar是基于Python的一个日期项目,如果你观察源码,你会发现日期数据是使用枚举类和Python字典存储的:

甚至结构和vsme/chinese-days也是一样的;其实vsme/chinese-days的JSON数据就是基于LKI/chinese-calendar的。在vsme/chinese-days的项目简介内,就有提及。

两个项目,都是基于MIT协议,在团队和企业内也可以放心使用。

MIT协议

比较有趣的是节日的中英名字映射、放假天数,LKI/chinese-calendar使用的是枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Holiday(Enum):
def __new__(cls, english, chinese, days):
obj = object.__new__(cls)
obj._value_ = english

obj.chinese = chinese
obj.days = days
return obj

new_years_day = "New Year's Day", "元旦", 1
spring_festival = "Spring Festival", "春节", 3
tomb_sweeping_day = "Tomb-sweeping Day", "清明", 1
labour_day = "Labour Day", "劳动节", 1
dragon_boat_festival = "Dragon Boat Festival", "端午", 1
national_day = "National Day", "国庆节", 3
mid_autumn_festival = "Mid-autumn Festival", "中秋", 1

# special holidays
anti_fascist_70th_day = "Anti-Fascist 70th Day", "中国人民抗日战争暨世界反法西斯战争胜利70周年纪念日", 1

倒不是使用枚举类有多么让人意想不到,只是发现原来放假的节日这么少😭。

以为节日很多……

至于这个库怎么使用,其实也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import datetime

# 判断 2018年4月30号 是不是节假日
from chinese_calendar import is_holiday, is_workday
april_last = datetime.date(2018, 4, 30)
assert is_workday(april_last) is False
assert is_holiday(april_last) is True

# 或者在判断的同时,获取节日名
import chinese_calendar as calendar # 也可以这样 import
on_holiday, holiday_name = calendar.get_holiday_detail(april_last)
assert on_holiday is True
assert holiday_name == calendar.Holiday.labour_day.value

# 还能判断法定节假日是不是调休
import chinese_calendar
assert chinese_calendar.is_in_lieu(datetime.date(2006, 2, 1)) is False
assert chinese_calendar.is_in_lieu(datetime.date(2006, 2, 2)) is True

接下来,我们就封转一下。 使其生成SQL脚本。

数据库设计

既然需要一张表来存储工作日历,那么数据库的表应该如何设计?

我这里设计得比较简单,采用每天一条记录的方式进行记录,表名为WORK_CALENDAR,主键为CALENDAR_DATE

YEAR CALENDAR_DATE DATE_TYPE COMMENTS
2024 2024-01-01 1 New Year’s Day
2024 2024-01-02 0
2024 2024-01-03 0
2024 2024-01-04 0
2024 2024-01-05 0
2024 2024-01-06 3
2024 2024-01-07 3

解释一下各个字段:

  • YEAR: 日期所属的年份;
  • CALENDAR_DATE: 数据对应的日期;
  • DATE_TYPE: 日期类型,0为普通工作日,1为节日放假,2为节日调休补班,3为周末放假;
  • COMMENTS: 备注节日。

可能你有更好的方法,可以评论区留言嗷。

代码实现

接下来,我们看看代码如何实现。

因为需要一次性生成一年的工作日历,所以我们需要先获取一年的数据日期,之后遍历数据日期,使用LKI/chinese-calendar去解析每次的数据日期,将返回的结果包转为CSV或者拼接SQL。

流程图如下:

graph TD;
    A[开始]:::start --> B{获取全年日期}:::process;
    B -->|全年日期| C[循环遍历日期]:::loop;
    C --> D{判断日期类型}:::decision;
    D -->|节假日| E((生成SQL: 节假日)):::holiday;
    D -->|周末| F((生成SQL: 周末)):::weekend;
    D -->|工作日| G((生成SQL: 工作日)):::workday;
    D -->|补班| H((生成SQL: 补班)):::compensation;
    D -->|未匹配| I((打印: 未匹配)):::unmatched;
    C --> J[收集数据]:::collect;
    J --> K(写入SQL文件):::write-sql;
    J --> L(导出CSV文件):::export-csv;
    K --> Z[结束]:::endPoint;
    L --> Z;

classDef start fill:#f9d635,stroke:#333,stroke-width:4px;
classDef process fill:#2196f3,stroke:#333,stroke-width:4px;
classDef loop fill:#4caf50,stroke:#333,stroke-width:4px;
classDef decision fill:#ff9800,stroke:#333,stroke-width:4px;
classDef holiday fill:#e91e63,stroke:#333,stroke-width:4px;
classDef weekend fill:#9c27b0,stroke:#333,stroke-width:4px;
classDef workday fill:#00bcd4,stroke:#333,stroke-width:4px;
classDef compensation fill:#ffeb3b,stroke:#333,stroke-width:4px;
classDef unmatched fill:#607d8b,stroke:#333,stroke-width:4px;
classDef collect fill:#8bc34a,stroke:#333,stroke-width:4px;
classDef write-sql fill:#795548,stroke:#333,stroke-width:4px;
classDef export-csv fill:#673ab7,stroke:#333,stroke-width:4px;
classDef endPoint fill:#f44336,stroke:#333,stroke-width:4px;

全年日期

我们先获取全年的日期,可以使用datetime进行日期类型的创建:

1
2
# year为所属的年,如:2024
begin = datetime.date(year, 1, 1)

之后,使用datetime.timedelta(days=1)方法 ,获取一条的数值,并遍历累加即可。完整的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_whole_year(year=TARGET_YEAR):
"""
获取一年内所有的日期
:param year: 获取的年
:return: 日期数组
"""
begin = datetime.date(year, 1, 1) # 设置开始日期为给定年的1月1日
now = begin
end = datetime.date(year, 12, 31) # 设置结束日期为给定年的12月31日
delta = datetime.timedelta(days=1) # 定义日期增量为1天
days = [] # 初始化日期数组
while now <= end: # 循环直到当前日期达到结束日期
days.append(now.strftime("%Y-%m-%d")) # 将当前日期以"YYYY-MM-DD"格式添加到数组中
now += delta # 增加日期增量
return days # 返回日期数组

日期类型

在项目的代码内,我们知道LKI/chinese-calendar返回的都是TrueFasle,比如:calendar.is_workday(date)

但是我们数据库内的设计,使用的是0~3作为存储,并且还有一个注释字段,用来存储节日;所以,我们也需要设计一个枚举类,用来映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DATATYPE(Enum):
"""
日期类型枚举类
"""
WORKDAY = ("0", "普通工作日")
WEEKEND = ("3", "普通周末")
HOLIDAY = ("1", "节日假期")
WORKING_HOLIDAY = ("2", "节日补班")

def __init__(self, code: str, description: str):
self.code = code
self.description = description

@property
def code(self) -> str:
return self._code

@code.setter
def code(self, value: str):
self._code = value

@property
def description(self) -> str:
return self._description

@description.setter
def description(self, value: str):
self._description = value

当我们进行日期的判断,就可以把布尔类型,经过枚举进行包转:

1
2
3
4
5
6
7
8
if calendar.is_holiday(date):
print("{}是节假日".format(judge_date))
on_holiday, holiday_name = calendar.get_holiday_detail(date)
# 判断是否为节日(否:周末;是:节日)
if holiday_name is not None:
return DATATYPE.HOLIDAY.code + "-" + str(holiday_name)
else:
return DATATYPE.WEEKEND.code

实现效果

最后,把上述的代码进行整理,核心判断内容就这样完成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def judge_date_type(judge_date):
"""
判断日期的类型
:param judge_date:
:return: 判断的类型;如果是假期有关,附带假期备注
"""
date = datetime.datetime.strptime(judge_date, '%Y-%m-%d').date()
if calendar.is_holiday(date):
print("{}是节假日".format(judge_date))
on_holiday, holiday_name = calendar.get_holiday_detail(date)
# 判断是否为节日(否:周末;是:节日)
if holiday_name is not None:
return DATATYPE.HOLIDAY.code + "-" + str(holiday_name)
else:
return DATATYPE.WEEKEND.code
elif calendar.is_workday(date):
on_holiday, holiday_name = calendar.get_holiday_detail(date)
if holiday_name is not None:
# 节日名称为非空,说明是补班,否则就是普通工作日
print("{}是补班日".format(judge_date))
return DATATYPE.WORKING_HOLIDAY.code + "-" + str(holiday_name)
else:
return DATATYPE.WORKDAY.code
else:
# 理论上不存在没有匹配的情况
print("{}没有匹配" .format(judge_date))
assert False

再使用pandas进行包装一下,可以轻松得到SQL和CSV文件:

生成的SQL和CSV文件

完整的代码,可以在我的GitHub仓库上找到:

GitHub地址

END

哈哈,本次的分享就到这边。 其实是一件非常简单的事情,希望设计思路可以帮助到你。

如果你也需要工作日历,那么你可以直接clone代码并运行即可。

大家一般又是如何生成“工作日历”的呢?

期待大家的声音



如何快速给数据库内生成工作日历?Python生成中国节假日工作表
https://www.mintimate.cn/2024/08/24/chineseCalendar/
作者
Mintimate
发布于
2024年8月24日
许可协议