基于 joda-time 将时间范围按自然天、周、月切割

249 阅读3分钟

背景

最近项目中遇到了「按照时间范围」以及「用户所处时区」查询统计的需求,服务端需要根据用户选择的时间范围,自动切割成按「天」「周」「月」的时间维度进行数据统计。

  • 如果时间范围在 30 天之内,则自动按「自然天」统计
  • 如果时间范围在 90 天之内,则自动按「自然周」统计
  • 大于 90 天自动按「自然月」统计

技术实现

项目里用 joda-time 二方包比较多,jdk 自带的日期工具用的比较少,所以就打算用 joda-time 2.10.14版本 来实现

前置信息

  1. 入参
  • TimeRange: 用户选择的时间范围,里面包含有 start-开始时间的时间戳,end-结束时间的时间戳
  • zoneId: 用户所处的时区字符串,比如巴黎时区 Europe/paris,上海时区 Asia/Shanghai,可以将其通过 Java 自带的 ZoneId.of() 静态方法转为ZoneId对象
  1. 返回值

List<TimeRange>: 里面是切割后的时间范围

实体类

  1. TimeRange.java
private Long start; //开始时间的时间戳
private Long end; //结束时间的时间戳

按天切割

核心实现分析

  1. 首先基于时区,根据开始时间算出该时区的时间
DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId));

startDateTimeWithZone 作为接下去计算所用到的基准时间。

  1. 基于这个基准时间,可以得出该时区自然天的结束时间
long dayMaxMills = startDateTimeWithZone.millisOfDay().withMaximumValue().getMillis(); // 开始时间所处的天的毫秒数的最大值(最小值是 withMinimumValue)

或者

long nextDayStart = startDateTimeWithZone.plusDays(1).withTimeAtStartOfDay().getMillis(); // 明天零点的时间戳
long dayMaxMills = nextDayStart - 1; // 明天零点时间戳减去 1 毫秒,就是当天的最大时间戳

这里选择了第 2 种方式来计算结束时间,因为每个自然天都需要知道每个自然天的开始时间

完整实现

public List<TimeRange> divide(TimeRange range, ZoneId zoneId) {
    List<TimeRange> timeRanges = Lists.newArrayList();
    long start = range.getStart();
    long end = range.getEnd();
    while (start < end) { // start会不断的往上增加,直到超出用户选择的结束时间
        DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId()));
        long nextStart = startDateTimeWithZone.plusDays(1).withTimeAtStartOfDay().getMillis();
        TimeRange tr = new TimeRange(start, Math.min(nextStart - 1, end));
        timeRanges.add(tr);
        start = nextStart;
    }
    return timeRanges;
}

测试 main 方法

TimeRange timeRange = new TimeRange();
timeRange.setStart(DateTime.parse("2022-10-01T00:00:00").getMillis());
timeRange.setEnd(DateTime.parse("2022-10-25T00:00:00").getMillis());
List<TimeRange> list = divide(timeRange, ZoneId.of("Asia/Shanghai"));
System.out.println(list);

按周切割

核心实现分析

  1. 首先基于时区,根据开始时间算出该时区的时间
DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId())).withDayOfWeek(DateTimeConstants.MONDAY); // 指定周一为每周的第一天

startDateTimeWithZone 作为接下去计算所用到的基准时间。

  1. 基于这个基准时间,可以得出该时区自然周的结束时间
long nextStart = startDateTimeWithZone.plusWeeks(1).dayOfWeek().withMinimumValue().millisOfDay().withMinimumValue().getMillis();
long endDayMaxMillsOfCurrentWeek = nextStart - 1; // 本周的结束时间

完整实现

public static List<TimeRange> divide(TimeRange range, ZoneId zoneId) {
    List<TimeRange> timeRanges = Lists.newArrayList();
    long start = range.getStart();
    long end = range.getEnd();
    while (start < end) {
        DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId())).withDayOfWeek(DateTimeConstants.MONDAY);
        long nextStart = startDateTimeWithZone.plusWeeks(1).dayOfWeek().withMinimumValue().millisOfDay().withMinimumValue().getMillis();
        TimeRange tr = new TimeRange(start, Math.min(nextStart - 1, end));
        timeRanges.add(tr);
        start = nextStart;
    }
    return timeRanges;
}

测试 main 方法

TimeRange timeRange = new TimeRange();
timeRange.setStart(DateTime.parse("2022-09-01T00:00:00").getMillis());
timeRange.setEnd(DateTime.parse("2022-10-25T00:00:00").getMillis());
List<TimeRange> list = divide(timeRange, ZoneId.of("Asia/Shanghai"));
System.out.println(list);

按月切割

核心实现分析

  1. 首先基于时区,根据开始时间算出该时区的时间
DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId()));

startDateTimeWithZone 作为接下去计算所用到的基准时间。

  1. 基于这个基准时间,可以得出该时区自然月的结束时间
long nextStart = startDateTimeWithZone.plusMonths(1).dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().getMillis();
long endDayMaxMillsOfCurrentMonth = nextStart - 1; // 本月的结束时间

完整实现

public static List<TimeRange> divide(TimeRange range, ZoneId zoneId) {
    List<TimeRange> timeRanges = Lists.newArrayList();
    long start = range.getStart();
    long end = range.getEnd();
    while (start < end) {
        DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId()));
        long nextStart = startDateTimeWithZone.plusMonths(1).dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().getMillis();
        TimeRange tr = new TimeRange(start, Math.min(nextStart - 1, end));
        timeRanges.add(tr);
        start = nextStart;
    }
    return timeRanges;
}

测试 main 方法

public static void main(String[] args) {
    TimeRange timeRange = new TimeRange();
    timeRange.setStart(DateTime.parse("2022-02-01T00:00:00").getMillis());
    timeRange.setEnd(DateTime.parse("2022-10-25T00:00:00").getMillis());
    List<TimeRange> list = divide(timeRange, ZoneId.of("Asia/Shanghai"));
    System.out.println(list);
}

总结

  1. 从代码上也可以看出,joda time的时间处理大同小异
  2. 按周切割的时候,需要注意每个时区可能每周的开始时间不一样,国际惯例似乎是默认周一开始