使用 Java 流和 groupingBy() 从每天的计数中获取每月的计数

Obtain the Count per Month from the Count per Day using Java Streams and groupingBy()

我想将 每天 销售额(随便,无所谓)的列表转换为 每月 的列表] 销售额。

所以我有一个 List<Map<String, Object>> perDay,其中包含这些每日值:

[{DATE=2022-01-01, COUNT=5}, {DATE=2022-01-02, COUNT=3}, {DATE=2022-02-29, COUNT=4}]

这些值应该收集到月值中,像这样:

[{DATE=2022-01, COUNT=8}, {DATE=2022-02, COUNT=4}]

我想对流使用 groupingBy() 方法。

这是我的代码:

    List<Map<String, Object>> perDay = new ArrayList<>();
    Map<String, Object> jan1 = new HashMap<>();
    jan1.put("DATE", "2022-01-01"); jan1.put("COUNT", "5");
    Map<String, Object> jan2 = new HashMap<>();
    jan2.put("DATE", "2022-01-02"); jan2.put("COUNT", "3");
    Map<String, Object> feb1 = new HashMap<>();
    feb1.put("DATE", "2022-02-29"); feb1.put("COUNT", "4");

Map<String, Object> perMonth = perDay.stream()
        .collect(Collectors.groupingBy("???", 
                    Collectors.summingInt(f -> Integer.parseInt((String) f))));

我想,我已经接近了,但我还没有完全做到。

有什么想法吗?

假设您的日期始终采用格式

2022-01-01

你可以试试:

 var map = perDay.stream().collect(
                  Collectors.groupingBy(monthMap -> ((String) monthMap.get("DATE")).substring(0, 7),
                  Collectors.summingInt(monthMap -> Integer.parseInt(String.valueOf(monthMap.get("COUNT"))))));

Returns:

{2022-01=8, 2022-02=4}

您介绍的方法不可维护,因此容易出现错误和代码重复,也使代码的扩展更加困难。

这些是主要问题:

  • 不要将 Object 用作通用类型。因为 Object 类型的集合元素如果不进行转换就一文不值。如果您无意编写充满强制转换和 intanceof 检查的泥泞代码,请不要走这条路。

  • Map - 不是 与 JSON-object 相同的东西(我不是在谈论 类 来自 Java-libraries 就像 Gson,它们是 map-based),不要混淆 构建 human-readable 文本的方式 数据结构.

    的实现
  • "DATE""COUNT" 这样的键是没用的——你可以简单地存储实际的 datecount 相反。

  • 使用 String 作为数字和日期的类型不会给您带来任何优势。在不解析的情况下,您无法对它们进行任何操作(除了在控制台上进行比较和打印),几乎与 Object 类似。为每个元素使用适当的数据类型。为什么?因为 LocalDateIntegerYearMonth 具有 String 无法提供给您的独特行为。

这就是您可以通过将表示 每天销售额 List<Map<String, Object>> 的集合调整为更方便的 Map<LocalDate, Integer> 来改进代码的方法:

Map<LocalDate, Integer> salesPerDay = perDay.stream()
    .map(map -> Map.entry(LocalDate.parse((String) map.get("DATE"), DateTimeFormatter.ISO_DATE),
                          Integer.parseInt((String) map.get("COUNT"))))
    .collect(Collectors.groupingBy(
                Map.Entry::getKey,
                Collectors.summingInt(Map.Entry::getValue)));

这就是您可以从中生成包含 每月销售额 的地图的方法(java.time 包中的 YearMonth 用作 ):

Map<YearMonth, Integer> salesPerMonth = salesPerDay.entrySet().stream()
    .collect(Collectors.groupingBy(
                entry -> YearMonth.from(entry.getKey()),
                Collectors.summingInt(Map.Entry::getValue)));

salesPerMonth.forEach((k, v) -> System.out.println(k + " -> " + v));

输出:

2022-01 -> 8
2022-02 -> 4

A link to Online Demo

首先,我建议不要将地图列表用于日期销售或月份销售。

这是您的数据。

Map<String, Object> jan1 = new HashMap<>();
jan1.put("DATE", "2022-01-01");
jan1.put("COUNT", "5");
Map<String, Object> jan2 = new HashMap<>();
jan2.put("DATE", "2022-01-02");
jan2.put("COUNT", "3");
Map<String, Object> feb1 = new HashMap<>();
feb1.put("DATE", "2022-02-29");
feb1.put("COUNT", "4");
List<Map<String, Object>> perDay = List.of(jan1, jan2, feb1);

这是您的操作方式。

  • 流式传输地图
  • 获取日期并仅使用 year/month 部分
  • 将计数转换为整数(因此可以求和)
  • 当遇到重复键时,添加计数
  • 请注意,这涉及值的转换。
Map<String, Integer> map = perDay.stream()
        .collect(Collectors.toMap(
                mp -> ((String) mp.get("DATE")).substring(0,
                        7),
                mp -> Integer
                        .valueOf((String) mp.get("COUNT")),
                Integer::sum));

但是您的初始 List<Map<String,Object>> 不允许您选择日期,因为地图的 DATE 值与列表的索引没有任何关系。但是,如果将它们存储为 Map<String,Integer>,则可以使用 yyyy-MM-dd 的一致格式检索任何 DATE 的计数。而且这个过程不需要转换就可以工作。

Map<String, Integer> perDay = new HashMap<>();
perDay.put("2022-01-01", 5);
perDay.put("2022-01-02", 3);
perDay.put("2022-02-29", 4);

这样也可以更轻松地转换为当月总销售额,如下所示:

  • 流式传输 entrySets perDay
  • 转换为 Map<String,Integer> 表示月份和销售额。使用 substring 通过 Entry.getKey() 获取年月部分。注意:这仅在您的日期格式一致时才有效。
  • 使用方法引用从 Entry::getValue 中获取计数。
  • 对于重复计数(同一个月,不同日期)使用 Integer::sum
  • 求和

Map<String, Integer> monthSales = daySales.entrySet()
        .stream().collect(Collectors.toMap(e->e.getKey().substring(0,7),
                             Entry::getValue,
                             Integer::sum));

monthSales.entrySet().forEach(System.out::println);

打印

2022-01=8
2022-02=4

和以前一样,您可以获得任何 yyyy-MM 密钥(如果存在)的每月计数。

建议:

  • 首先,不要使用Object。使用特定类型,在您的情况下为 Integer 作为值。否则你将不得不进行转换,这可能会导致错误。

  • 考虑使用class来存储相关信息。销售额 class 可用于存储日期和计数以及其他信息。

     class Sales {
         private int count; 
         private String date;
         // getters
         // toString
    }
    
  • 要处理日期,请查看 java.time 包中的 classes。