基于Java8的可复用时间区间获取方法

在项目开发中,特别是报表展示的应用场景,我们经常会涉及到一些时间段的处理情况。例如本周,本月,上周,上月这种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;
}
  1. IndexWithCoordinate:这是一个抽象类,仅包含一个描述该指标的坐标名字,最常见的是一个日期,例如"2019-02-02"
  2. CoordinateBuilder: 这是一个函数式接口,用于返回一个数组,作为模板构造方法的返回值
  3. DateStepType: 这是一个函数式接口,用于返回一个Calendar中的日期步进值
  4. 返回结果的map,是一个有序的map,key为每个间隔的时间下限,value为一个继承了IndexWithCoordinate的具体类
  5. 具体做法是这样的,首先用lower时间构造一个Calendar对象,然后逐步按DateStepType的返回值步进,每个时间构造一个IndexWithCoordinate对象,并通过CoordinateBuilder的模板找到具体的指标名字进行填充,最后放到返回的有序map中,直到Calendar对象的值超过upper为止
  6. 由该方法的几个参数配合,保证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落到了哪个区间上,并返回该区间的下限。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352