
给定这样一个场景,现在我们要在特定的某一天里,来汇总业务系统中某一段时间内的数据。在这基础上再加上一个要求,那就是不同的业务类型数据,汇总时间及区间也不同。这样的需求场景看起来是很合理,那么在这样的背景要求下,汇总数据很简单,但是如何拿到动态的时间区间,并且我们的汇总时间能够动态的配置,怎么能够方便快速的满足需求呢?
一.需求场景
什么?需求看不懂?好吧是我没说明白!来来来,没有什么东西是用举个栗子解释不清楚的。
假设:今天是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号汇总的数据,怎么办?
先想想,再想想…其实…
二.方法及表达式定义
别想了,这里我来简单的说。我们需要一个公式,根据当前日期或者指定的一个日期,算出我们想要的时间区间,是不是就能够满足我们上面所有的情况?
那么好了,总结一下,其实需要的方法就这么四个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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(再减去一天),即表示为前一季度的倒数第二天
三.完整代码
现在要做的,无非就是如何去解析表达式。这里我直接去按位去解析字符,由于公式比较简单,自己使用因此没有做太多的校验。
废话不多说,直接上完整的代码吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | /** * 自定义日期范围表达式,格式如下:暂支持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)
最后再少贴一些测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | @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)); } } |