Skip to content
Advertisement

What is the right way to compare the List of HashMap in Java using streams

I want to compare two List of HashMaps in Java. I want to understand a way to compare them using lambda expressions and not with a nested for loop.

Two List of HashMaps :

  1. productDetailsFromInputData;

Sample Data :

    [{ProductName=Prod1, Quantity=1.0, Perpetual=true}, 
    {ProductName=Prod2, Quantity=1.0, Perpetual=false}, 
    {ProductName=Prod3, Quantity=1.0, Perpetual=false}]
  1. productDetailsFromApplication;

Sample Data :

    [{Perpetual=true, ProductName=Prod1, Quantity=1.0}, 
    {Perpetual=true,  ProductName=Prod2, Quantity=1.0}, 
    {Perpetual=false, ProductName=Prod3, Quantity=2.0}, 
    {Perpetual=false, ProductName=Prod4, Quantity=1.0}, 
    {Perpetual=false, ProductName=Prod5, Quantity=1.0}, 
    {Perpetual=false, ProductName=Prod6, Quantity=1.0}] 

Logic : I want to loop through the productDetailsFromInputData List and fetch the ProductName Value for each hashmap, check if the value exists in any of the hashmaps present in productDetailsFromApplication List. If not, mark the result as failed.

If the ProductName Exists as value in any one of the hashmap of productDetailsFromApplication List, fetch the Perpetual and Quantity values from that hashmap and compare it with hashmap in productDetailsFromInputData list.

I have tried below approach as of now :

    for(HashMap<String,Object> inputProductHashMap : productDetailsFromInputData) {
           String productName = inputProductHashMap.get("ProductName").toString();
           String inputQuantity = inputProductHashMap.get("Quantity").toString();
           String inputPerpetual = inputProductHashMap.get("Perpetual").toString();
           LOGGER.info("Validating Product details for Product "+productName);
           if(productDetailsFromApplication.stream().anyMatch(t->t.containsValue(productName))){
               productDetailsFromApplication.stream()
                               .filter(p->p.containsValue(productName))
                               .findAny().ifPresent(p->
                               {
                                   String AppQuantity = p.get("Quantity").toString();
                                   String AppPerpetual = p.get("Perpetual").toString();
                                   LOGGER.info("Found the Product details in SFDC matching with input data");
                                   if(!inputQuantity.equalsIgnoreCase(AppQuantity)){
                                       LOGGER.error("APP Quantity does not matches with Input Quantity for Product "+productName
                                               +" App Quantity "+AppQuantity+" Input Quantity "+inputQuantity);
                                       Assert.fail("Product Validation Failed for "+productName);
                                   }
                                   if(!inputPerpetual.equalsIgnoreCase(AppPerpetual)){
                                       LOGGER.error("App Perpetual value does not matches with Input Perpetual value for Product "
                                               +productName +" App Perpetual "+AppPerpetual+" Input perpetual "+inputPerpetual);
                                       Assert.fail("Product Validation Failed "+productName);
                                   }
                               });

           }
           else {
               LOGGER.error("Did not find any matching Product Name with the given details in App");
               return false;
           }
        }

I want to understand if there is a way to remove the first for loop as well. As I am fairly new to using lambda and streams, how can I optimize it.

Also I would like to know a way to return boolean value if the code reaches the LOGGER.error part where I have currently added assertions to mark the failures.

If there is an existing Stack-overflow question that covers the solution to this problem, feel free to guide me there.

Advertisement

Answer

From your statement, I understand you want to return false instead of running the Assert.fail. The way of doing this by only using Streams is:

return !productDetailsFromInputData.stream().map(i ->
        productDetailsFromApplication.stream().filter((a) -> a.get("ProductName").toString().equalsIgnoreCase(i.get("ProductName").toString())).findAny()
                .map(a ->
                        !a.get("Perpetual").toString().equalsIgnoreCase(i.get("Perpetual").toString()) || !a.get("Quantity").toString().equalsIgnoreCase(i.get("Quantity").toString()))
                .orElse(true)
).anyMatch(b -> b);

Notice I am mapping the failures to true, that’s because anyMatch would look for any true value. We then negate that with the ! in the return.

If you want to keep the LOGGER verbosity, you can probably do something like:

return !productDetailsFromInputData.stream().map(i ->
        productDetailsFromApplication.stream().filter((a) -> a.get("ProductName").toString().equalsIgnoreCase(i.get("ProductName").toString())).findAny()
                .map(a -> {
                    if(!a.get("Quantity").toString().equalsIgnoreCase(i.get("Quantity").toString())) {
                        LOGGER.error("APP Quantity does not matches with Input Quantity for Product "+i.get("ProductName").toString()
                                +" App Quantity "+a.get("Quantity").toString()+" Input Quantity "+i.get("Quantity").toString());
                        return true;
                    }
                    if(!a.get("Perpetual").toString().equalsIgnoreCase(i.get("Perpetual").toString())) {
                        LOGGER.error("App Perpetual value does not matches with Input Perpetual value for Product "
                                +i.get("ProductName").toString() +" App Perpetual "+a.get("Perpetual").toString()+" Input perpetual "+i.get("Perpetual").toString());
                        return true;
                    }
                    return false;
                })
                .orElseGet(() -> {
                    LOGGER.error("Did not find any matching Product Name with the given details in App");
                    return true;
                })
).anyMatch(b -> b);

However, I would suggest not to use only Streams and first converting productDetailsFromApplication to a Map so it is faster to find the match:

Map<String, Map<String, Object>> mapFromApplication = productDetailsFromApplication.stream().collect(Collectors.toMap(e -> e.get("ProductName").toString(), Function.identity()));
return !productDetailsFromInputData.stream().map(i ->
        Optional.ofNullable(mapFromApplication.get(i.get("ProductName").toString()))
                .map(a -> {
                    if(!a.get("Quantity").toString().equalsIgnoreCase(i.get("Quantity").toString())) {
                        LOGGER.error("APP Quantity does not matches with Input Quantity for Product "+i.get("ProductName").toString()
                                +" App Quantity "+a.get("Quantity").toString()+" Input Quantity "+i.get("Quantity").toString());
                        return true;
                    }
                    if(!a.get("Perpetual").toString().equalsIgnoreCase(i.get("Perpetual").toString())) {
                        LOGGER.error("App Perpetual value does not matches with Input Perpetual value for Product "
                                +i.get("ProductName").toString() +" App Perpetual "+a.get("Perpetual").toString()+" Input perpetual "+i.get("Perpetual").toString());
                        return true;
                    }
                    return false;
                })
                .orElseGet(() -> {
                    LOGGER.error("Did not find any matching Product Name with the given details in App");
                    return true;
                })
).anyMatch(b -> b);

This would stop on the first failure and not count all of them. If you could benefit from counting the failures and/or logging all of them, you could use .filter(b -> b).count() instead of .anyMatch(b -> b).

long count = productDetailsFromInputData.stream().map(i ->
...
.filter(b -> b).count();
if(count>0) {
    LOGGER.error(count+" failures.");
    return false;
} else {
    return true;
}

EDIT: You can actually move the anyMatch statement further up replacing the first map, but having it at the bottom let’s you change it easily to other uses such as the count if needed.

User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement