3

I try to use java 8 features.

I have a class

@Data
@NoArgsConstructor
@AllArgsConstructor
class User {
    private String pId;
    private String uId;
    private String price;
}

I have a list, I try to group by pId and count the non null uId and price. Example:

List<User> list =
Arrays.asList(
    new User ("p1", "u1", null),
    new User ("p1", "u2", "a"),
    new User ("p2", null, "b"),
    new User ("p2", null, "c"),
    new User ("p3", "u4", "d")
);

My expected output is

[
    { pId:"p1", uCount:2, priceCount:1 },
    { pId:"p2", uCount:0, priceCount:2 },
    { pId:"p3", uCount:1, priceCount:1 }
]

I tried like following

Map<String, Map<Object, Long>> collect =
    list.stream()
        .collect(
            Collectors.groupingBy(
                User ::getPId,
                Collectors.groupingBy(f -> f.getUId(), Collectors.counting())));

My final mapping class is

@Data
@NoArgsConstructor
@AllArgsConstructor
class Stat {
    private String pId;
    private Integer uCount;
    private Integer priceCount;
}

Since I'm new to java, I'm facing struggle to complete it, I tried my best. Is that possible to remove null filed and count?

Gautham M
  • 4,816
  • 3
  • 15
  • 37

4 Answers4

2

Java 12+ solution

By using teeing collectors and filtering you can do like this:

Map<String, Detail> result = list.stream()
       .collect(Collectors.groupingBy(User::getpId, Collectors.teeing(
           Collectors.filtering(u -> u.getuId() != null, Collectors.counting()),
           Collectors.filtering(u -> u.getPrice() != null, Collectors.counting()),
           (uCount, priceCount) -> new Detail(uCount, priceCount)
        )));

and

class Detail{
  private long uCount;
  private long priceCount;
}

output:

{ p1=Detail{uCount=2, priceCount=1}, p2=Detail{uCount=0, priceCount=2}, p3=Detail{uCount=1, priceCount=1} }

Hadi J
  • 16,989
  • 4
  • 36
  • 62
  • 1
    why not phrase it to use `Stat` class in the question instead of `Detail`? – Naman May 31 '21 at 12:26
  • @Naman, `Stat` object included `pId` because of that it needs to more process to create it. I think by using `Detail` it is straightforward. – Hadi J May 31 '21 at 12:35
0

Can you try below solution? It will provide correct output:

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        List<User> list =
                Arrays.asList(
                    new User ("p1", "u1", null),
                    new User ("p1", "u2", "a"),
                    new User ("p2", null, "b"),
                    new User ("p2", null, "c"),
                    new User ("p3", "u4", "d")
                );

        Set<Stat> set = list.stream().map(s -> new Stat(s.getPId(), 0, 0)).collect(Collectors.toSet());
        set = set.stream().map(s -> updateCounts(list, s.getPId())).collect(Collectors.toSet());

        System.out.println(set);
    }

    public static Stat updateCounts(List<User> list, String pId) {
        Long uCount = list.stream().filter(s -> pId.equals(s.getPId()) && Objects.nonNull(s.getUId()))
            .collect(Collectors.counting());

        Long priceCount = list.stream().filter(s -> pId.equals(s.getPId()) && Objects.nonNull(s.getPrice()))
                .collect(Collectors.counting());
        return new Stat(pId, Integer.valueOf(uCount+""), Integer.valueOf(priceCount+""));
    }
}
pcsutar
  • 1,715
  • 2
  • 9
  • 14
0

I would suggest the below solution. It uses reduction operation for each group and requires some additions to the Stat class so that the code look somewhat neat.

@Data
@NoArgsConstructor
@AllArgsConstructor
class Stat {
  private String pId;
  private Integer uCount;
  private Integer priceCount;

  public static Stat init() {
    return new Stat(null, Integer.valueOf(0), Integer.valueOf(0));
  }

  public void incValue(Stat s) {
    this.uCount += (s.uCount !=null) ? s.uCount : 0;
    this.priceCount+= (s.priceCount!=null) ? s.priceCount: 0;
  }

  public static Stat mapToStat(User user) {
    Stat s = new Stat(user.getPId(), 0, 0);
    if(user.getUid() != null) {
      s.uCount++;
    }
    if(user.getPrice() != null) {
      s.priceCount++;
    }
  }
}

Now the stream operation:

return list.stream() // List<Stat> returned
           .collect(Collectors.groupingBy(User::getPId, Collectors.reducing(new Stat(), Stat::mapToStat, (s1, s2) -> {
                // You could move the below part as well to a method in Stat
                Stat s = Stat.init();
                s.setPId(s1.getPId());
                s.incValue(s1);
                s.incValue(s2);
                if (s1.getPId() == null) {
                    s.setPId(s2.getPId());
                }
                return s;
            })))
            .values()
            .stream()
            .collect(Collectors.toList());

I will leave the optimization/cleanup part to you :-)

Also, check out this question/answer. It is also about a very similar but slightly different scenario. https://stackoverflow.com/a/67240321/7804477

Gautham M
  • 4,816
  • 3
  • 15
  • 37
  • 1
    [`toMap` instead of `grouping`+`reducing`](https://stackoverflow.com/questions/57041896/java-streams-replacing-groupingby-and-reducing-by-tomap) could be much cleaner for such a job. – Naman May 31 '21 at 12:19
  • @Naman yes. the `reducing` part in the above involves unnecessary `Stat` object creation – Gautham M May 31 '21 at 12:51
0

You can use reducing operation:

import static java.util.stream.Collectors.*;

Function<User, Stat> getStatFromUser = 
    u -> new Stat(u.getpId(), 
                  u.getuId() == null ? 0 : 1,
                  u.getPrice() == null ? 0 : 1);
                        
BinaryOperator<Stat> addStats = 
    (o1, o2) -> new Stat(// Imp to getpId from o2 as for the first element pId of o1 (identity element) would be ""
                          o2.getpId(), 
                          o1.getuCount() + o2.getuCount(),
                          o1.getPriceCount() + o2.getPriceCount());

Map<String, Stat> result = 
    list.stream().collect(
                    groupingBy(User::getpId,
                               reducing(
                                  new Stat(""),
                                  u -> statFromUser.apply(u),
                                  (o1, o2) -> addStats.apply(o1, o2))));

For the test case in question, the output should look like:

{
 p1= {pId=p1, uCount=2, priceCount=1}, 
 p2= {pId=p2, uCount=0, priceCount=2}, 
 p3= {pId=p3, uCount=1, priceCount=1}
}

To get your expected output, you can invoke collectingAndThen on groupingBy:

Object result = 
    list.stream()
          .collect(
             collectingAndThen(
                 groupingBy(User::getpId,
                            reducing(new Output(""),
                                     u -> outputFromUser.apply(u),
                                     (o1, o2) -> addOuputs.apply(o1, o2))),
                 m -> m.values()));

The output of above construct is:

[
   Output {pId=p1, uCount=2, priceCount=1}, 
   Output {pId=p2, uCount=0, priceCount=2}, 
   Output {pId=p3, uCount=1, priceCount=1}
]

Edit 1:

Another solution is to use toMap:

Object result = 
     list.stream()
           .collect(
               collectingAndThen(
                    toMap(User::getpId, 
                          u -> outputFromUser.apply(u), 
                          (o1, o2) -> addOuputs.apply(o1, o2)),
                    m -> m.values()));
adarsh
  • 1,393
  • 1
  • 8
  • 16