背景
最近项目中遇到了「按照时间范围」以及「用户所处时区」查询统计的需求,服务端需要根据用户选择的时间范围,自动切割成按「天」「周」「月」的时间维度进行数据统计。
- 如果时间范围在 30 天之内,则自动按「自然天」统计
- 如果时间范围在 90 天之内,则自动按「自然周」统计
- 大于 90 天自动按「自然月」统计
技术实现
项目里用 joda-time 二方包比较多,jdk 自带的日期工具用的比较少,所以就打算用 joda-time 2.10.14版本 来实现
前置信息
- 入参
- TimeRange: 用户选择的时间范围,里面包含有 start-开始时间的时间戳,end-结束时间的时间戳
- zoneId: 用户所处的时区字符串,比如巴黎时区 Europe/paris,上海时区 Asia/Shanghai,可以将其通过 Java 自带的 ZoneId.of() 静态方法转为ZoneId对象
- 返回值
List<TimeRange>: 里面是切割后的时间范围
实体类
- TimeRange.java
private Long start; //开始时间的时间戳
private Long end; //结束时间的时间戳
按天切割
核心实现分析
- 首先基于时区,根据开始时间算出该时区的时间
DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId));
startDateTimeWithZone 作为接下去计算所用到的基准时间。
- 基于这个基准时间,可以得出该时区自然天的结束时间
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);
按周切割
核心实现分析
- 首先基于时区,根据开始时间算出该时区的时间
DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId())).withDayOfWeek(DateTimeConstants.MONDAY); // 指定周一为每周的第一天
startDateTimeWithZone 作为接下去计算所用到的基准时间。
- 基于这个基准时间,可以得出该时区自然周的结束时间
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);
按月切割
核心实现分析
- 首先基于时区,根据开始时间算出该时区的时间
DateTime startDateTimeWithZone = new DateTime(start).withZone(DateTimeZone.forID(zoneId.getId()));
startDateTimeWithZone 作为接下去计算所用到的基准时间。
- 基于这个基准时间,可以得出该时区自然月的结束时间
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);
}
总结
- 从代码上也可以看出,joda time的时间处理大同小异
- 按周切割的时候,需要注意每个时区可能每周的开始时间不一样,国际惯例似乎是默认周一开始