想自由配置并动态获取日期区间?那就用一个表达式搞定
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号汇总的数据,怎么办?

先想想,再想想…其实…


二.方法及表达式定义

别想了,这里我来简单的说。我们需要一个公式,根据当前日期或者指定的一个日期,算出我们想要的时间区间,是不是就能够满足我们上面所有的情况?
那么好了,总结一下,其实需要的方法就这么四个:

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(再减去一天),即表示为前一季度的倒数第二天

三.完整代码

现在要做的,无非就是如何去解析表达式。这里我直接去按位去解析字符,由于公式比较简单,自己使用因此没有做太多的校验。
废话不多说,直接上完整的代码吧!

/**
 * 自定义日期范围表达式,格式如下:暂支持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)

最后再少贴一些测试代码:

@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));
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注