想自由配置并动态获取日期区间?那就用一个表达式搞定
By: Date: 2020年9月29日 Categories: 程序

给定这样一个场景,现在我们要在特定的某一天里,来汇总业务系统中某一段时间内的数据。在这基础上再加上一个要求,那就是不同的业务类型数据,汇总时间及区间也不同。这样的需求场景看起来是很合理,那么在这样的背景要求下,汇总数据很简单,但是如何拿到动态的时间区间,并且我们的汇总时间能够动态的配置,怎么能够方便快速的满足需求呢?

一.需求场景

什么?需求看不懂?好吧是我没说明白!来来来,没有什么东西是用举个栗子解释不清楚的。

假设:今天是10月2日(周一),我们的不同的业务线要在今天来汇总不同日期区间的数据

  1. A业务在今天来汇总上一季度的数据(7月1日-9月30日)
  2. B业务在今天来汇总上一个月(9月1日-9月30日)的数据
  3. C业务在今天分别汇总上个月上半旬及下半旬的数据,即(1日-15日,16日-30日,不同的月份最后一天不同)
  4. D业务在今天来汇总上月2日-次月1日的数据(9月2日-10月1日)
  5. E业务在今天汇总上上个月最后一日至上个月倒数第二日的数据(8月31日-9月29日)
  6. F业务在今天汇总上一周的数据(9月24日-9月30日)
  7. G业务在今天汇总前一天的数据(10月1日)

这好像没什么嘛!

那再来点有难度的!今天依旧是10月2日(周一),假设各个业务线汇总的时间不一样,如下:

  1. A业务在每个季度的1号来汇总上一季度(7月1日汇总4月1日-6月30日)
  2. B业务在每月的第一天来汇总上一个月(9月1日-9月30日)的数据(10月1日汇总9月1日-30日)
  3. C业务在5号汇总上个月15日-上月之后一天的数据,在18日汇总1-15日的数据,即(10月5日汇总9月16-30日,10月18日汇总10月1日-15日)
  4. D业务在每月2日来汇总上月2日-次月1日的数据(10月2日汇总9月2日-10月1日)
  5. E业务在每月2日汇总上上个月最后一天至上个月倒数第二天的数据(10月2日汇总8月31日-9月29日)
  6. F业务在每周二汇总上一周的数据(10月3日汇总9月24日-9月30日)
  7. G业务在当天汇总前一天的数据(10月2日汇总10月1日)
  8. 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;
    }
}

有了上面的方法,再来想想我们的表达式怎么定义:

  1. 用'/'字符来将时间区间分为两个部分,前半部分表示开始日期,后半部分表示截止日期,形如:~/~。
  2. 用Q,M,D,W,T 5个字母来分表代表季度,月度,日期,周,交易日。
  3. 用字母'+'或'-'带一个数字,来代表向前或向后计算几个单位,如Q-1表示上一季度,M-0表示当前月份,W+1表示下一周。
  4. '/'表达式的前半部分,取首字母,计算第一天,按顺序向后计算开始日期。
  5. 表达式后半部分取首字母的最后一天,按顺序向后开始计算。
  6. 如果'/'前后的表达式一样,则可以简写为前半部分即可。如M-1-D-1/M-1-D-1可以简写为M-1-D-1
  7. 以上字母可以随意组合使用。

啥意思呢?同样来举个栗子:

  1. M-1-D+1/M-1-D+1 表示为上个月2号至次月1号。
    分解一下:
    '/'之前,M-1(上个月1号)加 D+1(表示加上一天)=上个月2号。
    '/'之后,M-1(上个月最后一天)在加上1天,表示 第二个月1号。
  2. 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日(周一),假设各个业务线汇总的时间不一样,如下:

  1. A业务在每个季度的1号来汇总上一季度(Q-1)
  2. B业务在每月的第一天来汇总上一个月(M-1)的数据
  3. 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)的数据
  4. D业务在每月2日(M-0+D+1)来汇总上月2日-次月1日(M-1-D+1/M-1-D+1)的数据
  5. E业务在每月2日(M-0+D+1)汇总上上个月最后一天至上个月倒数第二天(M-1-D-1/M-1-D-1)的数据
  6. F业务在每周二(W-0+1)汇总上一周(W-1)的数据
  7. G业务在当天(D-0)汇总前一天的数据(D-1)
  8. 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));
    }
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注