Java8日期API实现多时间段匹配

4,044 阅读5分钟

背景

需求描述

在监控系统中,触犯规则并进行告警时,需要根据特定时间点(如每天的00:00-06:00,或工作日的22:00-24:00等)进行特殊处理。该需求包含以下2点:

  1. 设计一个存储格式来表示特定的时间段
  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())