给定这样一个场景,现在我们要在特定的某一天里,来汇总业务系统中某一段时间内的数据。在这基础上再加上一个要求,那就是不同的业务类型数据,汇总时间及区间也不同。这样的需求场景看起来是很合理,那么在这样的背景要求下,汇总数据很简单,但是如何拿到动态的时间区间,并且我们的汇总时间能够动态的配置,怎么能够方便快速的满足需求呢?
一.需求场景
什么?需求看不懂?好吧是我没说明白!来来来,没有什么东西是用举个栗子解释不清楚的。
假设:今天是10月2日(周一),我们的不同的业务线要在今天来汇总不同日期区间的数据
- A业务在今天来汇总上一季度的数据(7月1日-9月30日)
- B业务在今天来汇总上一个月(9月1日-9月30日)的数据
- C业务在今天分别汇总上个月上半旬及下半旬的数据,即(1日-15日,16日-30日,不同的月份最后一天不同)
- D业务在今天来汇总上月2日-次月1日的数据(9月2日-10月1日)
- E业务在今天汇总上上个月最后一日至上个月倒数第二日的数据(8月31日-9月29日)
- F业务在今天汇总上一周的数据(9月24日-9月30日)
- G业务在今天汇总前一天的数据(10月1日)
这好像没什么嘛!
那再来点有难度的!今天依旧是10月2日(周一),假设各个业务线汇总的时间不一样,如下:
- A业务在每个季度的1号来汇总上一季度(7月1日汇总4月1日-6月30日)
- B业务在每月的第一天来汇总上一个月(9月1日-9月30日)的数据(10月1日汇总9月1日-30日)
- C业务在5号汇总上个月15日-上月之后一天的数据,在18日汇总1-15日的数据,即(10月5日汇总9月16-30日,10月18日汇总10月1日-15日)
- D业务在每月2日来汇总上月2日-次月1日的数据(10月2日汇总9月2日-10月1日)
- E业务在每月2日汇总上上个月最后一天至上个月倒数第二天的数据(10月2日汇总8月31日-9月29日)
- F业务在每周二汇总上一周的数据(10月3日汇总9月24日-9月30日)
- G业务在当天汇总前一天的数据(10月2日汇总10月1日)
- H业务在每月第二个交易日汇总上月的数据(10月9日汇总9月1日-30日,假设10月国庆放假7天,8号(周一)正常上班,那么第二个交易日即10月9日)
来看看,其实真正的在10月2日这一天,我们需要跑的只有业务D,E和G。好了到这里比较清晰了,但一般汇总数据的任务都需要支持重跑。假如现在已经到了10月5日,我还想重新汇总D业务线在10月2号汇总的数据,怎么办?
先想想,再想想…其实…
二.方法及表达式定义
别想了,这里我来简单的说。我们需要一个公式,根据当前日期或者指定的一个日期,算出我们想要的时间区间,是不是就能够满足我们上面所有的情况?
那么好了,总结一下,其实需要的方法就这么四个:
public class DateExpressionUtil {
/**
* 根据表达式获取日期
* @param dateExpression
* @return
*/
public static Date explainDateExpression(String dateExpression){
//@TODO;
}
/**
* 根据表达式获取日期
* @param dateExpression
* @param baseTime 基础日期
* @return
*/
public static Date explainDateExpression(String dateExpression,Date baseTime){
//@TODO;
}
/**
* 检查规则执行日期是否是当前日期
* @param executeData
* @return
*/
public static boolean checkExecuteData(String executeData){
//@TODO;
}
/**
* 解析日期表达式,并计算日期区间
* @param dateExpression
* Q-1: 前一季度1日-前一季度最后一日
* M-1: 前月1日-前月最后一日
* Q-1-D-1/Q-0-D-2: 前两季度最后一日-前一季度倒数第二日 ==> 前1季度的前1天/当前季度前第二天
* @param timeExpression '00:00:00/23:59:59'
* @param baseTime 基础日期
* @return
*/
public static Date[] explainDateRangeExpression(String dateExpression, String timeExpression, Date baseTime){
//@TODO;
}
}
有了上面的方法,再来想想我们的表达式怎么定义:
- 用'/'字符来将时间区间分为两个部分,前半部分表示开始日期,后半部分表示截止日期,形如:~/~。
- 用Q,M,D,W,T 5个字母来分表代表季度,月度,日期,周,交易日。
- 用字母'+'或'-'带一个数字,来代表向前或向后计算几个单位,如Q-1表示上一季度,M-0表示当前月份,W+1表示下一周。
- '/'表达式的前半部分,取首字母,计算第一天,按顺序向后计算开始日期。
- 表达式后半部分取首字母的最后一天,按顺序向后开始计算。
- 如果'/'前后的表达式一样,则可以简写为前半部分即可。如M-1-D-1/M-1-D-1可以简写为M-1-D-1
- 以上字母可以随意组合使用。
啥意思呢?同样来举个栗子:
- M-1-D+1/M-1-D+1 表示为上个月2号至次月1号。
分解一下:
'/'之前,M-1(上个月1号)加 D+1(表示加上一天)=上个月2号。
'/'之后,M-1(上个月最后一天)在加上1天,表示 第二个月1号。 - Q-1-D-1/Q-1-D-1 前两季度最后一日-前一季度倒数第二日
分解一下:
'/'之前 Q-1(上一季度的第一天)-D-1(再减去一天),即表示为前两季度的最后一日
'/'之后 Q-1(上一季度的最后一天)-D-1(再减去一天),即表示为前一季度的倒数第二天
三.完整代码
现在要做的,无非就是如何去解析表达式。这里我直接去按位去解析字符,由于公式比较简单,自己使用因此没有做太多的校验。
废话不多说,直接上完整的代码吧!
/**
* 自定义日期范围表达式,格式如下:暂支持Q,M,D,W,T(季度,月度,日期,周,交易日)
* Q-1: 前一季度1日-前一季度最后一日
* M-1: 前月1日-前月最后一日
* Q-1-D-1/Q-0-D-1: 前两季度最后一日-前一季度倒数第二日 ==> 前1季度的前1天/当前季度前第二天
*/
@Component
public class DateExpressionUtil {
@Autowired
AttendanceSettingService attendanceSettingService;
@Autowired
private static DateExpressionUtil dateExpressionUtil;
@PostConstruct
public void init() {
dateExpressionUtil = this;
dateExpressionUtil.attendanceSettingService = this.attendanceSettingService;
}
/**
* 根据表达式获取日期
* @param dateExpression
* @return
*/
public static Date explainDateExpression(String dateExpression){
return calculate(dateExpression,null,0,null).getTime();
}
/**
* 根据表达式获取日期
* @param dateExpression
* @param baseTime 基础日期
* @return
*/
public static Date explainDateExpression(String dateExpression,Date baseTime){
return calculate(dateExpression,null,0,baseTime).getTime();
}
/**
* 检查规则执行日期是否是当前日期
* @param executeData
* @return
*/
public static boolean checkExecuteData(String executeData){
if(!StringUtils.isEmpty(executeData)){
Date execDate = explainDateExpression(executeData,null);
if(DateExpressionUtil.dateToStr(execDate).equals(DateExpressionUtil.dateToStr(getCurrentDate(null)))){
return true;
}
}
return false;
}
/**
* 解析单据发生时间或其他时间范围表达式
* @param dateExpression
* @param timeExpression '00:00:00/23:59:59'
* @param baseTime 基础日期
* @return
*/
public static Date[] explainDateRangeExpression(String dateExpression, String timeExpression, Date baseTime){
Date[] dateArray= new Date[2];
//解析表达式
String[] dataExpressionArr = dateExpression.split("/");
String[] timeFormatters = convertTimeExpressionToArray(timeExpression);
Calendar calendar ;
int calDateIndex=0;
for(String dateExpress : dataExpressionArr) {
//计算表达式
calendar = calculate(dateExpress, timeFormatters[calDateIndex], calDateIndex, baseTime);
dateArray[calDateIndex] = calendar.getTime();
calDateIndex++;
}
if(dateArray[1]==null){
//处理不带'/'的表达式,计算array[1]
dateArray[1] = calculate(dataExpressionArr[0],timeFormatters[1],1,baseTime).getTime();
}
return dateArray;
}
/**
* 获取当前月第一天,初始化时刻
* @param baseTime 基础日期
* @param timeFormatter
* @return
*/
private static Calendar getCurrentMonthMinDate(Date baseTime,String timeFormatter){
//当前月第一天
Calendar calendar=Calendar.getInstance();
if(baseTime!=null){
calendar.setTime(baseTime);
}
calendar.set(Calendar.DAY_OF_MONTH, 1);
//清空时刻
calendar =setCalendarTime(calendar, timeFormatter);
return calendar;
}
//获取当天日期,初始化时刻 00:00:00
private static Date getCurrentDate(Date baseTime){
return getCurrentDate(baseTime,null).getTime();
}
/**
* 获取当前日期,初始化时刻
* @param baseTime 基础日期
* @param timeFormatter 初始化时刻
* @return
*/
private static Calendar getCurrentDate(Date baseTime, String timeFormatter){
//当前月第一天
Calendar calendar=Calendar.getInstance();
if(baseTime!=null){
calendar.setTime(baseTime);
}
//清空时刻
calendar =setCalendarTime(calendar, timeFormatter);
return calendar;
}
/**
* 获取本周的第一天
* @param baseTime 基础日期
* @return String
* **/
private static Calendar getCurrentWeekStart(Date baseTime, String timeFormatter){
Calendar calendar=Calendar.getInstance();
if(baseTime!=null){
calendar.setTime(baseTime);
}
calendar.add(Calendar.DAY_OF_MONTH, -1); // -1 是因为周日是一周的第一天
calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
//清空时刻
calendar =setCalendarTime(calendar, timeFormatter);
return calendar;
}
/**
* 设置日期时刻
* @param calendar
* @param timeFormatter "00:00:00" , "23:59:59" ...
* @return
*/
private static Calendar setCalendarTime(Calendar calendar,String timeFormatter){
if(!StringUtils.isEmpty(timeFormatter)){
String[] str= timeFormatter.split(":");
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(str[0]));
calendar.set(Calendar.MINUTE, Integer.parseInt(str[1]));
calendar.set(Calendar.SECOND, Integer.parseInt(str[2]));
calendar.set(Calendar.MILLISECOND,0);
}else{
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE,0);
calendar.set(Calendar.SECOND,0);
calendar.set(Calendar.MILLISECOND,0);
}
return calendar;
}
/**
* 转换时间表达式为数组
* @param timeExpression
* @return
*/
private static String[] convertTimeExpressionToArray(String timeExpression){
if(StringUtils.isEmpty(timeExpression)){
return new String[]{"00:00:00","23:59:59"};
}else{
return timeExpression.split("/");
}
}
/**
* 解析单个表达式
* @param dateExpress
* @param timeFormatters
* @param calDateIndex
* @param baseTime
* @return
*/
private static Calendar calculate(String dateExpress,String timeFormatters, int calDateIndex, Date baseTime){
//解析起/止日期表达式
char[] chars= dateExpress.toCharArray();
Calendar calendar= null ;
char tempType='\0';
int operator = 0;
String tempValue="";
//按照字符解析
for(char ch : chars){
switch(ch){
case 'Q'://季度
case 'M'://月度
case 'W'://周
case 'D'://自然日
case 'T':
//交易日
calendar = calculate(calendar, tempType, operator, tempValue, timeFormatters,calDateIndex,baseTime);
tempType = ch;
operator=0;
tempValue="";
break;
case '-':
if(operator==0){
operator = -1;
}
break;
case '+':
if(operator==0){
operator = 1;
}
break;
default:
//做数字处理
if(StringUtils.isEmpty(tempValue)){
tempValue = String.valueOf(ch);
}else{
tempValue += String.valueOf(ch);
}
}
}
return calculate(calendar,tempType,operator,tempValue,timeFormatters,calDateIndex,baseTime);
}
/**
* 计算单个表达式
* @param calendar 指定日期
* @param dateType Q,M,D,T,W
* @param operator 1,-1
* @param value 自然数
* @param timeFormatter
* @param calDateIndex 计算位置 0:起始时间,1:结束时间
* @param baseTime
* @return
*/
private static Calendar calculate(Calendar calendar, char dateType, int operator, String value, String timeFormatter, int calDateIndex, Date baseTime) {
if (dateType != '\0' && operator!=0) {
//初始化日期
calendar = initCalendar(calendar,timeFormatter,dateType,baseTime);
switch (dateType) {
case 'Q':
//季度
calendar.add(Calendar.MONTH, operator * Integer.parseInt(value) * 3 - ( calendar.get(Calendar.MONTH) % 3));
break;
case 'M':
//月度
calendar.add(Calendar.MONTH, operator * Integer.parseInt(value));
break;
case 'W':
int week = calendar.get(Calendar.DAY_OF_WEEK);
//取周一
calendar.add(Calendar.DAY_OF_MONTH, -1 * (week-2));
//减去表达式中的周数
calendar.add(Calendar.DAY_OF_MONTH, operator * Integer.parseInt(value)*7);
break;
case 'D':
//自然日
calendar.add(Calendar.DAY_OF_MONTH, operator * Integer.parseInt(value));
break;
case 'T':
//交易日
calendar.setTime(dateExpressionUtil.attendanceSettingService.getNextNumMarketDay(calendar.getTime(),Integer.parseInt(value)+1)); //+1是因为从0开始,即包含当天的意思
break;
default:
}
//修正
calendar = fixedDate(calendar,dateType,calDateIndex);
}
return calendar;
}
/**
* 初始化日期
* @param calendar
* @param timeFormatter
* @param dateType
* @param baseTime
* @return
*/
private static Calendar initCalendar(Calendar calendar, String timeFormatter, char dateType, Date baseTime){
if(calendar==null) {
switch(dateType){
case 'Q':
case 'M':
calendar = getCurrentMonthMinDate(baseTime, timeFormatter);
break;
case 'W':
calendar = getCurrentWeekStart(baseTime,timeFormatter);
break;
case 'D':
case 'T':
calendar = getCurrentDate(baseTime,timeFormatter);
default:
}
}
return calendar;
}
/**
* 根据日期取给定日期的季度或月度最后一天
* @param calendar 给定日期
* @param dateType Q,M,D,T,W
* @param calDateIndex 时刻
* @return
*/
private static Calendar fixedDate(Calendar calendar, char dateType, int calDateIndex){
if(calDateIndex>0){
if (dateType != '\0') {
switch (dateType) {
case 'Q':
//取季度最后一天
calendar.add(Calendar.MONTH, 3);
calendar.add(Calendar.DAY_OF_MONTH, -1);
break;
case 'M':
//取月度最后一天
calendar.add(Calendar.MONTH, 1);
calendar.add(Calendar.DAY_OF_MONTH, -1);
break;
case 'W':
//相当于+6天
calendar.add(Calendar.DAY_OF_MONTH, 6);
break;
case 'D':
case 'T':
//@TODO
break;
default:
}
}
}
return calendar;
}
private static String dateToStr(Date date) {
return dateToStr(date, "yyyy-MM-dd HH:mm:ss");
}
private static String dateToStr(Date date, String format) {
return (new SimpleDateFormat(format)).format(date);
}
}
注:代码中并没有给出获取交易日的方法实现,当然这个需要系统自身来维护交易日,从而通过调用来获取,因此上述代码中,attendanceSettingService.getNextNumMarketDay 获取第几个交易日,是需要自己来实现的,这里就不在给出了。
当然了,上面代码中是支持时间的,时间的表达式就简单一些了,我们可以直接用00:00:00/23:59:59来表示即可,应该很好理解,就不详述了。
好了,只能送到这里了,看看针对上面的场景,我们最后抽象出来的公式是什么样的。
四.最终结果
回过头来,看看我们之前的场景,我们再用我们定义的公式,来解决这个问题:
再来点难度!今天依旧是10月2日(周一),假设各个业务线汇总的时间不一样,如下:
- A业务在每个季度的1号来汇总上一季度(Q-1)
- B业务在每月的第一天来汇总上一个月(M-1)的数据
- C业务在每月5号(M-0+D+4)汇总上个月15日-上月之后一天(M-1/M-2-D+15)的数据,在18日(M-0+D+17)汇总1-15日(M-1/M-2-D+15)的数据
- D业务在每月2日(M-0+D+1)来汇总上月2日-次月1日(M-1-D+1/M-1-D+1)的数据
- E业务在每月2日(M-0+D+1)汇总上上个月最后一天至上个月倒数第二天(M-1-D-1/M-1-D-1)的数据
- F业务在每周二(W-0+1)汇总上一周(W-1)的数据
- G业务在当天(D-0)汇总前一天的数据(D-1)
- H业务在每月第二个交易日(M-0+T+1)汇总上月的数据(M-1)
最后再少贴一些测试代码:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DateExpressUtilTest {
@Test
public void calcDateExpress2(){
//当月1号-当月25号
Date[] d1Arr = DateExpressionUtil.explainDateRangeExpression("M-0/M-1+D+25","00:00:00/23:59:59", DateUtil.parse("2020-05-27"));
Assert.assertEquals("2020-05-01 00:00:00",DateUtils.dateToStr(d1Arr[0]));
Assert.assertEquals("2020-05-25 23:59:59",DateUtils.dateToStr(d1Arr[1]));
}
@Test
public void calcTxnDateExpress(){
String expq0m1d4="Q-0+T+3";
Date expq0m1d4Result= DateExpressionUtil.explainDateExpression(expq0m1d4,DateUtil.parse("2019-04-11"));
Assert.assertEquals("2019-04-03 00:00:00",DateUtils.dateToStr(expq0m1d4Result));
String expq0m1d5="Q-0+T+7";
Date expq0m1d5Result= DateExpressionUtil.explainDateExpression(expq0m1d5,DateUtil.parse("2019-04-11"));
Assert.assertEquals("2019-04-10 00:00:00",DateUtils.dateToStr(expq0m1d5Result));
}
@Test
public void calcDateExpress4(){
//上一周
Date[] d3Arr = DateExpressionUtil.explainDateRangeExpression("W-1","00:00:00/23:59:59", DateUtil.parse("2020-09-25"));
Assert.assertEquals("2020-09-14 00:00:00",DateUtils.dateToStr(d3Arr[0]));
Assert.assertEquals("2020-09-20 23:59:59",DateUtils.dateToStr(d3Arr[1]));
}
@Test
public void calcDateExpress3(){
//每月第二个交易日执行
String expq0m1d4="W-0+D+6";
Date expq0m1d4Result= DateExpressionUtil.explainDateExpression(expq0m1d4,DateUtil.parse("2020-09-27"));
Assert.assertEquals("2020-09-27 00:00:00",DateUtils.dateToStr(expq0m1d4Result));
}
}