mattsmith5
mattsmith5

Reputation: 1113

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);
}

Upvotes: 1

Views: 492

Answers (7)

Eritrean
Eritrean

Reputation: 16498

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());

To get Partition by First item in sort, just change to min

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

Upvotes: 4

RajTechnorama
RajTechnorama

Reputation: 1

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]

Upvotes: 0

shmosel
shmosel

Reputation: 50716

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();

Upvotes: 1

Oleg Cherednik
Oleg Cherednik

Reputation: 18245

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.

E.g. 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());
}

Upvotes: 1

knittl
knittl

Reputation: 265241

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).

Upvotes: 6

tnusraddinov
tnusraddinov

Reputation: 750

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]

Upvotes: 1

Risalat Zaman
Risalat Zaman

Reputation: 1337

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);

Upvotes: 1

Related Questions