叨叨:真的讨厌调休,呐呐呐
简介插件
一共使用了两款插件,moment-recur和 moment-business-days
- moment-recur:是用来生成时间范围的,可以根据间隔天数,每周固定日或者每年固定月份指定生成规则,如果提供了开始和结束的日期可以获取时间范围内所有的指定生成规则的日期序列,本文利用该插件生成全年的周六和周日的日期序列。
- moment-business-days:工作日插件,默认提供了双休日作为工作日,并且支持自定工作日范围以及节假日。
大致思路
由于每年的法定节假日和调休的时间多且繁杂,利用双休日并设置法定节假日为假期,然后生成结束日期,再根据开始日期和结束日期的范围去判断是否含有调休日再对日期进行加减的思路较为不便。
首先应当明确:
- 一年中要么为假期、要么为工作日。
- 调休日都是周六和周日,则一定会在生成的双休日序列a中。
实际使用的思路是:
- 生成每一年的周六和周日的双休日序列:a
- 和已知的假期序列:b进行合并得到 c
- 将c中的调休日剔除,得到d
- 将一周7天都设置为工作日
- 自定义假期
则d即为一年中所需要休息的日期序列。如果将d设置为假期,则其他日期都是工作日了,就可以利用工作日插件对相应内容进行计算了。
代码实现
需要安装相应插件
npm i moment moment-business-days moment-recur
主要有两部分:生成双休日序列,设置工作日以及自定义假期,合并假期以及双休日。
生成双休日序列
const formatStr = "YYYY-MM-DD";
const saturday = 6
const sunday = 7
/**
* 根据给定的年份,生成该年所有的周六和周日的日期
* @param startYear 开始年份,默认值为当年
* @param endYear 结束年份,默认值为当年
* @return {Array} 年份范围内的所有周六周日的时间,格式:YYYY-MM-DD
*/
const getAllYearWeekend = (startYear = moment().year(), endYear = moment().year()) => {
// 指定时间生成插件规则时间范围为开始年份的第一天到结束年份的最后一天
const recurInstance = moment(startYear, "YYYY").startOf("year").recur(moment(endYear, "YYYY").endOf("year"));
return recurInstance.every([saturday, sunday]).daysOfWeek().days().all().map(item => item.format(formatStr));
};
可能有坑的点有两个地方,一个是周一对应的是1不是0,一个是recurInstance
如果没有设置结束日期的话,不能使用all
方法。
设置工作日及自定义假期
这块实际上是moment
的自定义语言区的功能,如果说有坑的地方就是需要搞清语言区的代码,不然可能用不了,中国是zh-cn
。
const language = "zh-cn";
const localSpec = {
workingWeekdays: [monday, tuesday, wednesday, thursday, friday, saturday, sunday],
holidays: yearRangeHolidayList,
holidayFormat: formatStr
}
moment.updateLocale(language, localSpec);
moment.locale(language);
合并假期以及双休日
const yearStart = 0, yearEnd = 4, toNumber = 1;
// 取假期的前四位作为年份,并将其乘1转为数字,之后利用set去重,再利用解构重新转为数字数组作为节假日中存在的年份序列
const holidayListYearRange = [...new Set(holidays.map(dateStr => dateStr.substring(yearStart, yearEnd) * toNumber))];
// 从年份序列中找到最大和最小的作为生成周六周日的开始和结束年份
const yearRangeWeekendDateList = getAllYearWeekend(Math.min(...holidayListYearRange), Math.max(...holidayListYearRange));
// 合并双休日序列和法定节假日序列
const yearRangeHolidaySet = new Set([...holidays, ...yearRangeWeekendDateList]);
// 删除节日中调休的日期
businessDays.forEach(item => yearRangeHolidaySet.delete(item));
const yearRangeHolidayList = [...yearRangeHolidaySet];
这块是用了[...new Set(Array)]
做了一个去重,还有一个是Set
和数组可以利用展开运算符...
相互转换。
整体代码
import moment from "moment";
import "moment-recur";
import "moment-business-days";
import holidayDetail from "./holiday.json";
const { holidays, businessDays } = holidayDetail;
const formatStr = "YYYY-MM-DD";
/**
* 根据给定的年份,生成该年所有的周六和周日的日期,参数应当格式化为YYYY
* @param startYear {Number} 开始年份,默认值为当年
* @param endYear {Number} 结束年份,默认值为当年
* @return {Array} 年份范围内的所有周六周日的时间,格式:YYYY-MM-DD
*/
const getAllYearWeekend = (startYear = moment().year(), endYear = moment().year()) => {
// 指定时间生成插件规则时间范围为开始年份的第一天到结束年份的最后一天
const recurInstance = moment(startYear, yearFormat).startOf("year").recur(moment(endYear, yearFormat).endOf("year"));
return recurInstance.every([saturday, sunday]).daysOfWeek().days().all().map(item => item.format(formatStr));
};
/**
* 初始化,设置工作日及自定义假期
*/
const initHolidays = () => {
const yearStart = 0, yearEnd = 4, toNumber = 1;
const monday = 1;
const tuesday = 2;
const wednesday = 3;
const thursday = 4;
const friday = 5;
const saturday = 6;
const sunday = 7;
const yearFormat = "YYYY";
const language = "zh-cn";
// 取假期的前四位作为年份,并将其乘1转为数字,之后利用set去重,再利用解构重新转为数字数组
const holidayListYearRange = [...new Set(holidays.map(dateStr => dateStr.substring(yearStart, yearEnd) * toNumber))];
const yearRangeWeekendDateList = getAllYearWeekend(Math.min(...holidayListYearRange), Math.max(...holidayListYearRange));
const yearRangeHolidaySet = new Set([...holidays, ...yearRangeWeekendDateList]);
businessDays.forEach(item => yearRangeHolidaySet.delete(item));
const yearRangeHolidayList = [...yearRangeHolidaySet];
const localSpec = {
workingWeekdays: [monday, tuesday, wednesday, thursday, friday, saturday, sunday],
holidays: yearRangeHolidayList,
holidayFormat: formatStr
}
moment.updateLocale(language, localSpec);
moment.locale(language);
};
/**
* 根据给定的起始日期和工作日数量,返回对应的结束日期
* @param startDate {String} YYYY-MM-DD
* @param businessDayCount {Number} 工作日数量
* @return {String} 结束日期,YYYY-MM-DD
*/
const getSpecificBusinessDayCountEndDate = (startDate, businessDayCount) => {
console.log(moment(startDate, formatStr).businessAdd(2));
return moment(startDate, formatStr).businessAdd(businessDayCount).format(formatStr);
};
initHolidays();
// 调用,传入测试日期,应当返回 2023-10-07,因为中间跨了2023-09-29~2023-10-06一个中秋一个国庆
getSpecificBusinessDayCountEndDate("2023-09-28", 2);
计算工作日(2023-01-30 update)
传入一个开始日期一个结束日期的话,可以计算出中间究竟用了多少工作日。
实际上如果现在有了工作日序列了,如果传入一个开始日期一个结束日期的话,也就可以计算出中间究竟用了多少工作日了,可以使用business-days
的 businessDiff
api了。或者说有多少休息日了。但是该api有三个问题:
- 如果计算工作日同一天(2023-01-30,2023-01-30会返回0)
- 如果开始时间选择周五,结束时间为周六,则工作日会返回1
- 如果开始时间大于结束时间,只会继续返回整数差值,不会返回负数差值或者报错。
所以需要被改造。
getBusinessDayCountByDateRange: () => (startDate, endDate) => {
if (!endDate) {
return 0
}
// 对开始和结束时间做校验,使开始时间大于结束时间
if (moment(endDate, formatStr).diff(moment(startDate, formatStr), 'seconds') < 0) {
throw new Error('开始时间必须小于结束时间')
}
const businessDayCount = moment(startDate, formatStr).businessDiff(moment(endDate, formatStr))
// 如果工作日天数为0有两种情况,一是开始和结束时间都是工作日,但是是同一天(加1)。二是开始和结束时间都是休息日,则返回0
if (businessDayCount === 0) {
if (moment(startDate, formatStr).isBusinessDay() && moment(startDate, formatStr).diff(moment(endDate, formatStr), 'day')=== 0) {
return 1
}
if (moment(startDate, formatStr).isHoliday() && moment(endDate, formatStr).isHoliday()) {
return 0
}
}
// 边界处理:如果结束时间为休息日,则无需加一
if (moment(endDate).isHoliday()) {
return businessDayCount
}
return businessDayCount + 1
}
如果使用可能存在的坑
这里没有对在已知假期之外的进行处理,所以如果说假期序列中存在2022,2023的假日,但是最后计算出来的时间到2024了,那肯定会出问题的,如果使用的话请注意下这点。
另外没有对开始日期进行判断,如果开始日期落在了假日内,不知道是否存在坑。
代码仓库
暂无