在项目开发中,特别是报表展示的应用场景,我们经常会涉及到一些时间段的处理情况。例如本周,本月,上周,上月这种human reading
的显示方式,其后台应转换为一个时间段,本文结合这个需求,提出一种可复用的方法,同时还包括在这个时间段内做一步sub-interval
的方法。
基本数据结构
ReportDateRequest
首先从用户角度看,需要接收用户的选择,是一个约定俗成的时间区间,或是自定义的。该Request就封装了用户的时间区间选择,既包括一些固定时间段,也可以是自定义时间段,具体定义如下:
public class ReportDateRequest {
/**
* 时间请求的类型
* @see ReportDateCondition
*/
private int type;
/**
* 自定义情况下的上限(最晚的值)
*/
private long customUpper;
/**
* 自定义情况下的下限(最早的值)
*/
private long customLower;
type的定义使用一个枚举值固定下来,并根据每个值的不同特性,可转化为不同的时间段,如下。
ReportDateCondition
如上所述,RDC定义了可以选择的约定俗成的时间段的基本含义,具体定义如下(未列出构造函数):
public enum ReportDateCondition {
/**
* 任意时间段
*/
CUSTOM(0),
/**
* 当日
*/
TODAY(1),
/**
* 本周
*/
THIS_WEEK(2),
/**
* 上周
*/
LAST_WEEK(20),
/**
* 本月
*/
THIS_MONTH(3),
/**
* 上月
*/
LAST_MONTH(30)
}
枚举值中的code,即用于给前端标记当前用户选择的是何种时间区间。
ReportTimeLimit
有了枚举值,我们就可以将其转化为一种时间上下限,该类就是定义了一种时间上下限,根据每个枚举值可以生成一个ReportTimeLimit类。另外为了满足一些报表需要同比,环比的要求,在该类中再加入同比(上年同时间段)和环比(上日/周/月同时间段)的定义。具体描述如下
public class ReportTimeLimit {
/**
* 当前时间上限
*/
long currentUpper;
/**
* 当前时间下限
*/
long currentLower;
/**
* 环比上限
*/
long momUpper;
/**
* 环比下限
*/
long momLower;
/**
* 同比上限
*/
long yoyUpper;
/**
* 同比下限
*/
long yoyLower;
}
利用Java8时间函数确定时间段
有了以上的数据结构,我们就可以将一个约定俗成的时间区间,转换为long型的两个值,分别对应时间上限和下限。这是通过ReportDateCondition
枚举中的方法实现的。
/**
* 时间类型的缺省方法不能获取时间段,如果调用的话返回异常
*
* @param reportDateRequest 仅限是custom时使用
* @return
*/
public ReportTimeLimit getReportTimeLimit(ReportDateRequest reportDateRequest) {
throw new AbstractMethodError();
}
注意此方法的默认实现是不允许调用的,因此每个枚举值要重载该方法获取包含不同上下限的ReportTimeLimit
类,下面一个一个来看。
当日
@Override
public ReportTimeLimit getReportTimeLimit(ReportDateRequest reportDateRequest) {
ReportTimeLimit res = new ReportTimeLimit();
res.currentUpper = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
res.currentLower = LocalDate.now().atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
res.yoyUpper = LocalDateTime.now().minusYears(1L).toEpochSecond(ZoneOffset.of("+8"));
res.yoyLower = LocalDate.now().minusYears(1L).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
res.momUpper = LocalDateTime.now().minusDays(1L).toEpochSecond(ZoneOffset.of("+8"));
res.momLower = LocalDate.now().minusDays(1L).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
return res;
}
此处利用Java8的新时间函数取时间,这里有一些非常好的,简单易懂的API可以构造需要的时间点,并返回long型的时间戳。
以下其他时间类型处理是类似的,只在API的选择上略有不同,不再赘述。
当周
@Override
public ReportTimeLimit getReportTimeLimit(ReportDateRequest reportDateRequest) {
ReportTimeLimit res = new ReportTimeLimit();
res.currentUpper = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
// 需要自己实现第一周的算法
TemporalAdjuster FIRST_OF_WEEK = TemporalAdjusters.ofDateAdjuster(localDate -> localDate.minusDays(localDate.getDayOfWeek().getValue() - DayOfWeek.MONDAY.getValue()));
res.currentLower = LocalDate.now().with(FIRST_OF_WEEK).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
res.yoyUpper = LocalDateTime.now().minusYears(1L).toEpochSecond(ZoneOffset.of("+8"));
res.yoyLower = LocalDate.now().minusYears(1L).with(FIRST_OF_WEEK).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
res.momUpper = LocalDateTime.now().minusWeeks(1L).toEpochSecond(ZoneOffset.of("+8"));
res.momLower = LocalDate.now().minusWeeks(1L).with(FIRST_OF_WEEK).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
return res;
}
上周
@Override
public ReportTimeLimit getReportTimeLimit(ReportDateRequest reportDateRequest) {
ReportTimeLimit res = new ReportTimeLimit();
// 需要自己实现第一周的算法
TemporalAdjuster FIRST_OF_WEEK = TemporalAdjusters.ofDateAdjuster(localDate -> localDate.minusDays(localDate.getDayOfWeek().getValue() - DayOfWeek.MONDAY.getValue()));
res.currentUpper = LocalDate.now().with(FIRST_OF_WEEK).atStartOfDay().toEpochSecond(ZoneOffset.of("+8")) - 1L;
res.currentLower = LocalDate.now().minusWeeks(1L).with(FIRST_OF_WEEK).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
return res;
}
本月
@Override
public ReportTimeLimit getReportTimeLimit(ReportDateRequest reportDateRequest) {
ReportTimeLimit res = new ReportTimeLimit();
res.currentUpper = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
res.currentLower = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
res.yoyUpper = LocalDateTime.now().minusYears(1L).toEpochSecond(ZoneOffset.of("+8"));
res.yoyLower = LocalDate.now().minusYears(1L).with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
res.momUpper = LocalDateTime.now().minusMonths(1L).toEpochSecond(ZoneOffset.of("+8"));
res.momLower = LocalDate.now().minusMonths(1L).with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
return res;
}
上月
@Override
public ReportTimeLimit getReportTimeLimit(ReportDateRequest reportDateRequest) {
ReportTimeLimit res = new ReportTimeLimit();
res.currentUpper = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay().toEpochSecond(ZoneOffset.of("+8")) - 1L;
res.currentLower = LocalDate.now().minusMonths(1L).with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay().toEpochSecond(ZoneOffset.of("+8"));
return res;
}
使用方法
在需要使用时间区间的方法中,用一个ReportDateRequest
作为参数传入,通过转换和调用对应方法获取ReportTimeLimit
即可从该对象中获取时间区间的上下限,用于各类数据库Criteria对象的值。
public ReturnResult<ComprehensiveOrderStat> getOrderStat( ReportDateRequest dateRequest,...) {
// ...
ReportDateCondition dateCondition = dateRequest.convertType();
ReportTimeLimit reportTimeLimit = dateCondition.getReportTimeLimit(dateRequest);
orderDetailCriteria.andOperator(
Criteria.where(KEY_OF_RELEASE_TIME).lte(reportTimeLimit.getCurrentUpper()),
Criteria.where(KEY_OF_RELEASE_TIME).gte(reportTimeLimit.getCurrentLower()));
// ...
}
获取下一级时间的间隔
需求
除了以上通过约定俗成的说法获取时间上下限之外,我们有时还需要在当前时间段内,获取一个下一级的时间间隔,例如
- 本日:获取每小时的时间间隔
- 本月/上月:获取每日的时间间隔
- 本周/上周:获取一周7天的时间间隔
- 自定义:获取自定义区间内每日的时间间隔
同时我们还希望在每个时间间隔上有个title,这样可以描述每个间隔的指标,例如一个该时间区间的柱状图或折线图。
核心方法
protected TreeMap<Long, IndexWithCoordinate> getInterval(long upper, long lower, CoordinateBuilder coordinateBuilder, DateStepType dateStepType, Class indexClazz) {
TreeMap<Long, IndexWithCoordinate> res = new TreeMap<>();
String[] coordinate = coordinateBuilder.getCoordinate();
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(lower);
int index = 0;
try {
while (calendar.getTimeInMillis() < upper) {
IndexWithCoordinate iwc = (IndexWithCoordinate) indexClazz.newInstance();
iwc.setCoordinate(coordinate[index]);
index++;
res.put(calendar.getTimeInMillis(), iwc);
calendar.add(dateStepType.getDateStepType(), 1);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return res;
}
- IndexWithCoordinate:这是一个抽象类,仅包含一个描述该指标的坐标名字,最常见的是一个日期,例如"2019-02-02"
- CoordinateBuilder: 这是一个函数式接口,用于返回一个数组,作为模板构造方法的返回值
- DateStepType: 这是一个函数式接口,用于返回一个Calendar中的日期步进值
- 返回结果的map,是一个有序的map,key为每个间隔的时间下限,value为一个继承了IndexWithCoordinate的具体类
- 具体做法是这样的,首先用lower时间构造一个Calendar对象,然后逐步按DateStepType的返回值步进,每个时间构造一个IndexWithCoordinate对象,并通过CoordinateBuilder的模板找到具体的指标名字进行填充,最后放到返回的有序map中,直到Calendar对象的值超过upper为止
- 由该方法的几个参数配合,保证CoordinateBuilder的结果数组不会越界
使用方法
这个有序map可用于手工聚合时间类的数据结构,主要依据以下方法
private Long getSection(Long[] interval, long time) {
Long res = interval[0];
for (Long single : interval) {
if (time > single) {
res = single;
continue;
}
break;
}
return res;
}
interval数组即有序map的keyset转换的数组(一组时间戳),time是任意一个时间,通过遍历的方式可以获取这个time落到了哪个区间上,并返回该区间的下限。