数据开发之离线计算_自定义调度时间表达式定义与解析

数据开发之离线计算_自定义调度时间表达式定义与解析

一、背景

常见的周期调度平台只能支持最简单的按天、按周、按月等自然时间粒度,但是每次调度要参与计算的数据的时间范围是不一定的。整天、整周、整月、周至今、月至今、近7天等都是比较常见简单的了。比如有人可能希望每次取当天的上一次每月3号到当天这个时间范围内的数据进行推数或计算。所以作为计算调度平台,为用户提供可以表达任意相对或绝对的一天的自定义计算开始&结束时间表达式是非常有必要的。

二、表达式函数

2.1 函数枚举

系统变量

${scheduleDt}

scheduleDt表示调度日期,也就是生产任务实际运行的日期。

自然周期始末函数

#yearFirstDt、#yearLastDt、#quarterFirstDt、#quarterLastDt、#monthFirstDt、#monthLastDt、#weekFirstDt、#weekLastDt

上述自然周期始末函数只有一个入参,这个入参必须是yyyy-MM-dd格式的日期字符串。可以是一个绝对日期字符串,如#yearFirstDt(‘2025-01-01’);也可以是一个调度日期表示的相对日期,如#yearFirstDt(${scheduleDt});也可以是一个嵌套的表达式函数,如#yearFirstDt(#yearFirstDt(‘2025-01-01’))。

差值函数

#add

该函数有三个入参,第一个入参必须是yyyy-MM-dd格式的日期字符串;第二个入参必须是用于表示差值的自然数;第三个入参数是用于表示差值的时间单位,只能是DAY、WEEK、MONTH、YEAR。如#add(${scheduleDt}, -2, ‘MONTH’)表示调度日期的两个月前的那天。

格式化函数

#fmt

该函数有两个参数,第一个入参必须是yyyy-MM-dd格式的日期字符串;第二个参数目前只能是’yyyy-MM-dd’,后续可以有其他枚举的扩展,如截取到月份、扩充到时分秒等。如#fmt(${scheduleDt},’yyyy-MM-dd’)。

上述函数可以无限嵌套实现获取任意想要的一个相对或绝对的时间

2.2 格式校验

编写一个java工具类,用来校验用户输入的计算开始&结束时间是否是符合格式标准的自定义表达式:

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
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StrategyTimeCheckUtil {
private static final String DATE_REGEX = "\\d{4}-\\d{2}-\\d{2}";

private static final String SCHEDULE_DT_REGEX = "\\$\\{scheduleDt\\}";

private static final String DATE_EXPR_REGEX = "#(yearFirstDt|yearLastDt|quarterFirstDt|quarterLastDt|monthFirstDt|monthLastDt|weekFirstDt|weekLastDt)\\((.*?)\\)";

private static final String ADD_EXPR_REGEX = "#add\\((.*?),(-?\\d+),'(BY_DAY|BY_MONTH)'\\)";

public static final String FMT_EXPR_REGEX = "#fmt\\((.*?),'yyyy-MM-dd'\\)";

private static boolean isValidDateFunctionExpression(String expression) {
return isValidExpression(expression.trim());
}

private static boolean isValidExpression(String expression) {
if (expression.matches(DATE_REGEX) || expression.matches(SCHEDULE_DT_REGEX)) {
return true;
}

if (expression.matches(DATE_EXPR_REGEX)) {
return validateDateExpression(expression);
}

if (expression.matches(ADD_EXPR_REGEX)) {
return validateAddExpression(expression);
}

if (expression.matches(FMT_EXPR_REGEX)) {
return validateFmtExpression(expression);
}
return false;
}

private static boolean validateDateExpression(String expression) {
Pattern pattern = Pattern.compile(DATE_EXPR_REGEX);
Matcher matcher = pattern.matcher(expression);
if (matcher.matches()) {
String innerExpression = matcher.group(2).trim();
return isValidExpression(innerExpression);
}
return false;
}

private static boolean validateFmtExpression(String expression) {
Pattern pattern = Pattern.compile(FMT_EXPR_REGEX);
Matcher matcher = pattern.matcher(expression);
if (matcher.matches()) {
String innerExpression = matcher.group(1).trim();
return isValidExpression(innerExpression);
}
return false;
}

private static boolean validateAddExpression(String expression) {
Pattern pattern = Pattern.compile(ADD_EXPR_REGEX);
Matcher matcher = pattern.matcher(expression);
if (matcher.matches()) {
String datePart = matcher.group(1).trim();
String unitType = matcher.group(3).trim();
return isValidExpression(datePart) && (unitType.equals("DAY") || unitType.equals("MONTH"));
}
return false;
}
}

三、表达式解析

用户按照上述自定义表达式定义好计算开始&结束时间后,任务每次实际调度执行时,还需要工具类将上述自定义函数表达式,转化为本次执行实际需要的日期。

其实就是一个一个枚举,在工具类中把所有定义函数的处理逻辑按一个一个工具方法写好:

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
public static String add(String dt, String dtTimeInterval, String timeInterval, Integer relativeTime) {
Date date = DateProcessManager.getDate(dt, dtTimeInterval);
TimeIntervalCalendarField calendarField = TimeIntervalCalendarField.getValue(timeInterval);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
if (Objects.equals(timeInterval, TimeIntervalCalendarField.BY_MONTH.getTimeInterval()) && isLastDayOfMonth(calendar)) {
calendar.set(5, 0);
} else {
calendar.add(calendarField.getFieldNum(), relativeTime * calendarField.getMagnification());
}

String res = DateProcessManager.transDate(dt, dtTimeInterval, calendar.getTime());
return res;
}

public static String fmt(String dateStr, String format) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
SimpleDateFormat sdf2 = new SimpleDateFormat(format);
return sdf2.format(sdf.parse(dateStr));
}

public static String getLastDayOfMonth(int year, int month) {
Calendar cal = Calendar.getInstance();
cal.set(1, year);
cal.set(2, month - 1);
cal.set(5, 1);
cal.setFirstDayOfWeek(2);
cal.setMinimalDaysInFirstWeek(4);
int lastDay = cal.getActualMaximum(5);
cal.set(5, lastDay);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String lastDayOfMonth = sdf.format(cal.getTime());
return lastDayOfMonth;
}

public static String getLastDayOfQuarter(String dateStr) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(dateStr);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMinimum(Calendar.DAY_OF_MONTH));
final int month = (calendar.get(Calendar.MONTH) / 3 + 1) * 3 - 1; // 季度末对应的月
calendar.set(Calendar.MONTH, month);
return getLastDayOfMonth(sdf.format(calendar.getTime()));
}

注意难点:

1.农历同比是个难点,也可以扩展开来。

2.月末最后一天取上个月日期是个难点,需要定义好规则,如3.31取上个月日期是不是应该取到2月份最后一天,还是直接取2.31。