将 LocalDates 地图的内容分组为日期范围地图

Group the contents of a Map of LocalDates into a Map of Ranges of dates

我有一张按天索引的工作班次图 Map<LocalDate, Collection<Shift>> shiftsAllDays 如:

"2020-03-26": [
    {
      "id": 4,
      "startTime": "21:00:00"
    },
    {
      "id": 5,
      "startTime": "09:00:00",
    }
  ],
  "2020-03-27": [
    {
      "id": 4,
      "startTime": "22:00:00"
    },
    {
      "id": 5,
      "startTime": "10:00:00",
    }
  ],
  ...
]

可以看出,班次有一个 ID 和一个开始时间。

但是按天为地图编制索引是一种浪费:除了一件事之外,班次恰好始终相同。开始时间是 LocalTime,因此班次的唯一区别是在应用时区后,产生的班次仅在发生 DST 翻转时在小时上有所不同。

我想在收集器中做一些 groupBy 类似的事情:

Range.of(2020-01-01 ... 2020-03-26) [
    {
      "id": 4,
      "startTime": "21:00:00"
    },
    {
      "id": 5,
      "startTime": "09:00:00",
    }
],
Range.of(2020-03-27 ... 2020-10-30) [
    {
      "id": 4,
      "startTime": "22:00:00"
    },
    {
      "id": 5,
      "startTime": "10:00:00",
    }
],
Range.of(2020-10-31 ... 2021-01-01) [
    {
      "id": 4,
      "startTime": "21:00:00"
    },
    {
      "id": 5,
      "startTime": "09:00:00",
    }
]

...从 DST 翻转到 DST 翻转分组在 Ranges 中。我很难编写 Collectors.groupBy(...) 并将密钥从 LocalDate 天转换为 Range<LocalDate>

具有自定义范围的解决方案 class

首先,您需要定义一个 class Range,其中包含两个字段(开始日期结束日期),并创建一个范围列表。由于只需要三个实例,因此将此列表声明为实用程序 class.

中的 public static final 字段是有意义的

下面是Range的例子,为了简洁起见,我把它实现为Java16条记录。给出了一种实用方法,用于检查给定日期是否在 range:

的此实例内
public record Range(LocalDate start, LocalDate end) {
    public boolean isWithinRange(LocalDate date) { // start inclusive, end exclusive
        return date.isBefore(start) || date.isBefore(end) && date.isAfter(start);
    }
}

范围列表:

public static final List<Range> RANGES =
    List.of(new Range(LocalDate.of(2020,1, 1), LocalDate.of(2020,3, 27)),
            new Range(LocalDate.of(2020,3, 27), LocalDate.of(2020,10, 31)),
            new Range(LocalDate.of(2020,10, 31), LocalDate.of(2021,1, 1)));

实用程序方法 getRange() 负责根据给定日期从范围列表中检索 Range 的实例:

public static Range getRange(LocalDate data) {
    return RANGES.stream()
        .filter(range -> range.isWithinRange(data))
        .findFirst()
        .orElseThrow();
}

虚拟记录Shift用于测试目的):

public record Shift(int id, LocalTime startTime) {}

为了使用流将 Map<LocalDate, Collection<Shift>> 转换为 Map<Range, List<Shift>>,我们需要创建映射条目流。比起使用收集器 groupingBy() 来按 范围 对数据进行分组。作为 groupingBy() 的下游收集器,我们必须提供 flatMapping() 以便展平与特定数据关联的数据(以便所有 Shift 对象将映射到相同的 range 将被放置在同一个集合中)。在 flatMapping() 的下游,我们需要提供一个收集器,它定义如何存储( 或如何执行缩减 )扁平元素。数据中collector toList()用于将映射到同一个key的数据存储在一个列表中

main() - 演示

public static void main(String[] args) {
    Map<LocalDate, Collection<Shift>> shiftsByDate =
        Map.of(LocalDate.of(2020,3, 26),
                   List.of(new Shift(4, LocalTime.of(21, 0, 0)), new Shift(5, LocalTime.of(9, 0, 0))),
               LocalDate.of(2020,3, 27),
                   List.of(new Shift(4, LocalTime.of(22, 0, 0)), new Shift(5, LocalTime.of(10, 0, 0)))
            );
    
    Map<Range, List<Shift>> shiftsByRange = shiftsByDate.entrySet().stream()
        .collect(Collectors.groupingBy(entry -> getRange(entry.getKey()),
            Collectors.flatMapping(entry -> entry.getValue().stream(),
                Collectors.toList())));
    
    shiftsByRange.forEach((k, v) -> System.out.println(k + " : " + v));
}

输出

Range[start=2020-10-31, end=2021-01-01] : [Shift[id=4, startTime=22:00], Shift[id=5, startTime=10:00]]
Range[start=2020-01-01, end=2020-03-27] : [Shift[id=4, startTime=21:00], Shift[id=5, startTime=09:00]]

范围来自 Spring 数据的解决方案

以前的版本只需要适度的更改即可使用 Spring 数据项目中的 Range<T>。我们需要修复实例化这些对象的列表 RANGES,并将类型 Range 替换为 Range<T>.

注意事项: 如果您希望泛型类型参数 <T>LocalDate - 这是不正确的,也不会起作用。

Range<T> class 期望其泛型类型是可比较的,即 T extends Comparable<T>LocalDate 不直接扩展可比性,它实现了 ChronoLocalDate 接口它又实现了 Comparable 接口,而 LocalDate 继承了 compareTo().

的默认实现

换句话说:

  • 不正确 LocalDate 扩展 Comparable<LocalDate>
  • 因为实际上 LocalDate 扩展 Comparable<ChronoLocalDate>.

因此,我们不得不使用ChronoLocalDate作为泛型。

RANGES 列表如下所示:

public static final List<Range<ChronoLocalDate>> RANGES =
    List.of(Range.rightOpen(LocalDate.of(2020,1, 1), LocalDate.of(2020,3, 27)),
            Range.rightOpen(LocalDate.of(2020,3, 27), LocalDate.of(2020,10, 31)),
            Range.rightOpen(LocalDate.of(2020,10, 31), LocalDate.of(2021,1, 1)));

方法 getRange() 负责根据给定日期从范围列表中检索 Range 的实例:

public static Range<ChronoLocalDate> getRange(LocalDate data) {
    return RANGES.stream()
        .filter(range -> range.contains(data))
        .findFirst()
        .orElseThrow();
}

main() - 演示

public static void main(String[] args) {
    Map<LocalDate, Collection<Shift>> shiftsByDate =
        Map.of(LocalDate.of(2020,3, 26),
                   List.of(new Shift(4, LocalTime.of(21, 0, 0)), new Shift(5, LocalTime.of(9, 0, 0))),
               LocalDate.of(2020,3, 27),
                   List.of(new Shift(4, LocalTime.of(22, 0, 0)), new Shift(5, LocalTime.of(10, 0, 0)))
        );
    
    Map<Range<ChronoLocalDate>, List<Shift>> shiftsByRange = shiftsByDate.entrySet().stream()
        .collect(Collectors.groupingBy(entry -> getRange(entry.getKey()),
            Collectors.flatMapping(entry -> entry.getValue().stream(),
                Collectors.toList())));
    
    shiftsByRange.forEach((k, v) -> System.out.println(k + " : " + v));
}

输出:

[2020-03-27-2020-10-31) : [Shift[id=4, startTime=22:00], Shift[id=5, startTime=10:00]]
[2020-01-01-2020-03-27) : [Shift[id=4, startTime=21:00], Shift[id=5, startTime=09:00]]