
给定这样一个场景,现在我们要在特定的某一天里,来汇总业务系统中某一段时间内的数据。在这基础上再加上一个要求,那就是不同的业务类型数据,汇总时间及区间也不同。这样的需求场景看起来是很合理,那么在这样的背景要求下,汇总数据很简单,但是如何拿到动态的时间区间,并且我们的汇总时间能够动态的配置,怎么能够方便快速的满足需求呢?
一.需求场景
什么?需求看不懂?好吧是我没说明白!来来来,没有什么东西是用举个栗子解释不清楚的。
假设:今天是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)); } }