JAVA8 中 LocalDateTime 任意时间组合匹配当前时间 now() 触发条件

2018/8/3 源自  Java

LocalDateTime 是 jdk8 新添加的 API,改变了以往 java.util.Date 的弊端,进行对年月日、时分秒进行重构,不仅 API 看着更直观,使用起来也更方便。

jdk8 以前的版本 java.util.Date 和 SimpleDateFormat 是非线程安全的,我们再使用的过程中,一般会配合 ThreadLocal 使用,保证在并发环境中,时间戳不会被其他线程篡改。LocalDateTime 天生就是线程安全的,并且时间戳一旦初始化,就不能进行更改,从根本上保证了多线程并发的支持。

让我们看一下官方对 API 的解释:

/**
 * A date-time without a time-zone in the ISO-8601 calendar system,
 * such as {@code 2007-12-03T10:15:30}.
 * <p>
 * {@code LocalDateTime} is an immutable date-time object that represents a date-time,
 * often viewed as year-month-day-hour-minute-second. Other date and time fields,
 * such as day-of-year, day-of-week and week-of-year, can also be accessed.
 * Time is represented to nanosecond precision.
 * For example, the value "2nd October 2007 at 13:45.30.123456789" can be
 * stored in a {@code LocalDateTime}.
 * <p>
 * This class does not store or represent a time-zone.
 * Instead, it is a description of the date, as used for birthdays, combined with
 * the local time as seen on a wall clock.
 * It cannot represent an instant on the time-line without additional information
 * such as an offset or time-zone.
 * <p>
 * The ISO-8601 calendar system is the modern civil calendar system used today
 * in most of the world. It is equivalent to the proleptic Gregorian calendar
 * system, in which today's rules for leap years are applied for all time.
 * For most applications written today, the ISO-8601 rules are entirely suitable.
 * However, any application that makes use of historical dates, and requires them
 * to be accurate will find the ISO-8601 approach unsuitable.
 *
 * <p>
 * This is a <a href="{@docRoot}/java/lang/doc-files/ValueBased.html">value-based</a>
 * class; use of identity-sensitive operations (including reference equality
 * ({@code ==}), identity hash code, or synchronization) on instances of
 * {@code LocalDateTime} may have unpredictable results and should be avoided.
 * The {@code equals} method should be used for comparisons.
 *
 * @implSpec
 * This class is immutable and thread-safe.
 *
 * @since 1.8
 */

使用 LocalDateTime 创建一个当前时间

/**
 * Obtains the current date-time from the system clock in the default time-zone.
 * <p>
 * This will query the {@link Clock#systemDefaultZone() system clock} in the default
 * time-zone to obtain the current date-time.
 * <p>
 * Using this method will prevent the ability to use an alternate clock for testing
 * because the clock is hard-coded.
 *
 * @return the current date-time using the system clock and default time-zone, not null
 */
public static LocalDateTime now() {
    return now(Clock.systemDefaultZone());
}

把当前时间格式化

LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
String text = date.format(formatter);
LocalDate parsedDate = LocalDate.parse(text, formatter);

具体更多的 API 自己可以在 jdk8 的源码中阅读。不明白的地方也可以在下方评论。

言归正传,我们有一个需求,需要前端自定义出一组时间组合,后端按照这套时间组合,进行当前时间验证,是否符合触发条件。需求的场景很多,例如当收到一封邮件,用户并不想在预设值的时间内提醒;还有在预设值的时间内,用户想收到某些自定义的提示等等。

LocalDateTime 中把年月日、时分秒进行枚举化,来看一下 LocalDateTime.get() 方法

/**
 * Gets the value of the specified field from this date-time as an {@code int}.
 * <p>
 * This queries this date-time for the value of the specified field.
 * The returned value will always be within the valid range of values for the field.
 * If it is not possible to return the value, because the field is not supported
 * or for some other reason, an exception is thrown.
 * <p>
 * If the field is a {@link ChronoField} then the query is implemented here.
 * The {@link #isSupported(TemporalField) supported fields} will return valid
 * values based on this date-time, except {@code NANO_OF_DAY}, {@code MICRO_OF_DAY},
 * {@code EPOCH_DAY} and {@code PROLEPTIC_MONTH} which are too large to fit in
 * an {@code int} and throw a {@code DateTimeException}.
 * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}.
 * <p>
 * If the field is not a {@code ChronoField}, then the result of this method
 * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
 * passing {@code this} as the argument. Whether the value can be obtained,
 * and what the value represents, is determined by the field.
 *
 * @param field  the field to get, not null
 * @return the value for the field
 * @throws DateTimeException if a value for the field cannot be obtained or
 *         the value is outside the range of valid values for the field
 * @throws UnsupportedTemporalTypeException if the field is not supported or
 *         the range of values exceeds an {@code int}
 * @throws ArithmeticException if numeric overflow occurs
 */
@Override
public int get(TemporalField field) {
    if (field instanceof ChronoField) {
        ChronoField f = (ChronoField) field;
        return (f.isTimeBased() ? time.get(field) : date.get(field));
    }
    return ChronoLocalDateTime.super.get(field);
}
    
-------

private int get0(TemporalField field) {
    switch ((ChronoField) field) {
        case NANO_OF_SECOND: return nano;
        case NANO_OF_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'NanoOfDay' for get() method, use getLong() instead");
        case MICRO_OF_SECOND: return nano / 1000;
        case MICRO_OF_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'MicroOfDay' for get() method, use getLong() instead");
        case MILLI_OF_SECOND: return nano / 1000_000;
        case MILLI_OF_DAY: return (int) (toNanoOfDay() / 1000_000);
        case SECOND_OF_MINUTE: return second;
        case SECOND_OF_DAY: return toSecondOfDay();
        case MINUTE_OF_HOUR: return minute;
        case MINUTE_OF_DAY: return hour * 60 + minute;
        case HOUR_OF_AMPM: return hour % 12;
        case CLOCK_HOUR_OF_AMPM: int ham = hour % 12; return (ham % 12 == 0 ? 12 : ham);
        case HOUR_OF_DAY: return hour;
        case CLOCK_HOUR_OF_DAY: return (hour == 0 ? 24 : hour);
        case AMPM_OF_DAY: return hour / 12;
    }
    throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
}

通过 LocalDateTime.get() 方法可以很轻易的获取指定时间的年月日、时分秒,并且转换为 int 类型,进行大小判断。接下来是我们的具体实现:

我们先定义时间过滤的 Filter

static interface Filter{
    public boolean accepts(LocalDateTime ldt);
}

根据 ChronoField 实现一个可以进行时间比较的 FilterImpl

static class ChronoFieldFilter implements Filter{
    private ChronoField field;
    int begin;
    int end;

    ChronoFieldFilter(ChronoField field, int begin, int end){
        this.field = field;
        this.begin = begin;
        this.end = end;
    }

    @Override
    public boolean accepts(LocalDateTime datetime) {
        long value = datetime.get(field);
        return ((int)value)>=begin && ((int)value)<=end;
    }
}

另外实现一个对时分秒的比较 Filter

static class TimeRangeFilter implements Filter{
    LocalTime beginTime;
    LocalTime endTime;

    TimeRangeFilter(LocalTime beginTime, LocalTime endTime){
        this.beginTime = beginTime;
        this.endTime = endTime;
    }

    @Override
    public boolean accepts(LocalDateTime ldt) {
        LocalTime currTime = ldt.toLocalTime();
        if ( currTime.compareTo(beginTime)>=0 && currTime.compareTo(endTime)<=0 ) {
            return true;
        }
        return false;
    }

}

我们从自定义的 Json 中转换需要的年月日、时分秒,格式定义如下:

{"TimeRange":["00:00:00-23:59:59"],"DayOfWeek":["1","2","3","4","5","6","7"]}

加载 Json 并进行 ChronoField 转换

private Filter[] filters;

public DateTimeFilter(Map filterMap){
    List<Filter> result = new ArrayList<>();

    parseFilter(result, (List)filterMap.get("DayOfYear"), ChronoField.DAY_OF_YEAR);
    parseFilter(result, (List)filterMap.get("DayOfMonth"), ChronoField.DAY_OF_MONTH);
    parseFilter(result, (List)filterMap.get("DayOfWeek"), ChronoField.DAY_OF_WEEK);
    parseFilter(result, (List)filterMap.get("MonthOfYear"), ChronoField.MONTH_OF_YEAR);
    parseFilter(result, (List)filterMap.get("AlignedWeekOfYear"), ChronoField.ALIGNED_WEEK_OF_YEAR);
    parseFilter(result, (List)filterMap.get("AlignedWeekOfMonth"), ChronoField.ALIGNED_WEEK_OF_MONTH);
    parseTimeRangeFilters(result, (JSONArray)filterMap.get("TimeRange"));

    filters = result.toArray(new Filter[result.size()]);
}

TimeRange DayOfWeek 等是属于 AND 关系,DayOfWeek 里面的元素互相是 OR 关系。所以在解析 DayOfWeek 元素的时候,要把同等级别的元素进行合并。

static class OrFilter implements Filter{
    private List<Filter> filters;

    public OrFilter(List<Filter> filters) {
        this.filters = filters;
    }

    @Override
    public boolean accepts(LocalDateTime ldt) {
        boolean result = false;
        if( null==filters || filters.isEmpty()) {
            return result;
        }
        for( Filter f:filters ) {
            if( f.accepts(ldt) ) {
                return true;
            }
        }
        return result;
    }
}

这样,Json 数据解析后,我们会获得一组不同时间等级的 Filter。(年月日,划分为三个等级),同等级别进行所有 Filter 遍历,如果 Accept 返回 true。

我们可以根据这样的 JSON 体任意拼装组合时间,以达到我们想要的“自定义”效果。