Home > Mobile >  Java stream: How to map to a single item when using groupingBy instead of to list
Java stream: How to map to a single item when using groupingBy instead of to list

Time:04-22

Given an order class like:

@ToString
@AllArgsConstructor
@Getter
static class Order {
    long customerId;
    LocalDate orderDate;
}

and a list of orders:

List<Order> orderList = List.of(new Order(1, LocalDate.of(2020,Month.APRIL,21)),
                                new Order(1, LocalDate.of(2021,Month.APRIL,21)),
                                new Order(1, LocalDate.of(2022,Month.APRIL,21)),
                                new Order(2, LocalDate.of(2020,Month.APRIL,21)),
                                new Order(2, LocalDate.of(2021,Month.APRIL,21)),
                                new Order(3, LocalDate.of(2020,Month.APRIL,21)),
                                new Order(3, LocalDate.of(2022,Month.APRIL,21)),
                                new Order(4, LocalDate.of(2020,Month.APRIL,21)));

I need to get a list of customerId where last orderDate is older than 6 months. So for above example [2,4]. My idea is to first to group by customerId, second map to last orderDate and third to filter those which are older than 6 months. I am stuck at second step on how to map to a single order with the recent orderDate

First step

Map<Long, List<Order>> grouped =
        orderList.stream()
                .collect(Collectors.groupingBy(Order::getCustomerId));

Second step (stuck here how to change the above to get only one item as value)

Map<Long, Order> grouped =
        orderList.stream()
                .collect(Collectors.groupingBy(Order::getCustomerId, ???));

or even better

Map<Long, LocalDate> grouped =
        orderList.stream()
                .collect(Collectors.groupingBy(Order::getCustomerId, ???));

I have tried to use Collectors.mapping() , Collectors.reducing() and Collectors.maxBy() but having a lot of compile errors.

CodePudding user response:

Use Collectors.toMap collector to have a map of listed orders by customer id. After that, you can filter only those orders which are older than 6 months.

See the implementation below:

import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Collections;
import java.util.Objects;
import java.util.Comparator;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
public static List<Long> getCustomerIdsOfOrdersOlderThanSixMonths(final List<Order> orderList) {
    return Optional.ofNullable(orderList)
            .orElse(Collections.emptyList())
            .stream()
            .filter(o -> Objects.nonNull(o) && Objects.nonNull(o.getOrderDate()))
            .collect(Collectors.toMap(
                  Order::getCustomerId,
                  Function.identity(),
                  BinaryOperator.maxBy(Comparator.comparing(Order::getOrderDate))))
            .values()
            .stream()
            .filter(o -> o.getOrderDate()
                  .plusMonths(6)
                  .isBefore(ChronoLocalDate.from(LocalDate.now())))
            .map(Order::getCustomerId)
            .collect(Collectors.toList());
    }
}
List<Long> customerIds = getCustomerIdsOfOrdersOlderThanSixMonths(orderList);
// [2, 4]

CodePudding user response:

You can use groupingBy() with a downstream maxBy() collector, and then filter the results to just those dates that are older than six months:

import java.time.LocalDate;
import java.time.Month;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class Demo {
    private record Order(long customerId, LocalDate orderDate) {}

    public static void main(String[] args) {
        List<Order> orderList =
            List.of(new Order(1, LocalDate.of(2020,Month.APRIL,21)),
                    new Order(1, LocalDate.of(2021,Month.APRIL,21)),
                    new Order(1, LocalDate.of(2022,Month.APRIL,21)),
                    new Order(2, LocalDate.of(2020,Month.APRIL,21)),
                    new Order(2, LocalDate.of(2021,Month.APRIL,21)),
                    new Order(3, LocalDate.of(2020,Month.APRIL,21)),
                    new Order(3, LocalDate.of(2022,Month.APRIL,21)),
                    new Order(4, LocalDate.of(2020,Month.APRIL,21)));

        final LocalDate sixMonthsAgo = LocalDate.now().minusMonths(6);

        List<Order> mostRecentOrders =
            orderList.stream()
            .collect(Collectors.groupingBy(Order::customerId,
                                           Collectors.maxBy(Comparator.comparing(Order::orderDate))))
            .values().stream()
            .filter(opt -> opt.filter(o -> o.orderDate().isBefore(sixMonthsAgo)).isPresent())
            .map(Optional::orElseThrow)
            .collect(Collectors.toList());

        System.out.println(oldOrders);
    }
}

outputs

[Order[customerId=2, orderDate=2021-04-21], Order[customerId=4, orderDate=2020-04-21]]

First you get a map of customer ids and (Wrapped in an Optional due to the way Collectors.maxBy() works) the most recent order by date. Then filter out the entries where that most recent date is within the last six months. Then extract the remaining orders from the Optionals and return them in a List. If you just want the customer IDs and don't care about the rest of the Order object, modify the final map() and returned type appropriately.

CodePudding user response:

You can use Collectors.toMap with a mergeFunction for your step 2:

 Map<Long, LocalDate> latestOrderByCustomer = 
            orderList.stream()
                     .collect(Collectors.toMap(Order::customerId, 
                                                Order::orderDate, 
                                                (order1, order2) -> order1.isAfter(order2) ? order1 : order2));
  • Related