一、日期概念
- 自然日:一年365天都属于自然日
- 工作日:简单理解为上班就是工作日,不上班就是非工作日
- 交易日:证券、期货交易的日期
二、交易日表结构
CREATE TABLE "BUS_DATE"
( "ID" VARCHAR2(32) NOT NULL,
"SK_DATE" NUMBER(8,0) NOT NULL,
"BK_DATE" DATE NOT NULL,
"GRE_DAY" VARCHAR2(10) NOT NULL,
"GRE_WEEK" VARCHAR2(10) NOT NULL,
"GRE_MONTH" VARCHAR2(10) NOT NULL,
"GRE_QUARTER" VARCHAR2(10) NOT NULL,
"GRE_YEAR" VARCHAR2(10) NOT NULL,
"SEMI_YEAR" VARCHAR2(10) NOT NULL,
"IS_WORK_DAY" CHAR(1) NOT NULL,
"IS_YHJ_DAY" CHAR(1) NOT NULL,
"CREATE_TIME" DATE NOT NULL,
"UPDATE_TIME" DATE NOT NULL,
"CREATE_USER" VARCHAR2(20),
"UPDATE_USER" VARCHAR2(20),
PRIMARY KEY ("ID")
);
COMMENT ON TABLE BUS_DATE IS '交易日表';
COMMENT ON COLUMN BUS_DATE.ID IS '主键';
COMMENT ON COLUMN BUS_DATE.SK_DATE IS '日期';
COMMENT ON COLUMN BUS_DATE.BK_DATE IS '日期date类型';
COMMENT ON COLUMN BUS_DATE.GRE_DAY IS '公历日';
COMMENT ON COLUMN BUS_DATE.GRE_WEEK IS '公历周';
COMMENT ON COLUMN BUS_DATE.GRE_MONTH IS '公历月';
COMMENT ON COLUMN BUS_DATE.GRE_QUARTER IS '公历季';
COMMENT ON COLUMN BUS_DATE.GRE_YEAR IS '公历年';
COMMENT ON COLUMN BUS_DATE.SEMI_YEAR IS '半年度';
COMMENT ON COLUMN BUS_DATE.IS_WORK_DAY IS '是否交易日';
COMMENT ON COLUMN BUS_DATE.IS_YHJ_DAY IS '是否工作日/银行间交易日';
COMMENT ON COLUMN BUS_DATE.CREATE_TIME IS '创建时间';
COMMENT ON COLUMN BUS_DATE.UPDATE_TIME IS '修改时间';
COMMENT ON COLUMN BUS_DATE.CREATE_USER IS '创建人';
COMMENT ON COLUMN BUS_DATE.UPDATE_USER IS '修改人';
数据预览:
此交易日表需要预先插入下一年的数据,以防计算日期超出范围。
三、代码
1.枚举类
IEnum.java
DimTypeEnum.java
package com.zhou.pub.dim.common;
import com.zhou.util.enums.IEnum;
import lombok.Getter;
/**
* 交易日表计算维度枚举
* @author lang.zhou
* @since 2024/5/24 14:02
*/
public enum DimTypeEnum implements IEnum<String> {
DAY("gre_day","日"),
WEEK("gre_week","周"),
MONTH("gre_month","月"),
QUARTER("gre_quarter","季"),
SEMIYEAR("semi_year","半年"),
YEAR("gre_year","年");
@Getter
private final String value;
@Getter
private final String name;
DimTypeEnum(String value, String name) {
this.value = value;
this.name = name;
}
}
DateTypeEnum.java
package com.zhou.pub.dim.common;
import com.zhou.util.enums.IEnum;
import lombok.Getter;
/**
* 日期类型枚举
* @author lang.zhou
* @since 2024/5/24 13:10
*/
public enum DateTypeEnum implements IEnum<Integer> {
NATURE(0,"自然日"),
TRADE(1,"交易日"),
WORK(2,"工作日");
@Getter
private final Integer value;
@Getter
private final String name;
DateTypeEnum(Integer value, String name) {
this.value = value;
this.name = name;
}
}
2.实体类
DimTime.java
package com.zhou.pub.dim.dao;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.zhou.common.model.CommonBean;
import com.zhou.pub.dim.dto.DimTimeDTO;
import com.zhou.util.DateParse;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 交易日表
* </p>
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("pub_dim_time")
@ApiModel(value="DimTime对象", description="交易日表")
public class DimTime extends DimTimeDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "自然日")
@TableField("sk_date")
private Integer skDate;
@ApiModelProperty(value = "日期类型")
@JsonFormat(pattern = "yyyy-MM-dd")
@TableField("bk_date")
private Date bkDate;
@ApiModelProperty(value = "公历日")
@TableField("gre_day")
private String greDay;
@ApiModelProperty(value = "公历周")
@TableField("gre_week")
private String greWeek;
@ApiModelProperty(value = "公历月")
@TableField("gre_month")
private String greMonth;
@ApiModelProperty(value = "公历季")
@TableField("gre_quarter")
private String greQuarter;
@ApiModelProperty(value = "公历年")
@TableField("gre_year")
private String greYear;
@ApiModelProperty(value = "半年度")
@TableField("semi_year")
private String semiYear;
@ApiModelProperty(value = "是否交易日")
@TableField("is_work_day")
@NotBlank(message = "交易日类型不能为空")
private String isWorkDay;
@ApiModelProperty(value = "是否银行间交易日")
@TableField("is_yhj_day")
@NotBlank(message = "银行间工作日类型不能为空")
private String isYhjDay;
}
3.mapper层
DimTimeMapper.java
package com.zhou.pub.dim.mapper;
import com.zhou.pub.dim.dao.DimTime;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 交易日表 Mapper 接口
* </p>
*/
public interface DimTimeMapper extends BaseMapper<DimTime> {
}
DimTimeMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhou.pub.dim.mapper.DimTimeMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.zhou.pub.dim.dao.DimTime">
<id column="id" property="id" />
<result column="sk_date" property="skDate" />
<result column="bk_date" property="bkDate" />
<result column="gre_day" property="greDay" />
<result column="gre_week" property="greWeek" />
<result column="gre_month" property="greMonth" />
<result column="gre_quarter" property="greQuarter" />
<result column="gre_year" property="greYear" />
<result column="semi_year" property="semiYear" />
<result column="is_work_day" property="isWorkDay" />
<result column="is_yhj_day" property="isYhjDay" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="create_user" property="createUser" />
<result column="update_user" property="updateUser" />
</resultMap>
</mapper>
4.service层
DimTimeService.java
package com.zhou.pub.dim.service;
import com.zhou.pub.dim.dao.DimTime;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* <p>
* 交易日表 服务类
* </p>
*/
public interface DimTimeService extends IService<DimTime> {
/**
* 升序查询交易日表全部数据(此处为了方便,也可以传入一个日期范围,提高查询效率)
*/
List<DimTime> listAll();
}
DimTimeServiceImpl.java
package com.zhou.pub.dim.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhou.pub.dim.dao.DimTime;
import com.zhou.pub.dim.mapper.DimTimeMapper;
import com.zhou.pub.dim.service.DimTimeService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 交易日表 服务实现类
* </p>
*/
@Service
public class DimTimeServiceImpl extends ServiceImpl<DimTimeMapper, DimTime> implements DimTimeService {
@Override
public List<DimTime> listAll() {
LambdaQueryWrapper<DimTime> w = new LambdaQueryWrapper<>();
w.orderByAsc(DimTime::getSkDate);
return list(w);
}
}
5.封装工具类DimTimeUtil.java
package com.zhou.pub.dim.util;
import com.zhou.common.enums.RateTypeEnum;
import com.zhou.pub.dim.common.DimTypeEnum;
import com.zhou.pub.dim.common.DateTypeEnum;
import com.zhou.pub.dim.dao.DimTime;
import com.zhou.util.DateParse;
import com.zhou.util.enums.IEnum;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Objects;
/**
* 交易日计算工具类
* @author lang.zhou
* @since 2023/9/15 15:39
*/
@SuppressWarnings("all")
@Slf4j
public class DimTimeUtil {
/**
* 找到日期所在索引
*/
private static int findIndex(Integer date, List<DimTime> dimTimeList){
if(date != null){
for (int i = 0; i < dimTimeList.size(); i++) {
DimTime dimTime = dimTimeList.get(i);
if (Objects.equals(dimTime.getSkDate(), date)) {
return i;
}
}
}
return -1;
}
/**
* 找到日期DimTime对象
*/
public static DimTime findDimTime(Integer date, List<DimTime> dimTimeList){
if(date != null){
for (DimTime dimTime : dimTimeList) {
if (Objects.equals(dimTime.getSkDate(), date)) {
return dimTime;
}
}
}
return null;
}
/**
* 判断两个日期是否属于相同日/周/月/季/半年/年
*/
private static boolean isSameName(DimTime time1, DimTime time2, DimTypeEnum name){
if(name == DimTypeEnum.DAY){
return Objects.equals(time1.getGreDay(),time2.getGreDay());
}else if(name == DimTypeEnum.WEEK){
return Objects.equals(time1.getGreWeek(),time2.getGreWeek());
}else if(name == DimTypeEnum.MONTH){
return Objects.equals(time1.getGreMonth(),time2.getGreMonth());
}else if(name == DimTypeEnum.QUARTER){
return Objects.equals(time1.getGreQuarter(),time2.getGreQuarter());
}else if(name == DimTypeEnum.SEMIYEAR){
return Objects.equals(time1.getSemiYear(),time2.getSemiYear());
}else if(name == DimTypeEnum.YEAR){
return Objects.equals(time1.getGreYear(),time2.getGreYear());
}
return false;
}
/**
* 得到date所在周/月/季/年/半年/的第一天
*/
private static Integer getFirstDate(Integer date, DimTypeEnum name){
String res = null;
if(date != null){
DateParse parse = DateParse.ct();
if(name == DimTypeEnum.DAY){
res = String.valueOf(date);
}else if(name == DimTypeEnum.WEEK){
res = parse.getFirstDayOfWeek(date,0);
}else if(name == DimTypeEnum.MONTH){
res = parse.getFirstDayOfMonth(date,0);
}else if(name == DimTypeEnum.QUARTER){
res = parse.getFirstDayOfQuarter(date,0);
}else if(name == DimTypeEnum.SEMIYEAR){
res = parse.getFirstDayOfHalfYear(date,0);
}else if(name == DimTypeEnum.YEAR){
res = parse.getFirstDayOfYear(date,0);
}
}
return res == null ? null : Integer.valueOf(res);
}
/**
* 判断日期是否是自然日/交易日/银行间交易日
*/
private static boolean judgeWorkDay(DimTime dimTime, DateTypeEnum dimType){
boolean res = dimType == null ? true : false;
if(DateTypeEnum.NATURE == dimType){
return true;
}
if(DateTypeEnum.TRADE == dimType && Objects.equals("Y",dimTime.getIsWorkDay())){
res = true;
}
if(DateTypeEnum.WORK == dimType && Objects.equals("Y",dimTime.getIsYhjDay())){
res = true;
}
return res;
}
/**
* 得到date所在(年/月/周)第n个工作日
* @param date 当前日期
* @param n 正数或负数(n<0倒数第n个)
* @param dimTimeList 交易日表数据(按sk_date升序)
* @param dimType 交易日类型DimTypeEnum
*/
public static String getWorkDayByMap(Integer date, int n, List<DimTime> dimTimeList, DimTypeEnum name, DateTypeEnum dimType){
int realCnt = 0;
Integer res = null;
Integer firstDate = getFirstDate(date,name);
DimTime dimTime = findDimTime(firstDate, dimTimeList);
int index = findIndex(firstDate, dimTimeList);
if(index > -1 && dimTime != null){
if(n > 0){
for (int i = index; i < dimTimeList.size(); i++) {
DimTime time = dimTimeList.get(i);
if(isSameName(dimTime,time,name)){
if(judgeWorkDay(time, dimType)){
realCnt ++;
res = time.getSkDate();
}
}
if(realCnt >= n){
break;
}
}
}else if(n < 0){
n = -n;
for (int i = index; i >= 0; i--) {
DimTime time = dimTimeList.get(i);
if(isSameName(dimTime,time,name)){
if(judgeWorkDay(time, dimType)){
realCnt ++;
res = time.getSkDate();
}
}
if(realCnt >= n){
break;
}
}
}else{
res = date;
}
}
return res == null ? null res.toString();
}
/**
* 得到上周/上月/上季度/去年最后一个工作日
* @param date 当前日期
* @param dimTimeList 交易日表数据(按sk_date升序)
* @param dimType 交易日类型DimTypeEnum
*/
public static String getLastWorkDayByMap(Integer date, List<DimTime> dimTimeList, DimTypeEnum name, DateTypeEnum dimType){
int realCnt = 0;
String res = null;
//得到本月/周/季/年的第一天
Integer firstDate = getFirstDate(date,name);
DimTime dimTime = findDimTime(firstDate, dimTimeList);
if(dimTime != null){
res = getLastWorkDay(dimTime.getSkDate(), 1, dimTimeList, dimType);
}
return res;
}
/**
* 得到date上一个工作日
* @param date 当前日期
* @param n 前n个交易日(正数或负数)
* @param dimTimeList 交易日表数据(按sk_date升序)
* @param dimType 交易日类型DimTypeEnum
*/
public static String getWorkDay(Integer date, int n, List<DimTime> dimTimeList, DateTypeEnum dimType){
if(n < 0){
return getNextWorkDay(date, -n, dimTimeList, dimType);
}else{
return getLastWorkDay(date, n, dimTimeList, dimType);
}
}
/**
* 得到date前n个交易日/银行间交易日
* @param date 当前日期
* @param n 前n个交易日(正数是前n个或负数是后n个)
* @param dimTimeList 交易日表数据(按sk_date升序)
* @param dimType 交易日类型DimTypeEnum
*/
public static String getLastWorkDay(Integer date, int n, List<DimTime> dimTimeList, DateTypeEnum dimType){
int realCnt = 0;
Integer res = null;
int index = findIndex(date, dimTimeList);
if(index > -1 && (index -1) < dimTimeList.size() - 1){
int d = n > 0 ? 1 : 0;
for (int i = index - d; i >= 0; i--) {
DimTime dimTime = dimTimeList.get(i);
if(judgeWorkDay(dimTime, dimType)){
realCnt ++;
res = dimTime.getSkDate();
}
if(realCnt >= n){
break;
}
}
}
return res == null ? null res.toString();
}
/**
* 得到date下一个工作日
* @param date 当前日期
* @param n 后n个交易日(正数或负数)
* @param dimTimeList 交易日表数据(按sk_date升序)
* @param dimType 交易日类型DimTypeEnum
*/
public static String getNextWorkDay(Integer date, int n, List<DimTime> dimTimeList, DateTypeEnum dimType){
int realCnt = 0;
Integer res = null;
int index = findIndex(date, dimTimeList);
if(index > -1 && (index + 1) < dimTimeList.size() - 1){
if(n > 0){
for (int i = index + 1; i < dimTimeList.size(); i++) {
DimTime dimTime = dimTimeList.get(i);
if(judgeWorkDay(dimTime, dimType)){
realCnt ++;
res = dimTime.getSkDate();
}
if(realCnt >= n){
break;
}
}
}else{
res = date;
}
}
return res == null ? null res.toString();
}
/**
* @param t 当前时间
* @param rate3 小时
* @param rate4 分钟
*/
public static boolean isTimeEqual(String t,String rate3,String rate4){
//String time=t.substring(0,5);
int hour = Integer.parseInt(t.substring(0,2));
int min = Integer.parseInt(t.substring(3,5))+60*hour;
int ol = Integer.parseInt(rate4) + 60*Integer.parseInt(rate3);
return ol == min;
}
}
DateParse.java
四、使用示例
场景1:计算日期为20250730之后的第5个交易日:
List<DimTimeDTO> list = dimTimeService.listAll();
String date = "20250730";
String day = DimTimeUtil.getNextWorkDay(date, 5, list, DateTypeEnum.TRADE);
System.out.println(day);
场景2:计算日期为20250730之前的第5个交易日:
List<DimTimeDTO> list = dimTimeService.listAll();
String date = "20250730";
String day = DimTimeUtil.getLastWorkDay(date, 5, list, DateTypeEnum.TRADE);
System.out.println(day);
场景3:计算20250730所在月份的第10个工作日:
List<DimTimeDTO> list = dimTimeService.listAll();
String date = "20250730";
String day = DimTimeUtil.getWorkDayByMap(date, 10, list, DimTypeEnum.MONTH, DateTypeEnum.WORK);
System.out.println(day);
场景4:计算20250730所在月份的倒数第10个工作日:
List<DimTimeDTO> list = dimTimeService.listAll();
String date = "20250730";
String day = DimTimeUtil.getWorkDayByMap(date, -10, list, DimTypeEnum.MONTH, DateTypeEnum.WORK);
System.out.println(day);
拓展场景:统计一个日期区间中有多少个交易日