简体   繁体   中英

Java List, Partition by and Get Last Item in Sort

I have a List of ProductTransactions. I want to find the Final (largest) productTransactionId sale for each product in List<ProductTransaction> . So I partition this by ProductId, and order by ProductTransactionId. Final List in example below List<Integer> (2, 5, 9) How can this be done? I am trying to use stream and filter.

@Data
public class ProductTransaction {
    private int productTransactionId;
    private int productId;
    private Date saleDate;
    private BigDecimal amount;
}
ProductTransactionId ProductId SaleDate Amount
1 1 3/2/2019 5
2 1 4/1/2019 9
3 2 4/1/2019 2
4 2 8/21/2019 3
5 2 8/21/2019 4
6 3 10/1/2019 2
7 3 10/3/2019 5
8 3 10/3/2019 7
9 3 10/3/2019 8

(please ignore the SaleDate, only sort by ProductTransactionId; The table input data, may not be necessarily sorted

currently using Java 8

Attempt:

current Long Solution (want to make cleaner short hand, or perhaps faster performance)

Set<Long> finalProductTransactionIds = new HashSet<>();
    
Set<Long> distinctProductIds =  productTransactions.stream()
        .map(ProductTransaction::getProductid)
        .collect(Collectors.toSet());

for (Long productId: distinctProductIds) {
    Long productTransactionId = productTransactions.stream()
            .filter(x -> x.getProductId() == productId])
            .sorted(Comparator.comparing(ProductTransaction::getProductTransactionId)
            .reversed())
            .collect(Collectors.toList()).get(0).getProductTransactionId();
    finalProductTransactionIds.add(productTransactionId);
}

If you don't mind unwrapping Optionals, you can group by your product id and then use a mapping + maxBy downstream collector. This avoids having to collect to a temporary list, as only the last item will be kept (but adds minimal overhead for the optional instances).

final Map<Integer, Optional<Integer>> map = transactions.stream()
        .collect(
                Collectors.groupingBy(
                        ProductTransaction::getProductId,
                        Collectors.mapping(
                                ProductTransaction::getProductTransactionId,
                                Collectors.maxBy(Comparator.naturalOrder()))));

final Collection<Optional<Integer>> optionalMax = map.values();
final List<Optional<Integer>> max = optionalMax.stream()
        .filter(Optional::isPresent)
        .collect(Collectors.toList());

It is also possible to use the special overload of the toMap collector to avoid the Optional type:

final Collection<Integer> maxTransactionIds = transactions.stream()
        .collect(
                Collectors.toMap(
                        ProductTransaction::getProductId,
                        ProductTransaction::getProductTransactionId,
                        BinaryOperator.maxBy(Comparator.naturalOrder())))
        .values();

Thanks to Eritrean for pointing out that getProductId returns an int, so we can replace the generally applicable BinaryOperator.maxBy(Comparator.naturalOrder) with the shorter Math::max ( Math#max(int,int) ) method reference, which will return the larger value of two integers:

final Collection<Integer> maxTransactionIds = transactions.stream()
        .collect(
                Collectors.toMap(
                        ProductTransaction::getProductId,
                        ProductTransaction::getProductTransactionId,
                        Math::max))
        .values();

And maybe you don't like the Stream API. You can use a regular loop and the Map#merge function to achieve the same end result. If you squint, the merge call even looks like the toMap collector (why that is, is left as an exercise to the reader:)).

final Map<Integer, Integer> maxTxPerProduct = new HashMap<>();
for (final ProductTransaction transaction : transactions) {
    maxTxPerProduct.merge(
            transaction.getProductId(),
            transaction.getProductTransactionId(),
            Math::max);
}
final Collection<Integer> max = maxTxPerProduct.values();

It definitely avoids creating stream and collector objects (which is rarely a problem anyway).

Stream over your list and collect to map using productId as key and productTransactionId as value. If one or more objects share the same productId , take the one with the highest productTransactionId using Math::max and get the values of the map:

List<Integer> result =  new ArrayList<>(
        productTransactions.stream()
                           .collect(Collectors.toMap(ProductTransaction::getProductId, 
                                                     ProductTransaction::getProductTransactionId,
                                                     Math::max))
                           .values());

You can achieve it with a little bit of collectors and grouping by. You can follow this helpful article for reference

    Map<Integer, List<Integer>> productTransactionIdsByProductId = transactionList.stream()
            .collect(Collectors.groupingBy(
                    ProductTransaction::getProductId,
                    Collectors.mapping(ProductTransaction::getProductTransactionId, Collectors.toList())));

    final List<Integer> latestTransactionIds = new ArrayList<>();

    productTransactionIdsByProductId.forEach( (k,v)-> {
        if(!v.isEmpty())
            latestTransactionIds.add(v.get(v.size()-1));
    });
    System.out.println(latestTransactionIds);

Using stream

record A(int tId, int pId, double amount) {

}

List<A> list = List.of(
        new A(6, 3, 2),
        new A(7, 3, 5),

        new A(3, 2, 2),
        new A(4, 2, 3),
        new A(5, 2, 4),

        new A(1, 1, 5),
        new A(2, 1, 9),

        new A(8, 3, 7),
        new A(9, 3, 8)
);

Map<Integer, List<A>> grouped = list.stream()
        .collect(Collectors.groupingBy(A::pId));

grouped.forEach((integer, as) -> as.sort(Comparator.comparing(A::tId).reversed()));
List<Integer> integers = grouped.values().stream()
        .map(as -> as.stream().map(A::tId).findFirst().orElse(0))
        .collect(Collectors.toList());

System.out.println(grouped);
System.out.println(integers);

[2, 5, 9]

BE SIMPLE !!!

Remember, that support of the code is much more complicated that implemnting. It is better to write smth.with a bit more lines, but much more clear.

Eg Streams are quitre efficient, but sometime much more complicated to realise how it does it's work. In case you can write smth without it, do think about. Probably it can be more clear than streams.

public static List<Integer> getLargest(List<ProductTransaction> transactions) {
    Map<Integer, Integer> map = new HashMap<>();

    for (ProductTransaction transaction : transactions) {
        int productId = transaction.getProductId();
        map.put(productId, Math.max(map.getOrDefault(productId, 0),
                                    transaction.getProductTransactionId()));
    }

    return new ArrayList<>(new TreeMap<>(map).values());
}

If you're open to third party libraries, StreamEx offers some nice helpers for more advanced transformations:

List<Integer> result = StreamEx.of(productTransactions)
        .mapToEntry(
                ProductTransaction::getProductId,
                ProductTransaction::getProductTransactionId)
        .collapseKeys(Math::max)
        .values()
        .toList();

Stream into a map accumulating on the desired key (in your case, productId) but resolving by max amount on map merge when you run into multiple values for the same key - BinaryOperator.maxBy below.

List<ProductTransaction> list = List.of(
new ProductTransaction(1,   1,  "3/2/2019", 5),
new ProductTransaction(2,   1,  "4/1/2019", 9),
new ProductTransaction(3,   2,  "4/1/2019", 2),
new ProductTransaction(4,   2,  "8/21/2019",    3),
new ProductTransaction(5,   2,  "8/21/2019",    4),
new ProductTransaction(6,   3,  "10/1/2019",    2),
new ProductTransaction(7,   3,  "10/3/2019",    5),
new ProductTransaction(8,   3,  "10/3/2019",    7),
new ProductTransaction(9,   3,  "10/3/2019",    8));

Map<Integer, ProductTransaction> result = list.stream()
        .collect(Collectors.toMap(tx -> tx.productId, Function.identity(),
            BinaryOperator.maxBy(Comparator.comparingDouble(tx -> tx.amount.doubleValue()))));


System.out.println(result.values().stream().map(tx -> tx.productTransactionId).collect(Collectors.toList()));

prints: [2, 5, 9]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM