@weiensong/chinese-calendar 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 weiensong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # chinese-calendar
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@weiensong/chinese-calendar)](https://www.npmjs.com/package/@weiensong/chinese-calendar)
4
+ [![license](https://img.shields.io/npm/l/@weiensong/chinese-calendar)](./LICENSE)
5
+
6
+ A zero-dependency ESM package that determines whether a given date is a workday in mainland China. It respects the State Council's official holiday and adjusted-workday (调休) schedules, with optional support for Guangxi Sanyuesan (广西三月三).
7
+
8
+ [中文文档](./README.zh-CN.md)
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @weiensong/chinese-calendar
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ import { isWorkday, getDayInfo } from "@weiensong/chinese-calendar";
22
+ ```
23
+
24
+ ### `isWorkday(input, options?)`
25
+
26
+ Returns `true` if the date is a workday, `false` otherwise.
27
+
28
+ ```js
29
+ isWorkday(new Date(2026, 0, 5)); // true — Monday
30
+ isWorkday("2026-01-01"); // false — New Year holiday
31
+ isWorkday("2026-01-04"); // true — adjusted workday for New Year
32
+ ```
33
+
34
+ ### `getDayInfo(input, options?)`
35
+
36
+ Returns a detailed result object explaining the classification.
37
+
38
+ ```js
39
+ getDayInfo("2026-01-04");
40
+ // {
41
+ // date: "2026-01-04",
42
+ // isWorkday: true,
43
+ // type: "adjusted-workday",
44
+ // holidayName: "元旦",
45
+ // scheduleYear: 2026,
46
+ // scheduleStatus: "official",
47
+ // sourceUrl: "https://www.gov.cn/..."
48
+ // }
49
+ ```
50
+
51
+ | `type` | Meaning |
52
+ | ------------------ | ----------------------- |
53
+ | `regular-workday` | Regular weekday |
54
+ | `weekend` | Regular weekend |
55
+ | `holiday` | Official holiday |
56
+ | `adjusted-workday` | Adjusted workday (调休) |
57
+
58
+ ### Options
59
+
60
+ ```ts
61
+ interface Options {
62
+ requireOfficialSchedule?: boolean; // throw if no data for the year
63
+ enableGuangxiSanyuesan?: boolean; // enable Guangxi Sanyuesan holidays
64
+ }
65
+ ```
66
+
67
+ #### Strict mode
68
+
69
+ ```js
70
+ import { isWorkday, ScheduleNotFoundError } from "@weiensong/chinese-calendar";
71
+
72
+ try {
73
+ isWorkday("2027-01-04", { requireOfficialSchedule: true });
74
+ } catch (error) {
75
+ console.log(error instanceof ScheduleNotFoundError); // true
76
+ }
77
+ ```
78
+
79
+ #### Guangxi Sanyuesan
80
+
81
+ Disabled by default. When enabled, any date falling within the Sanyuesan period is resolved solely by Guangxi rules — national adjusted-workday schedules do not apply during this window.
82
+
83
+ ```js
84
+ isWorkday("2025-03-31"); // true — national workday
85
+ isWorkday("2025-03-31", { enableGuangxiSanyuesan: true }); // false — Guangxi Sanyuesan
86
+ ```
87
+
88
+ ## Date input
89
+
90
+ `isWorkday` and `getDayInfo` accept a `Date` object or a string. Strings in `YYYY-MM-DD` format are parsed as local calendar dates (no timezone offset). Other strings are parsed via `new Date()` and interpreted in the runtime's local timezone. Invalid input throws a `TypeError`.
91
+
92
+ ## Coverage
93
+
94
+ Built-in data covers **2014–2026**, including subsequent adjustments such as the 2015 Victory Day holiday, 2019 Labor Day revision, and 2020 Spring Festival extension.
95
+
96
+ When no schedule data exists for a year, the package falls back to simple weekday/weekend rules. Set `requireOfficialSchedule: true` to throw a `ScheduleNotFoundError` instead.
97
+
98
+ ## Adding a new year
99
+
100
+ 1. Create `data/YYYY.js` following the schema below.
101
+ 2. Register it in `data/index.js`.
102
+
103
+ ```js
104
+ export default {
105
+ year: 2026,
106
+ source: {
107
+ title: "国务院办公厅关于2026年部分节假日安排的通知",
108
+ documentNumber: "国办发明电〔2025〕7号",
109
+ publishedAt: "2025-11-04",
110
+ url: "https://www.gov.cn/...",
111
+ },
112
+ holidays: [
113
+ { name: "春节", start: "2026-02-15", end: "2026-02-23" },
114
+ ],
115
+ adjustedWorkdays: [
116
+ { date: "2026-02-14", relatedHoliday: "春节" },
117
+ ],
118
+ guangxiSanyuesan: {
119
+ name: "广西三月三",
120
+ daysOff: [],
121
+ adjustedWorkdays: [],
122
+ },
123
+ };
124
+ ```
125
+
126
+ The `guangxiSanyuesan` block is required; leave arrays empty if unused.
127
+
128
+ ## Testing
129
+
130
+ ```bash
131
+ npm test
132
+ ```
133
+
134
+ Uses Node.js built-in test runner. No additional dependencies required.
135
+
136
+ ## License
137
+
138
+ [MIT](./LICENSE)
@@ -0,0 +1,138 @@
1
+ # chinese-calendar
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@weiensong/chinese-calendar)](https://www.npmjs.com/package/@weiensong/chinese-calendar)
4
+ [![license](https://img.shields.io/npm/l/@weiensong/chinese-calendar)](./LICENSE)
5
+
6
+ 零依赖的 ESM npm 包,判断中国大陆指定日期是否需要上班,遵循国务院官方节假日和调休安排,可选支持广西三月三。
7
+
8
+ [English Documentation](./README.md)
9
+
10
+ ---
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install @weiensong/chinese-calendar
16
+ ```
17
+
18
+ ## 使用
19
+
20
+ ```js
21
+ import { isWorkday, getDayInfo } from "@weiensong/chinese-calendar";
22
+ ```
23
+
24
+ ### `isWorkday(input, options?)`
25
+
26
+ 返回 `true` 表示上班日,`false` 表示休息日。
27
+
28
+ ```js
29
+ isWorkday(new Date(2026, 0, 5)); // true — 星期一
30
+ isWorkday("2026-01-01"); // false — 元旦假期
31
+ isWorkday("2026-01-04"); // true — 元旦调休上班
32
+ ```
33
+
34
+ ### `getDayInfo(input, options?)`
35
+
36
+ 返回包含判断依据的详细对象。
37
+
38
+ ```js
39
+ getDayInfo("2026-01-04");
40
+ // {
41
+ // date: "2026-01-04",
42
+ // isWorkday: true,
43
+ // type: "adjusted-workday",
44
+ // holidayName: "元旦",
45
+ // scheduleYear: 2026,
46
+ // scheduleStatus: "official",
47
+ // sourceUrl: "https://www.gov.cn/..."
48
+ // }
49
+ ```
50
+
51
+ | `type` | 含义 |
52
+ | ------------------ | ------------ |
53
+ | `regular-workday` | 普通工作日 |
54
+ | `weekend` | 普通周末 |
55
+ | `holiday` | 法定节假日 |
56
+ | `adjusted-workday` | 调休上班日 |
57
+
58
+ ### 选项
59
+
60
+ ```ts
61
+ interface Options {
62
+ requireOfficialSchedule?: boolean; // 无年度数据时抛出错误
63
+ enableGuangxiSanyuesan?: boolean; // 启用广西三月三
64
+ }
65
+ ```
66
+
67
+ #### 严格模式
68
+
69
+ ```js
70
+ import { isWorkday, ScheduleNotFoundError } from "@weiensong/chinese-calendar";
71
+
72
+ try {
73
+ isWorkday("2027-01-04", { requireOfficialSchedule: true });
74
+ } catch (error) {
75
+ console.log(error instanceof ScheduleNotFoundError); // true
76
+ }
77
+ ```
78
+
79
+ #### 广西三月三
80
+
81
+ 默认不启用。启用后,三月三日期区间内的日期全部按广西规则判断,全国调休不覆盖该区间。
82
+
83
+ ```js
84
+ isWorkday("2025-03-31"); // true — 全国工作日
85
+ isWorkday("2025-03-31", { enableGuangxiSanyuesan: true }); // false — 广西三月三放假
86
+ ```
87
+
88
+ ## 日期输入
89
+
90
+ 接受 `Date` 对象或字符串。`YYYY-MM-DD` 格式按本地日历日期解析(无时区偏移),其他字符串通过 `new Date()` 解析并按运行环境本地时区判断。无效输入抛出 `TypeError`。
91
+
92
+ ## 覆盖范围
93
+
94
+ 内置 **2014—2026** 年国务院节假日安排,包含历史上的后续调整(2015 年抗战胜利纪念日、2019 年劳动节调整、2020 年春节延长)。
95
+
96
+ 缺少年度数据时默认退回普通星期规则,可启用严格模式抛出 `ScheduleNotFoundError`。
97
+
98
+ ## 添加新年份
99
+
100
+ 1. 按以下结构创建 `data/YYYY.js`。
101
+ 2. 在 `data/index.js` 中注册。
102
+
103
+ ```js
104
+ export default {
105
+ year: 2026,
106
+ source: {
107
+ title: "国务院办公厅关于2026年部分节假日安排的通知",
108
+ documentNumber: "国办发明电〔2025〕7号",
109
+ publishedAt: "2025-11-04",
110
+ url: "https://www.gov.cn/...",
111
+ },
112
+ holidays: [
113
+ { name: "春节", start: "2026-02-15", end: "2026-02-23" },
114
+ ],
115
+ adjustedWorkdays: [
116
+ { date: "2026-02-14", relatedHoliday: "春节" },
117
+ ],
118
+ guangxiSanyuesan: {
119
+ name: "广西三月三",
120
+ daysOff: [],
121
+ adjustedWorkdays: [],
122
+ },
123
+ };
124
+ ```
125
+
126
+ `guangxiSanyuesan` 字段为必填,没有假期时保留空数组。
127
+
128
+ ## 测试
129
+
130
+ ```bash
131
+ npm test
132
+ ```
133
+
134
+ 使用 Node.js 原生测试运行器,无需安装额外依赖。
135
+
136
+ ## 许可证
137
+
138
+ [MIT](./LICENSE)
package/data/2014.js ADDED
@@ -0,0 +1,30 @@
1
+ export default {
2
+ year: 2014,
3
+ source: {
4
+ title: "国务院办公厅关于2014年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2013〕28号",
6
+ publishedAt: "2013-12-11",
7
+ url: "https://www.yueyang.gov.cn/gggs/szbm/content_230742.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2014-01-01", end: "2014-01-01" },
11
+ { name: "春节", start: "2014-01-31", end: "2014-02-06" },
12
+ { name: "清明节", start: "2014-04-05", end: "2014-04-07" },
13
+ { name: "劳动节", start: "2014-05-01", end: "2014-05-03" },
14
+ { name: "端午节", start: "2014-05-31", end: "2014-06-02" },
15
+ { name: "中秋节", start: "2014-09-06", end: "2014-09-08" },
16
+ { name: "国庆节", start: "2014-10-01", end: "2014-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2014-01-26", relatedHoliday: "春节" },
20
+ { date: "2014-02-08", relatedHoliday: "春节" },
21
+ { date: "2014-05-04", relatedHoliday: "劳动节" },
22
+ { date: "2014-09-28", relatedHoliday: "国庆节" },
23
+ { date: "2014-10-11", relatedHoliday: "国庆节" },
24
+ ],
25
+ guangxiSanyuesan: {
26
+ name: "广西三月三",
27
+ daysOff: ["2014-04-02", "2014-04-03"],
28
+ adjustedWorkdays: [],
29
+ },
30
+ };
package/data/2015.js ADDED
@@ -0,0 +1,42 @@
1
+ export default {
2
+ year: 2015,
3
+ source: {
4
+ title: "国务院办公厅关于2015年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2014〕28号",
6
+ publishedAt: "2014-12-16",
7
+ url: "https://www.nea.gov.cn/2015-01/08/c_133904166.htm",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2015-01-01", end: "2015-01-03" },
11
+ { name: "春节", start: "2015-02-18", end: "2015-02-24" },
12
+ { name: "清明节", start: "2015-04-04", end: "2015-04-06" },
13
+ { name: "劳动节", start: "2015-05-01", end: "2015-05-03" },
14
+ { name: "端午节", start: "2015-06-20", end: "2015-06-22" },
15
+ {
16
+ name: "抗战胜利纪念日",
17
+ start: "2015-09-03",
18
+ end: "2015-09-05",
19
+ sourceUrl:
20
+ "https://www.ndrc.gov.cn/xxgk/zcfb/qt/201505/t20150513_967883.html",
21
+ },
22
+ { name: "中秋节", start: "2015-09-26", end: "2015-09-27" },
23
+ { name: "国庆节", start: "2015-10-01", end: "2015-10-07" },
24
+ ],
25
+ adjustedWorkdays: [
26
+ { date: "2015-01-04", relatedHoliday: "元旦" },
27
+ { date: "2015-02-15", relatedHoliday: "春节" },
28
+ { date: "2015-02-28", relatedHoliday: "春节" },
29
+ {
30
+ date: "2015-09-06",
31
+ relatedHoliday: "抗战胜利纪念日",
32
+ sourceUrl:
33
+ "https://www.ndrc.gov.cn/xxgk/zcfb/qt/201505/t20150513_967883.html",
34
+ },
35
+ { date: "2015-10-10", relatedHoliday: "国庆节" },
36
+ ],
37
+ guangxiSanyuesan: {
38
+ name: "广西三月三",
39
+ daysOff: ["2015-04-20", "2015-04-21"],
40
+ adjustedWorkdays: [],
41
+ },
42
+ };
package/data/2016.js ADDED
@@ -0,0 +1,36 @@
1
+ export default {
2
+ year: 2016,
3
+ source: {
4
+ title: "国务院办公厅关于2016年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2015〕18号",
6
+ publishedAt: "2015-12-11",
7
+ url: "https://www.nea.gov.cn/2015-12/11/c_134905936.htm",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2016-01-01", end: "2016-01-03" },
11
+ { name: "春节", start: "2016-02-07", end: "2016-02-13" },
12
+ { name: "清明节", start: "2016-04-02", end: "2016-04-04" },
13
+ { name: "劳动节", start: "2016-04-30", end: "2016-05-02" },
14
+ { name: "端午节", start: "2016-06-09", end: "2016-06-11" },
15
+ { name: "中秋节", start: "2016-09-15", end: "2016-09-17" },
16
+ { name: "国庆节", start: "2016-10-01", end: "2016-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2016-02-06", relatedHoliday: "春节" },
20
+ { date: "2016-02-14", relatedHoliday: "春节" },
21
+ { date: "2016-06-12", relatedHoliday: "端午节" },
22
+ { date: "2016-09-18", relatedHoliday: "中秋节" },
23
+ { date: "2016-10-08", relatedHoliday: "国庆节" },
24
+ { date: "2016-10-09", relatedHoliday: "国庆节" },
25
+ ],
26
+ guangxiSanyuesan: {
27
+ name: "广西三月三",
28
+ daysOff: [
29
+ "2016-04-08",
30
+ "2016-04-09",
31
+ "2016-04-10",
32
+ "2016-04-11",
33
+ ],
34
+ adjustedWorkdays: [],
35
+ },
36
+ };
package/data/2017.js ADDED
@@ -0,0 +1,33 @@
1
+ export default {
2
+ year: 2017,
3
+ source: {
4
+ title: "国务院办公厅关于2017年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2016〕17号",
6
+ publishedAt: "2016-12-02",
7
+ url: "https://app.www.gov.cn/govdata/gov/201612/01/394246/article.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2016-12-31", end: "2017-01-02" },
11
+ { name: "春节", start: "2017-01-27", end: "2017-02-02" },
12
+ { name: "清明节", start: "2017-04-02", end: "2017-04-04" },
13
+ { name: "劳动节", start: "2017-04-29", end: "2017-05-01" },
14
+ { name: "端午节", start: "2017-05-28", end: "2017-05-30" },
15
+ {
16
+ name: "国庆节、中秋节",
17
+ start: "2017-10-01",
18
+ end: "2017-10-08",
19
+ },
20
+ ],
21
+ adjustedWorkdays: [
22
+ { date: "2017-01-22", relatedHoliday: "春节" },
23
+ { date: "2017-02-04", relatedHoliday: "春节" },
24
+ { date: "2017-04-01", relatedHoliday: "清明节" },
25
+ { date: "2017-05-27", relatedHoliday: "端午节" },
26
+ { date: "2017-09-30", relatedHoliday: "国庆节、中秋节" },
27
+ ],
28
+ guangxiSanyuesan: {
29
+ name: "广西三月三",
30
+ daysOff: ["2017-03-30", "2017-03-31", "2017-04-01"],
31
+ adjustedWorkdays: ["2017-04-08"],
32
+ },
33
+ };
package/data/2018.js ADDED
@@ -0,0 +1,31 @@
1
+ export default {
2
+ year: 2018,
3
+ source: {
4
+ title: "国务院办公厅关于2018年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2017〕12号",
6
+ publishedAt: "2017-11-30",
7
+ url: "https://app.www.gov.cn/govdata/gov/201711/30/415918/article.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2017-12-30", end: "2018-01-01" },
11
+ { name: "春节", start: "2018-02-15", end: "2018-02-21" },
12
+ { name: "清明节", start: "2018-04-05", end: "2018-04-07" },
13
+ { name: "劳动节", start: "2018-04-29", end: "2018-05-01" },
14
+ { name: "端午节", start: "2018-06-16", end: "2018-06-18" },
15
+ { name: "中秋节", start: "2018-09-22", end: "2018-09-24" },
16
+ { name: "国庆节", start: "2018-10-01", end: "2018-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2018-02-11", relatedHoliday: "春节" },
20
+ { date: "2018-02-24", relatedHoliday: "春节" },
21
+ { date: "2018-04-08", relatedHoliday: "清明节" },
22
+ { date: "2018-04-28", relatedHoliday: "劳动节" },
23
+ { date: "2018-09-29", relatedHoliday: "国庆节" },
24
+ { date: "2018-09-30", relatedHoliday: "国庆节" },
25
+ ],
26
+ guangxiSanyuesan: {
27
+ name: "广西三月三",
28
+ daysOff: ["2018-04-18", "2018-04-19", "2018-04-20"],
29
+ adjustedWorkdays: ["2018-04-15"],
30
+ },
31
+ };
package/data/2019.js ADDED
@@ -0,0 +1,48 @@
1
+ export default {
2
+ year: 2019,
3
+ source: {
4
+ title: "国务院办公厅关于2019年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2018〕15号",
6
+ publishedAt: "2018-12-06",
7
+ url: "https://app.www.gov.cn/govdata/gov/201812/06/432792/article.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2018-12-30", end: "2019-01-01" },
11
+ { name: "春节", start: "2019-02-04", end: "2019-02-10" },
12
+ { name: "清明节", start: "2019-04-05", end: "2019-04-07" },
13
+ {
14
+ name: "劳动节",
15
+ start: "2019-05-01",
16
+ end: "2019-05-04",
17
+ sourceUrl:
18
+ "https://app.www.gov.cn/govdata/gov/201903/22/436883/article.html",
19
+ },
20
+ { name: "端午节", start: "2019-06-07", end: "2019-06-09" },
21
+ { name: "中秋节", start: "2019-09-13", end: "2019-09-15" },
22
+ { name: "国庆节", start: "2019-10-01", end: "2019-10-07" },
23
+ ],
24
+ adjustedWorkdays: [
25
+ { date: "2018-12-29", relatedHoliday: "元旦" },
26
+ { date: "2019-02-02", relatedHoliday: "春节" },
27
+ { date: "2019-02-03", relatedHoliday: "春节" },
28
+ {
29
+ date: "2019-04-28",
30
+ relatedHoliday: "劳动节",
31
+ sourceUrl:
32
+ "https://app.www.gov.cn/govdata/gov/201903/22/436883/article.html",
33
+ },
34
+ {
35
+ date: "2019-05-05",
36
+ relatedHoliday: "劳动节",
37
+ sourceUrl:
38
+ "https://app.www.gov.cn/govdata/gov/201903/22/436883/article.html",
39
+ },
40
+ { date: "2019-09-29", relatedHoliday: "国庆节" },
41
+ { date: "2019-10-12", relatedHoliday: "国庆节" },
42
+ ],
43
+ guangxiSanyuesan: {
44
+ name: "广西三月三",
45
+ daysOff: ["2019-04-04", "2019-04-08"],
46
+ adjustedWorkdays: [],
47
+ },
48
+ };
package/data/2020.js ADDED
@@ -0,0 +1,40 @@
1
+ export default {
2
+ year: 2020,
3
+ source: {
4
+ title: "国务院办公厅关于2020年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2019〕16号",
6
+ publishedAt: "2019-11-21",
7
+ url: "https://big5.www.gov.cn/gate/big5/www.gov.cn/zhengce/content/2019-11/21/content_5454164.htm",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2020-01-01", end: "2020-01-01" },
11
+ {
12
+ name: "春节",
13
+ start: "2020-01-24",
14
+ end: "2020-02-02",
15
+ sourceUrl:
16
+ "https://app.www.gov.cn/govdata/gov/202001/27/453442/article.html",
17
+ },
18
+ { name: "清明节", start: "2020-04-04", end: "2020-04-06" },
19
+ { name: "劳动节", start: "2020-05-01", end: "2020-05-05" },
20
+ { name: "端午节", start: "2020-06-25", end: "2020-06-27" },
21
+ {
22
+ name: "国庆节、中秋节",
23
+ start: "2020-10-01",
24
+ end: "2020-10-08",
25
+ },
26
+ ],
27
+ adjustedWorkdays: [
28
+ { date: "2020-01-19", relatedHoliday: "春节" },
29
+ { date: "2020-04-26", relatedHoliday: "劳动节" },
30
+ { date: "2020-05-09", relatedHoliday: "劳动节" },
31
+ { date: "2020-06-28", relatedHoliday: "端午节" },
32
+ { date: "2020-09-27", relatedHoliday: "国庆节、中秋节" },
33
+ { date: "2020-10-10", relatedHoliday: "国庆节、中秋节" },
34
+ ],
35
+ guangxiSanyuesan: {
36
+ name: "广西三月三",
37
+ daysOff: [],
38
+ adjustedWorkdays: [],
39
+ },
40
+ };
package/data/2021.js ADDED
@@ -0,0 +1,32 @@
1
+ export default {
2
+ year: 2021,
3
+ source: {
4
+ title: "国务院办公厅关于2021年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2020〕27号",
6
+ publishedAt: "2020-11-25",
7
+ url: "https://app.www.gov.cn/govdata/gov/202011/26/465359/article.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2021-01-01", end: "2021-01-03" },
11
+ { name: "春节", start: "2021-02-11", end: "2021-02-17" },
12
+ { name: "清明节", start: "2021-04-03", end: "2021-04-05" },
13
+ { name: "劳动节", start: "2021-05-01", end: "2021-05-05" },
14
+ { name: "端午节", start: "2021-06-12", end: "2021-06-14" },
15
+ { name: "中秋节", start: "2021-09-19", end: "2021-09-21" },
16
+ { name: "国庆节", start: "2021-10-01", end: "2021-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2021-02-07", relatedHoliday: "春节" },
20
+ { date: "2021-02-20", relatedHoliday: "春节" },
21
+ { date: "2021-04-25", relatedHoliday: "劳动节" },
22
+ { date: "2021-05-08", relatedHoliday: "劳动节" },
23
+ { date: "2021-09-18", relatedHoliday: "中秋节" },
24
+ { date: "2021-09-26", relatedHoliday: "国庆节" },
25
+ { date: "2021-10-09", relatedHoliday: "国庆节" },
26
+ ],
27
+ guangxiSanyuesan: {
28
+ name: "广西三月三",
29
+ daysOff: ["2021-04-14", "2021-04-15", "2021-04-16"],
30
+ adjustedWorkdays: ["2021-04-18"],
31
+ },
32
+ };
package/data/2022.js ADDED
@@ -0,0 +1,32 @@
1
+ export default {
2
+ year: 2022,
3
+ source: {
4
+ title: "国务院办公厅关于2022年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2021〕11号",
6
+ publishedAt: "2021-10-25",
7
+ url: "https://app.www.gov.cn/govdata/gov/202110/25/477428/article.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2022-01-01", end: "2022-01-03" },
11
+ { name: "春节", start: "2022-01-31", end: "2022-02-06" },
12
+ { name: "清明节", start: "2022-04-03", end: "2022-04-05" },
13
+ { name: "劳动节", start: "2022-04-30", end: "2022-05-04" },
14
+ { name: "端午节", start: "2022-06-03", end: "2022-06-05" },
15
+ { name: "中秋节", start: "2022-09-10", end: "2022-09-12" },
16
+ { name: "国庆节", start: "2022-10-01", end: "2022-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2022-01-29", relatedHoliday: "春节" },
20
+ { date: "2022-01-30", relatedHoliday: "春节" },
21
+ { date: "2022-04-02", relatedHoliday: "清明节" },
22
+ { date: "2022-04-24", relatedHoliday: "劳动节" },
23
+ { date: "2022-05-07", relatedHoliday: "劳动节" },
24
+ { date: "2022-10-08", relatedHoliday: "国庆节" },
25
+ { date: "2022-10-09", relatedHoliday: "国庆节" },
26
+ ],
27
+ guangxiSanyuesan: {
28
+ name: "广西三月三",
29
+ daysOff: ["2022-04-02", "2022-04-06"],
30
+ adjustedWorkdays: [],
31
+ },
32
+ };
package/data/2023.js ADDED
@@ -0,0 +1,35 @@
1
+ export default {
2
+ year: 2023,
3
+ source: {
4
+ title: "国务院办公厅关于2023年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2022〕16号",
6
+ publishedAt: "2022-12-08",
7
+ url: "https://app.www.gov.cn/govdata/gov/202212/08/495070/article.html",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2022-12-31", end: "2023-01-02" },
11
+ { name: "春节", start: "2023-01-21", end: "2023-01-27" },
12
+ { name: "清明节", start: "2023-04-05", end: "2023-04-05" },
13
+ { name: "劳动节", start: "2023-04-29", end: "2023-05-03" },
14
+ { name: "端午节", start: "2023-06-22", end: "2023-06-24" },
15
+ {
16
+ name: "中秋节、国庆节",
17
+ start: "2023-09-29",
18
+ end: "2023-10-06",
19
+ },
20
+ ],
21
+ adjustedWorkdays: [
22
+ { date: "2023-01-28", relatedHoliday: "春节" },
23
+ { date: "2023-01-29", relatedHoliday: "春节" },
24
+ { date: "2023-04-23", relatedHoliday: "劳动节" },
25
+ { date: "2023-05-06", relatedHoliday: "劳动节" },
26
+ { date: "2023-06-25", relatedHoliday: "端午节" },
27
+ { date: "2023-10-07", relatedHoliday: "中秋节、国庆节" },
28
+ { date: "2023-10-08", relatedHoliday: "中秋节、国庆节" },
29
+ ],
30
+ guangxiSanyuesan: {
31
+ name: "广西三月三",
32
+ daysOff: ["2023-04-21", "2023-04-22", "2023-04-23"],
33
+ adjustedWorkdays: [],
34
+ },
35
+ };
package/data/2024.js ADDED
@@ -0,0 +1,33 @@
1
+ export default {
2
+ year: 2024,
3
+ source: {
4
+ title: "国务院办公厅关于2024年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2023〕7号",
6
+ publishedAt: "2023-10-25",
7
+ url: "https://www.gov.cn/zhengce/content/202310/content_6911527.htm",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2024-01-01", end: "2024-01-01" },
11
+ { name: "春节", start: "2024-02-10", end: "2024-02-17" },
12
+ { name: "清明节", start: "2024-04-04", end: "2024-04-06" },
13
+ { name: "劳动节", start: "2024-05-01", end: "2024-05-05" },
14
+ { name: "端午节", start: "2024-06-10", end: "2024-06-10" },
15
+ { name: "中秋节", start: "2024-09-15", end: "2024-09-17" },
16
+ { name: "国庆节", start: "2024-10-01", end: "2024-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2024-02-04", relatedHoliday: "春节" },
20
+ { date: "2024-02-18", relatedHoliday: "春节" },
21
+ { date: "2024-04-07", relatedHoliday: "清明节" },
22
+ { date: "2024-04-28", relatedHoliday: "劳动节" },
23
+ { date: "2024-05-11", relatedHoliday: "劳动节" },
24
+ { date: "2024-09-14", relatedHoliday: "中秋节" },
25
+ { date: "2024-09-29", relatedHoliday: "国庆节" },
26
+ { date: "2024-10-12", relatedHoliday: "国庆节" },
27
+ ],
28
+ guangxiSanyuesan: {
29
+ name: "广西三月三",
30
+ daysOff: ["2024-04-11", "2024-04-12"],
31
+ adjustedWorkdays: [],
32
+ },
33
+ };
package/data/2025.js ADDED
@@ -0,0 +1,33 @@
1
+ export default {
2
+ year: 2025,
3
+ source: {
4
+ title: "国务院办公厅关于2025年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2024〕12号",
6
+ publishedAt: "2024-11-12",
7
+ url: "https://www.gov.cn/zhengce/zhengceku/202411/content_6986383.htm",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2025-01-01", end: "2025-01-01" },
11
+ { name: "春节", start: "2025-01-28", end: "2025-02-04" },
12
+ { name: "清明节", start: "2025-04-04", end: "2025-04-06" },
13
+ { name: "劳动节", start: "2025-05-01", end: "2025-05-05" },
14
+ { name: "端午节", start: "2025-05-31", end: "2025-06-02" },
15
+ {
16
+ name: "国庆节、中秋节",
17
+ start: "2025-10-01",
18
+ end: "2025-10-08",
19
+ },
20
+ ],
21
+ adjustedWorkdays: [
22
+ { date: "2025-01-26", relatedHoliday: "春节" },
23
+ { date: "2025-02-08", relatedHoliday: "春节" },
24
+ { date: "2025-04-27", relatedHoliday: "劳动节" },
25
+ { date: "2025-09-28", relatedHoliday: "国庆节、中秋节" },
26
+ { date: "2025-10-11", relatedHoliday: "国庆节、中秋节" },
27
+ ],
28
+ guangxiSanyuesan: {
29
+ name: "广西三月三",
30
+ daysOff: ["2025-03-31", "2025-04-01"],
31
+ adjustedWorkdays: [],
32
+ },
33
+ };
package/data/2026.js ADDED
@@ -0,0 +1,36 @@
1
+ export default {
2
+ year: 2026,
3
+ source: {
4
+ title: "国务院办公厅关于2026年部分节假日安排的通知",
5
+ documentNumber: "国办发明电〔2025〕7号",
6
+ publishedAt: "2025-11-04",
7
+ url: "https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm",
8
+ },
9
+ holidays: [
10
+ { name: "元旦", start: "2026-01-01", end: "2026-01-03" },
11
+ { name: "春节", start: "2026-02-15", end: "2026-02-23" },
12
+ { name: "清明节", start: "2026-04-04", end: "2026-04-06" },
13
+ { name: "劳动节", start: "2026-05-01", end: "2026-05-05" },
14
+ { name: "端午节", start: "2026-06-19", end: "2026-06-21" },
15
+ { name: "中秋节", start: "2026-09-25", end: "2026-09-27" },
16
+ { name: "国庆节", start: "2026-10-01", end: "2026-10-07" },
17
+ ],
18
+ adjustedWorkdays: [
19
+ { date: "2026-01-04", relatedHoliday: "元旦" },
20
+ { date: "2026-02-14", relatedHoliday: "春节" },
21
+ { date: "2026-02-28", relatedHoliday: "春节" },
22
+ { date: "2026-05-09", relatedHoliday: "劳动节" },
23
+ { date: "2026-09-20", relatedHoliday: "国庆节" },
24
+ { date: "2026-10-10", relatedHoliday: "国庆节" },
25
+ ],
26
+ guangxiSanyuesan: {
27
+ name: "广西三月三",
28
+ daysOff: [
29
+ "2026-04-17",
30
+ "2026-04-18",
31
+ "2026-04-19",
32
+ "2026-04-20",
33
+ ],
34
+ adjustedWorkdays: [],
35
+ },
36
+ };
package/data/index.js ADDED
@@ -0,0 +1,29 @@
1
+ import schedule2014 from "./2014.js";
2
+ import schedule2015 from "./2015.js";
3
+ import schedule2016 from "./2016.js";
4
+ import schedule2017 from "./2017.js";
5
+ import schedule2018 from "./2018.js";
6
+ import schedule2019 from "./2019.js";
7
+ import schedule2020 from "./2020.js";
8
+ import schedule2021 from "./2021.js";
9
+ import schedule2022 from "./2022.js";
10
+ import schedule2023 from "./2023.js";
11
+ import schedule2024 from "./2024.js";
12
+ import schedule2025 from "./2025.js";
13
+ import schedule2026 from "./2026.js";
14
+
15
+ export const schedules = [
16
+ schedule2014,
17
+ schedule2015,
18
+ schedule2016,
19
+ schedule2017,
20
+ schedule2018,
21
+ schedule2019,
22
+ schedule2020,
23
+ schedule2021,
24
+ schedule2022,
25
+ schedule2023,
26
+ schedule2024,
27
+ schedule2025,
28
+ schedule2026,
29
+ ];
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export {
2
+ getDayInfo,
3
+ isWorkday,
4
+ ScheduleNotFoundError,
5
+ } from "./lib/schedule.js";
package/lib/date.js ADDED
@@ -0,0 +1,86 @@
1
+ const INVALID_DATE_MESSAGE =
2
+ "date must be a valid Date object or date string";
3
+
4
+ /**
5
+ * Parse and validate a YYYY-MM-DD calendar date.
6
+ *
7
+ * @param {string} value
8
+ * @returns {{ year: number, month: number, day: number, timestamp: number }}
9
+ */
10
+ export function parseDateKey(value) {
11
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
12
+
13
+ if (!match) {
14
+ throw new TypeError(`invalid calendar date: ${value}`);
15
+ }
16
+
17
+ const [, year, month, day] = match.map(Number);
18
+ const timestamp = Date.UTC(year, month - 1, day);
19
+ const parsed = new Date(timestamp);
20
+
21
+ if (
22
+ parsed.getUTCFullYear() !== year ||
23
+ parsed.getUTCMonth() !== month - 1 ||
24
+ parsed.getUTCDate() !== day
25
+ ) {
26
+ throw new TypeError(`invalid calendar date: ${value}`);
27
+ }
28
+
29
+ return { year, month, day, timestamp };
30
+ }
31
+
32
+ /**
33
+ * Parse a public Date or string input using local calendar semantics.
34
+ *
35
+ * @param {Date|string} input
36
+ * @returns {Date}
37
+ */
38
+ export function parseDateInput(input) {
39
+ let date;
40
+
41
+ if (input instanceof Date) {
42
+ date = input;
43
+ } else if (typeof input === "string" && input.trim() !== "") {
44
+ const value = input.trim();
45
+
46
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
47
+ const { year, month, day } = parseDateKey(value);
48
+ date = new Date(0);
49
+ date.setHours(0, 0, 0, 0);
50
+ date.setFullYear(year, month - 1, day);
51
+ } else {
52
+ date = new Date(value);
53
+ }
54
+ } else {
55
+ throw new TypeError(INVALID_DATE_MESSAGE);
56
+ }
57
+
58
+ if (Number.isNaN(date.getTime())) {
59
+ throw new TypeError(INVALID_DATE_MESSAGE);
60
+ }
61
+
62
+ return date;
63
+ }
64
+
65
+ /**
66
+ * Format a Date as a local YYYY-MM-DD calendar key.
67
+ *
68
+ * @param {Date} date
69
+ * @returns {string}
70
+ */
71
+ export function toDateKey(date) {
72
+ const year = String(date.getFullYear()).padStart(4, "0");
73
+ const month = String(date.getMonth() + 1).padStart(2, "0");
74
+ const day = String(date.getDate()).padStart(2, "0");
75
+ return `${year}-${month}-${day}`;
76
+ }
77
+
78
+ /**
79
+ * Format a UTC timestamp as YYYY-MM-DD.
80
+ *
81
+ * @param {number} timestamp
82
+ * @returns {string}
83
+ */
84
+ export function utcTimestampToDateKey(timestamp) {
85
+ return new Date(timestamp).toISOString().slice(0, 10);
86
+ }
@@ -0,0 +1,322 @@
1
+ import { schedules } from "../data/index.js";
2
+ import {
3
+ parseDateInput,
4
+ parseDateKey,
5
+ toDateKey,
6
+ utcTimestampToDateKey,
7
+ } from "./date.js";
8
+
9
+ const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
10
+ const schedulesByYear = new Map();
11
+ const exceptionsByDate = new Map();
12
+ const guangxiSanyuesanByDate = new Map();
13
+ const guangxiSanyuesanWorkdaysByDate = new Map();
14
+ const guangxiSanyuesanPeriodDates = new Set();
15
+
16
+ export class ScheduleNotFoundError extends Error {
17
+ /** @param {number} year */
18
+ constructor(year) {
19
+ super(`official workday schedule is not available for ${year}`);
20
+ this.name = "ScheduleNotFoundError";
21
+ this.year = year;
22
+ }
23
+ }
24
+
25
+ function assertNonEmptyString(value, field) {
26
+ if (typeof value !== "string" || value.trim() === "") {
27
+ throw new TypeError(`${field} must be a non-empty string`);
28
+ }
29
+ }
30
+
31
+ function registerSchedule(schedule) {
32
+ if (!Number.isInteger(schedule.year)) {
33
+ throw new TypeError("schedule year must be an integer");
34
+ }
35
+
36
+ if (schedulesByYear.has(schedule.year)) {
37
+ throw new TypeError(`duplicate schedule year: ${schedule.year}`);
38
+ }
39
+
40
+ assertNonEmptyString(schedule.source?.title, "schedule source title");
41
+ assertNonEmptyString(schedule.source?.documentNumber, "schedule document number");
42
+ parseDateKey(schedule.source?.publishedAt);
43
+ assertNonEmptyString(schedule.source?.url, "schedule source URL");
44
+
45
+ schedulesByYear.set(schedule.year, schedule);
46
+
47
+ for (const holiday of schedule.holidays) {
48
+ assertNonEmptyString(holiday.name, "holiday name");
49
+ const start = parseDateKey(holiday.start);
50
+ const end = parseDateKey(holiday.end);
51
+
52
+ if (
53
+ Math.abs(start.year - schedule.year) > 1 ||
54
+ Math.abs(end.year - schedule.year) > 1
55
+ ) {
56
+ throw new TypeError(`holiday is too far from ${schedule.year}`);
57
+ }
58
+
59
+ if (end.timestamp < start.timestamp) {
60
+ throw new TypeError(`holiday starts after it ends: ${holiday.name}`);
61
+ }
62
+
63
+ for (
64
+ let timestamp = start.timestamp;
65
+ timestamp <= end.timestamp;
66
+ timestamp += ONE_DAY_IN_MILLISECONDS
67
+ ) {
68
+ const date = utcTimestampToDateKey(timestamp);
69
+
70
+ if (exceptionsByDate.has(date)) {
71
+ throw new TypeError(`conflicting schedule entry: ${date}`);
72
+ }
73
+
74
+ exceptionsByDate.set(date, {
75
+ type: "holiday",
76
+ isWorkday: false,
77
+ holidayName: holiday.name,
78
+ sourceUrl: holiday.sourceUrl ?? schedule.source.url,
79
+ schedule,
80
+ });
81
+ }
82
+ }
83
+
84
+ for (const adjustedWorkday of schedule.adjustedWorkdays) {
85
+ assertNonEmptyString(
86
+ adjustedWorkday.relatedHoliday,
87
+ "adjusted workday related holiday",
88
+ );
89
+ const { year } = parseDateKey(adjustedWorkday.date);
90
+
91
+ if (Math.abs(year - schedule.year) > 1) {
92
+ throw new TypeError(
93
+ `adjusted workday is too far from ${schedule.year}`,
94
+ );
95
+ }
96
+
97
+ if (exceptionsByDate.has(adjustedWorkday.date)) {
98
+ throw new TypeError(`conflicting schedule entry: ${adjustedWorkday.date}`);
99
+ }
100
+
101
+ exceptionsByDate.set(adjustedWorkday.date, {
102
+ type: "adjusted-workday",
103
+ isWorkday: true,
104
+ holidayName: adjustedWorkday.relatedHoliday,
105
+ sourceUrl: adjustedWorkday.sourceUrl ?? schedule.source.url,
106
+ schedule,
107
+ });
108
+ }
109
+
110
+ assertNonEmptyString(
111
+ schedule.guangxiSanyuesan?.name,
112
+ "Guangxi Sanyuesan name",
113
+ );
114
+
115
+ if (!Array.isArray(schedule.guangxiSanyuesan?.daysOff)) {
116
+ throw new TypeError("Guangxi Sanyuesan daysOff must be an array");
117
+ }
118
+
119
+ if (!Array.isArray(schedule.guangxiSanyuesan?.adjustedWorkdays)) {
120
+ throw new TypeError(
121
+ "Guangxi Sanyuesan adjustedWorkdays must be an array",
122
+ );
123
+ }
124
+
125
+ for (const date of schedule.guangxiSanyuesan.daysOff) {
126
+ const { year } = parseDateKey(date);
127
+
128
+ if (year !== schedule.year) {
129
+ throw new TypeError(
130
+ `Guangxi Sanyuesan day off must be within ${schedule.year}`,
131
+ );
132
+ }
133
+
134
+ if (guangxiSanyuesanByDate.has(date)) {
135
+ throw new TypeError(`duplicate Guangxi Sanyuesan day off: ${date}`);
136
+ }
137
+
138
+ guangxiSanyuesanByDate.set(date, {
139
+ holidayName: schedule.guangxiSanyuesan.name,
140
+ schedule,
141
+ });
142
+ }
143
+
144
+ for (const date of schedule.guangxiSanyuesan.adjustedWorkdays) {
145
+ const { year } = parseDateKey(date);
146
+
147
+ if (year !== schedule.year) {
148
+ throw new TypeError(
149
+ `Guangxi Sanyuesan adjusted workday must be within ${schedule.year}`,
150
+ );
151
+ }
152
+
153
+ if (
154
+ guangxiSanyuesanByDate.has(date) ||
155
+ guangxiSanyuesanWorkdaysByDate.has(date)
156
+ ) {
157
+ throw new TypeError(`conflicting Guangxi Sanyuesan entry: ${date}`);
158
+ }
159
+
160
+ guangxiSanyuesanWorkdaysByDate.set(date, {
161
+ holidayName: schedule.guangxiSanyuesan.name,
162
+ schedule,
163
+ });
164
+ }
165
+
166
+ const allSanyuesanDates = [
167
+ ...schedule.guangxiSanyuesan.daysOff,
168
+ ...schedule.guangxiSanyuesan.adjustedWorkdays,
169
+ ];
170
+
171
+ if (allSanyuesanDates.length > 0) {
172
+ const timestamps = allSanyuesanDates.map((d) => parseDateKey(d).timestamp);
173
+ const minTs = Math.min(...timestamps);
174
+ const maxTs = Math.max(...timestamps);
175
+
176
+ for (
177
+ let ts = minTs;
178
+ ts <= maxTs;
179
+ ts += ONE_DAY_IN_MILLISECONDS
180
+ ) {
181
+ guangxiSanyuesanPeriodDates.add(utcTimestampToDateKey(ts));
182
+ }
183
+ }
184
+ }
185
+
186
+ for (const schedule of schedules) {
187
+ registerSchedule(schedule);
188
+ }
189
+
190
+ function normalizeOptions(options) {
191
+ if (options === undefined) {
192
+ return {
193
+ requireOfficialSchedule: false,
194
+ enableGuangxiSanyuesan: false,
195
+ };
196
+ }
197
+
198
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
199
+ throw new TypeError("options must be an object");
200
+ }
201
+
202
+ const requireOfficialSchedule = options.requireOfficialSchedule ?? false;
203
+ const enableGuangxiSanyuesan = options.enableGuangxiSanyuesan ?? false;
204
+
205
+ if (typeof requireOfficialSchedule !== "boolean") {
206
+ throw new TypeError("requireOfficialSchedule must be a boolean");
207
+ }
208
+
209
+ if (typeof enableGuangxiSanyuesan !== "boolean") {
210
+ throw new TypeError("enableGuangxiSanyuesan must be a boolean");
211
+ }
212
+
213
+ return { requireOfficialSchedule, enableGuangxiSanyuesan };
214
+ }
215
+
216
+ /**
217
+ * Return an explainable workday classification for a date.
218
+ *
219
+ * @param {Date|string} input
220
+ * @param {{
221
+ * requireOfficialSchedule?: boolean,
222
+ * enableGuangxiSanyuesan?: boolean
223
+ * }} [options]
224
+ * @returns {{
225
+ * date: string,
226
+ * isWorkday: boolean,
227
+ * type: "regular-workday"|"weekend"|"holiday"|"adjusted-workday",
228
+ * holidayName: string|null,
229
+ * scheduleYear: number|null,
230
+ * scheduleStatus: "official"|"weekday-fallback",
231
+ * sourceUrl: string|null
232
+ * }}
233
+ */
234
+ export function getDayInfo(input, options) {
235
+ const date = parseDateInput(input);
236
+ const normalizedOptions = normalizeOptions(options);
237
+ const dateKey = toDateKey(date);
238
+ const year = date.getFullYear();
239
+ const schedule = schedulesByYear.get(year);
240
+
241
+ if (!schedule && normalizedOptions.requireOfficialSchedule) {
242
+ throw new ScheduleNotFoundError(year);
243
+ }
244
+
245
+ const guangxiSanyuesanWorkday = normalizedOptions.enableGuangxiSanyuesan
246
+ ? guangxiSanyuesanWorkdaysByDate.get(dateKey)
247
+ : undefined;
248
+
249
+ if (guangxiSanyuesanWorkday) {
250
+ return {
251
+ date: dateKey,
252
+ isWorkday: true,
253
+ type: "adjusted-workday",
254
+ holidayName: guangxiSanyuesanWorkday.holidayName,
255
+ scheduleYear: guangxiSanyuesanWorkday.schedule.year,
256
+ scheduleStatus: "official",
257
+ sourceUrl: null,
258
+ };
259
+ }
260
+
261
+ const guangxiSanyuesan = normalizedOptions.enableGuangxiSanyuesan
262
+ ? guangxiSanyuesanByDate.get(dateKey)
263
+ : undefined;
264
+
265
+ if (guangxiSanyuesan) {
266
+ return {
267
+ date: dateKey,
268
+ isWorkday: false,
269
+ type: "holiday",
270
+ holidayName: guangxiSanyuesan.holidayName,
271
+ scheduleYear: guangxiSanyuesan.schedule.year,
272
+ scheduleStatus: "official",
273
+ sourceUrl: null,
274
+ };
275
+ }
276
+
277
+ const inSanyuesanPeriod =
278
+ normalizedOptions.enableGuangxiSanyuesan &&
279
+ guangxiSanyuesanPeriodDates.has(dateKey);
280
+
281
+ if (!inSanyuesanPeriod) {
282
+ const exception = exceptionsByDate.get(dateKey);
283
+
284
+ if (exception) {
285
+ return {
286
+ date: dateKey,
287
+ isWorkday: exception.isWorkday,
288
+ type: exception.type,
289
+ holidayName: exception.holidayName,
290
+ scheduleYear: exception.schedule.year,
291
+ scheduleStatus: "official",
292
+ sourceUrl: exception.sourceUrl,
293
+ };
294
+ }
295
+ }
296
+
297
+ const isWorkday = date.getDay() >= 1 && date.getDay() <= 5;
298
+
299
+ return {
300
+ date: dateKey,
301
+ isWorkday,
302
+ type: isWorkday ? "regular-workday" : "weekend",
303
+ holidayName: null,
304
+ scheduleYear: schedule?.year ?? null,
305
+ scheduleStatus: schedule ? "official" : "weekday-fallback",
306
+ sourceUrl: schedule?.source.url ?? null,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Determine whether a date is a workday in mainland China's official schedule.
312
+ *
313
+ * @param {Date|string} input
314
+ * @param {{
315
+ * requireOfficialSchedule?: boolean,
316
+ * enableGuangxiSanyuesan?: boolean
317
+ * }} [options]
318
+ * @returns {boolean}
319
+ */
320
+ export function isWorkday(input, options) {
321
+ return getDayInfo(input, options).isWorkday;
322
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@weiensong/chinese-calendar",
3
+ "version": "0.0.2",
4
+ "description": "Determine whether a date is a workday in mainland China, respecting State Council holiday and adjusted-workday schedules",
5
+ "author": "weiensong",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": "./index.js",
9
+ "files": [
10
+ "index.js",
11
+ "lib",
12
+ "data"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/touero/chinese-calendar.git"
20
+ },
21
+ "homepage": "https://github.com/touero/chinese-calendar#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/touero/chinese-calendar/issues"
24
+ },
25
+ "publishConfig": {
26
+ "registry": "https://registry.npmjs.org"
27
+ },
28
+ "keywords": [
29
+ "chinese-calendar",
30
+ "workday",
31
+ "holiday",
32
+ "china",
33
+ "中国节假日",
34
+ "工作日",
35
+ "调休"
36
+ ],
37
+ "license": "MIT"
38
+ }