10.7 日期范围

问题

用户希望返回两个给定端点之间的日期流。

方案

使用 Java 9 为 LocalDate 类新增的 datesUntil 方法。

讨论

较之 java.util.Datejava.util.Calendar 以及 java.sql.Timestamp 类,Java 8 引入的 Date-Time API 是一种巨大改进。而 Java 9 新增的 datesUntil 方法解决了 Date-Time API 中一个令人头疼的问题:难以方便地创建日期流。

在 Java 8 中,创建日期流最简单的方式是以初始日期为基准,再添加一个偏移量。例如,为返回相隔一周的两个给定端点之间的所有天数,我们可能会写出如例 10-34 所示的代码。

例 10-34 返回两个日期之间的天数(存在问题)

  1. public List<LocalDate> getDays_java8(LocalDate start, LocalDate end) {
  2. Period period = start.until(end);
  3. return IntStream.range(0, period.getDays())
  4. .mapToObj(start:plusDays)
  5. .collect(Collectors.toList());
  6. }

➊ 实为陷阱!正确实现参见例 10-35。

程序首先计算两个日期之间的 Period,然后在二者之间创建一个 IntStream。执行程序,观察结束日期和开始日期相隔一周时的情况:

  1. LocalDate start = LocalDate.of(2017, Month.JUNE, 10);
  2. LocalDate end = LocalDate.of(2017, Month.JUNE, 17);
  3. System.out.println(dateRange.getDays_java8(start, end));
  4. // [2017-06-10, 2017-06-11, 2017-06-12, 2017-06-13,
  5. // 2017-06-14, 2017-06-15, 2017-06-16]

上述代码看似正确,实则有误。如果将结束日期改为与开始日期相隔正好一个月,很容易就能看出问题所在:

  1. LocalDate start = LocalDate.of(2017, Month.JUNE, 10);
  2. LocalDate end = LocalDate.of(2017, Month.JULY, 10);
  3. System.out.println(dateRange.getDays_java8(start, end));
  4. // []

可以看到,程序没有返回任何值。原因在于 period.getDays 方法返回的只是两个天数字段之间的天数,而非两个日期之间的总天数(getMonthsgetYears 等方法同样如此)。如上所示,由于开始日期和结束日期的天数相同,虽然月份不同,结果仍然是一个大小为 0 的范围。

为解决这个问题,应采用实现 TemporalUnit 接口的 ChronoUnit 枚举,它定义了 DAYSMONTHS 等多个枚举常量。Java 8 的正确实现如例 10-35 所示。

例 10-35 返回两个日期之间的天数(正确实现)

  1. public List<LocalDate> getDays_java8(LocalDate start, LocalDate end) {
  2. Period period = start.until(end);
  3. return LongStream.range(0, ChronoUnit.DAYS.between(start, end))
  4. .mapToObj(start:plusDays)
  5. .collect(Collectors.toList());
  6. }

➊ 正确无误

我们也可以使用 iterate 方法,但需要了解两个日期之间的天数,如例 10-36 所示。

例 10-36 LocalDate 的迭代

  1. public List<LocalDate> getDaysByIterate(LocalDate start, int days) {
  2. return Stream.iterate(start, date -> date.plusDays(1))
  3. .limit(days)
  4. .collect(Collectors.toList());
  5. }

好在 Java 9 引入的新方法使问题得以简化。LocalDate 类新增了一种名为 datesUntil 的方法,其重载形式传入 Period 作为参数。datesUntil 方法的签名如下:

  1. Stream<LocalDate> datesUntil(LocalDate endExclusive)
  2. Stream<LocalDate> datesUntil(LocalDate endExclusive, Period step)

不传入 PerioddatesUntil 方法实际上相当于将日期增量设置为一天,即 datesUntil(endExclusive) 等效于 datesUntil(endExclusive, Period.ofDays(1))

采用 datesUntil 方法返回两个日期之间的天数要简单得多,如例 10-37 所示。

例 10-37 返回两个日期之间的天数(Java 9 实现)

  1. public List<LocalDate> getDays_java9(LocalDate start, LocalDate end) {
  2. return start.datesUntil(end)
  3. .collect(Collectors.toList());
  4. }
  5. public List<LocalDate> getMonths_java9(LocalDate start, LocalDate end) {
  6. return start.datesUntil(end, Period.ofMonths(1))
  7. .collect(Collectors.toList());
  8. }

❶ 相当于 Period.ofDays(1)

❷ 日期增量为一个月

我们可以使用所有常规的流处理技术对 datesUntil 方法产生的 Stream 操作。

另见

有关在 Java 8 中计算两个日期之间的天数,请参见范例 8.8。