背景
需求描述
在监控系统中,触犯规则并进行告警时,需要根据特定时间点(如每天的00:00-06:00,或工作日的22:00-24:00等)进行特殊处理。该需求包含以下2点:
- 设计一个存储格式来表示特定的时间段
- 需要判断某个时间是否在这个时间范围内
需求分析
一般特定的时间段有以下特征:
- 指定月份列表,如: 1月、6月、12月等
- 指定每月的日期列表,如: 1日、10日、30日等
- 指定周几的列表,如: 周一、周三、周五等
- 具体某一天的时间段列表,如:09:00-18:00、22:00-24:00等
因为我们关注的是具体某天的时间段,因此月份、日期、周几等表示都简单的使用数据来表示,并不需要支持范围(当然也可以根据需要来进行修改)
设计开发
存储格式
我们使用Json格式来存储该时间范围的规则,这样就可以直接使用Json反序列化来转化为对象,然后调用方法判断某个时间是否在该范围内.
以下为java对象的属性:
/**
* 月份
*/
private List<Integer> months;
/**
* 每月的日期列表, 如1号,5号可表示为: 1,5
*/
private List<Integer> daysOfMonth;
/**
* 表示周几的列表
*/
private List<Integer> daysOfWeek;
/**
* 时间段表达式列表
* 单个表达式如: 10:00-20:00
*/
private List<String> timeRanges;
json格式的例子如下:
{"daysOfWeek":[1,3,6],"timeRanges":["09:00-18:00","22:00-24:00"]}
其中 months, daysOfMonth, daysOfWeek等属性值可以为空,表示忽略该属性值; 若这3个值都为空,则表示每天 这3个值的是and的关系,需要都满足,具体看下面的代码
由于我们需要支持多个时间段,因此需要使用json数组表示多个TimeSection, json格式如下:
[{"daysOfWeek":[1,2,3,4,5,6],"timeRanges":["20:00-24:00","00:00-09:00"]},{"daysOfWeek":[7],"timeRanges":["00:00-24:00"]}]
以上的区间满足了表示非工作时间的时间范围
通过如下代码转换成TimeSection对象(使用的是fastjson库)
List<TimeSection> timeSectionList = JSON.parseArray(json, TimeSection.class);
使用Java8日期API进行日期判断
Java8表示日期时间的类有:
- LocalDate: 表示日期(不含时间)
- LocalTime: 表示时间(不含日期)
- LocalDateTime: 同时包含日期和时间(LocalDateTime类中包含了2个属性LocalDate和LocalTime) 由于我们判断的时间段有日期和时间,因此使用LocalDateTime类
生成LocalDateTime:
// 当前时间
LocalDateTime.now()
// 以某个字符串形式的日期格式转换成LocalDateTime
// 使用LocalDateTime.parse(String text, DateTimeFormatter formatter)
// 如:
LocalDateTime.parse("2020-06-13 10:10:10", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
使用LocalDateTime的方法来判断月份、日期、周几等的判断
// 获取月份
Month month = localDateTime.getMonth();
// 获取日期
int dayOfMonth = localDateTime.getDayOfMonth();
// 获取周几
DayOfWeek dayOfWeek = localDateTime.getDayOfWeek()
其中Month,DayOfWeek为枚举类,通过.getValue()方法来获取int类型
// 枚举转换为数字: FEBRUARY -> 2, APRIL -> 4
month.getValue()
// 枚举转换为数据: MONDAY -> 1, SUNDAY -> 7
dayOfWeek.getValue()
把具体的时间转换成它在一天中的毫秒数(long)
// 这里的timePat为: HH:mm, timeValue可为:11:00...
TemporalAccessor temporalAccessor = DateTimeFormatter.ofPattern(timePat).parse(timeValue);
// 获取一天已经过去的毫秒数
temporalAccessor.getLong(ChronoField.MILLI_OF_DAY);
其中:
- Temporal: 是所有时间的超级接口
- TemporalAccessor : 只提供只读版本的接口
通过temporalAccessor.getLong(ChronoField.MILLI_OF_DAY)获取某个时间点在一天中过去的毫秒数来做时间判断非常合适
startTime的毫秒数 < 当前时间的毫秒数 < endTime的毫秒数
时间范围抽象
我们存储的时间范围为类似: 11:00-20:00 这样,而实际判断过程中我们需要将开始时间和结束时间提取出来, 因此需要抽象成如下的对象:
public static class TimeRange {
/**
* 开始时间段
* HH:mm格式
*/
private String timeStart;
/**
* 结束时间段
* HH:mm格式
*/
private String timeEnd;
// ...
}
在做匹配的时候通过字符串处理进行timeStart和timeEnd的赋值
附录
完整代码
/**
* 表示时间区间
*/
@Data
public class TimeSection {
/**
* 时间格式
*/
private final transient static String TIME_PATTERN = "HH:mm";
/**
* 月份
*/
private List<Integer> months;
/**
* 每月的日期列表, 如1号,5号可表示为: 1,5
*/
private List<Integer> daysOfMonth;
/**
* 表示周几的列表
*/
private List<Integer> daysOfWeek;
/**
* 时间段表达式列表
* 单个表达式如: 10:00-20:00
*/
private List<String> timeRanges;
@Data
@AllArgsConstructor
public static class TimeRange {
/**
* 开始时间段
* HH:mm格式
*/
private String timeStart;
/**
* 结束时间段
* HH:mm格式
*/
private String timeEnd;
/**
* 某个时间是否在该时间范围内
* @param localDateTime
* @return
*/
public boolean isIn(LocalDateTime localDateTime) {
String comparedCurrentHourMin = localDateTime.format(DateTimeFormatter.ofPattern(TIME_PATTERN));
long comparedMilliSeconds = getMillisOfDay(comparedCurrentHourMin, TIME_PATTERN);
long startMilliSeconds = getMillisOfDay(timeStart, TIME_PATTERN);
long endMilliSeconds = getMillisOfDay(timeEnd, TIME_PATTERN);
return comparedMilliSeconds >= startMilliSeconds && comparedMilliSeconds < endMilliSeconds;
}
}
/**
* 获取某一个时间在一天中的毫秒数
* @param timeValue
* @param timePat
* @return
*/
public static long getMillisOfDay(String timeValue, String timePat) {
return DateTimeFormatter.ofPattern(timePat).parse(timeValue).getLong(ChronoField.MILLI_OF_DAY);
}
/**
* 获取时间范围对象列表
* @return
*/
@JSONField(serialize = false)
public List<TimeRange> getRanges() {
List<TimeRange> resultList = new ArrayList<>(4);
if (CollectionUtils.isNotEmpty(timeRanges)) {
for (String timeSection : timeRanges) {
String[] array = timeSection.split("-");
if (array.length != 2) {
continue;
}
resultList.add(new TimeRange(array[0], array[1]));
}
}
return resultList;
}
/**
* 某一个时间是否在范围内
* @param localDateTime
* @return
*/
public boolean isCurrentIn(LocalDateTime localDateTime) {
// check month
if (CollectionUtils.isNotEmpty(months)) {
if (!months.contains(localDateTime.getMonth().getValue())) {
return false;
}
}
// check day of month
if (CollectionUtils.isNotEmpty(daysOfMonth)) {
if (!daysOfMonth.contains(localDateTime.getDayOfMonth())) {
return false;
}
}
// check day of week
if (CollectionUtils.isNotEmpty(daysOfWeek)) {
// localDateTime.getDayOfWeek() is Enum And it's getValue() is the dayofweek int value.
if (!daysOfWeek.contains(localDateTime.getDayOfWeek().getValue())) {
return false;
}
}
// check time range
List<TimeRange> ranges = getRanges();
if (CollectionUtils.isNotEmpty(ranges)) {
for (TimeRange timeRange : ranges) {
if (timeRange.isIn(localDateTime)) {
// satisfy one range is true
return true;
}
}
}
return false;
}
}
如何使用
String json = "{\"daysOfWeek\":[1,3,6],\"timeRanges\":[\"09:00-18:00\",\"22:00-24:00\"]}";
TimeSection timeSection = JSON.parseObject(json, TimeSection.class);
timeSection.isCurrentIn(LocalDateTime.now())