Java8嵌套分组与时间间隔

时间:2017-05-09 16:40:59

标签: java-8 java-stream

我按以下格式列出了该列表:

List<MyObject> myObj = Arrays.asList(
  new MyObject(time="2017-05-09T15:37:51.896+00:00", name="123", status=200)
  new MyObject(time="2017-05-09T15:37:57.090+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:37:59.733+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:39:57.883+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:40:00.862+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:40:04.659+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:40:05.114+00:00", name="ABC", status=500)
  new MyObject(time="2017-05-09T15:45:58.796+00:00", name="XYZ", status=200)
  new MyObject(time="2017-05-09T15:46:00.562+00:00", name="XYZ", status=200)
  new MyObject(time="2017-05-09T15:48:04.144+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:48:04.364+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:48:04.750+00:00", name="ABC", status=200)
  new MyObject(time="2017-05-09T15:48:07.052+00:00", name="XYZ", status=202)
);

我想迭代它并以1米间隔执行一些分组,并实现如下:

ABC 
  -> 15:37
    -> 200 -> 2
    -> 202 -> 0
    -> 500 -> 0
  -> 15:38
    -> 200 -> 0
    -> 202 -> 0
    -> 500 -> 0
  -> 15:39
    -> 200 -> 1
    -> 202 -> 0
    -> 500 -> 0
  -> 15:40
    -> 200 -> 2
    -> 202 -> 0
    -> 500 -> 1

到目前为止,我所尝试的是:

myObj.stream()
  .collect(Collectors.groupingBy(MyObject::getName, 
            Collectors.groupingBy(MyObject::getTime)));

但这实际上是按名称分组,然后按时间分组。但是我希望有1m的时间间隔,然后按状态分组。

在这里需要帮助,因为我是java中的溪流和lambdas的新手。

编辑:请注意,getTime会返回String,而不是Date

2 个答案:

答案 0 :(得分:3)

这可以通过以下方式实现:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
Map<String, Map<String, Map<Integer, Integer>>> collect = myObj.stream()
    .collect(Collectors.groupingBy(MyObject::getName, TreeMap::new,
        Collectors.groupingBy(
            myObject -> ZonedDateTime.parse(myObject.getTime()).format(formatter), 
            TreeMap::new, 
            Collectors.groupingBy(MyObject::getStatus, TreeMap::new, 
                Collectors.summingInt(i -> 1)
            )
         )
     ));

结果:

{
  123={
    15:37={200=1}
  }, 
  ABC={
    15:37={200=2}, 15:39={200=1}, 15:40={200=2, 500=1}, 15:48={200=3}
  }, 
  XYZ={
    15:45={200=1}, 15:46={200=1}, 15:48={202=1}
  }
}

答案 1 :(得分:0)

据我所知,您需要“空”条目,包括没有相关名称对象的分钟,以及在该分钟内未出现的状态。 Streams非常适合处理流中的元素,但不擅长发明那些尚未存在的元素。

我的建议是,您使用流构建元素来构建地图结构,然后填写缺少的空条目。第一部分非常像Flown’s answer

首先,我建议除了标准的外,我还能为你的MyObject课程增加一个额外的吸气剂:

public LocalTime getWholeMinute() {
    return ZonedDateTime.parse(time)
            .withZoneSameInstant(ZoneOffset.UTC)
            .toLocalTime()
            .truncatedTo(ChronoUnit.MINUTES);
}

这将返回整个时间段。如果您需要区分几天的时间(并且您可以随后将其格式化以仅打印时间),则该方法可以返回LocalDateTime。如果您无法修改MyObject,则可以改为声明采用MyObject的静态方法,提取时间字符串,执行上述计算并返回LocalTimeLocalDateTime。接下来,任何一种建议的方法都会派上用场:

    final int[] statuses = { 200, 202, 500 };

    if (! myObj.isEmpty()) {
        LocalTime minTime = myObj.stream()
                .map(MyObject::getWholeMinute)
                .min(Comparator.naturalOrder())
                .get();
        LocalTime maxTime = myObj.stream()
                .map(MyObject::getWholeMinute)
                .max(Comparator.naturalOrder())
                .get();
        Map<String, Map<LocalTime, Map<Integer, Long>>> counts = myObj.stream()
                .collect(Collectors.groupingBy(MyObject::getName, 
                        Collectors.groupingBy(MyObject::getWholeMinute, 
                                Collectors.groupingBy(MyObject::getStatus, 
                                        Collectors.counting()))));
        for (Map.Entry<String, Map<LocalTime, Map<Integer, Long>>> outerEntry : 
                counts.entrySet()) {
            Map<LocalTime, Map<Integer, Long>> middleMap = outerEntry.getValue();
            LocalTime currentMinute = minTime;
            while (! currentMinute.isAfter(maxTime)) {
                // fill in missing map
                Map<Integer, Long> innerMap 
                        = middleMap.computeIfAbsent(currentMinute, t -> new HashMap<>(4));
                // fill in missing counts
                for (int status : statuses) {
                    innerMap.putIfAbsent(status, 0L);
                }
                currentMinute = currentMinute.plusMinutes(1);
            }
        }

        // ...
    }

我想到的另一种方法是首先创建填充零计数的地图结构,然后迭代列表并增加遇到的每个对象的相关计数。我认为它只需要更多的代码行,并且可能更清晰,因为它不会使用许多嵌套的收集器。