4

I have the list that contains data that looks like this

game_id | user_id | status
   1         1       STARTED
   2         1       FINISHED
   1         2       STARTED
   2         2       FINISHED

I want to collect this list into two maps (or in a single map if possible)

One map should keep mapping of user_id to number of the games that user played - basically games with finished statuses

user_id | finished_games
   1            1
   2            1

Another map should store user_id to all games count. It would look like this

user_id  | all_games
   1            2
   2            2

I do it like this (I removed game_id field as it is that not important in fact)

import java.util.*;
import java.util.stream.*;

public class MyClass {
    public static void main(String args[]) {
        List<Game> games = Arrays.asList(
            create("user-1", "STARTED"),
            create("user-2", "STARTED"),
            create("user-1", "FINISHED"),
            create("user-2", "FINISHED"),
            create("user-1", "FINISHED"),
            create("user-2", "FINISHED")
        );

        // expect user-1: all_games (3), finished_games (2)

        Map<String, Long> allGames = games
            .stream()
            .collect(Collectors.groupingBy(Game::getUserId, Collectors.counting()));
        System.out.println(allGames);

        Map<String, Long> finishedGames = games
            .stream()
            .filter(game -> game.battleStatus.equals("FINISHED"))
            .collect(Collectors.groupingBy(Game::getUserId, Collectors.counting()));
        System.out.println(finishedGames);

    }

    private static Game create(String id, String status) {
        Game game = new Game();
        game.userId = id;
        game.battleStatus = status;
        return game;
    }

    private static class Game {
        String userId;
        String battleStatus;

        public String getUserId() {
            return userId;
        }
    }
}

It seems to work fine. But when I calculate games with status, I filter in order to leave only items with FINISHED status.

Map<String, Long> finishedGames = games
    .stream()
    .filter(game -> game.battleStatus.equals("FINISHED"))
    .collect(Collectors.groupingBy(Game::getUserId, Collectors.counting()));

Is there a way to achieve that inside collect block instead of using filter?

ETO
  • 6,970
  • 1
  • 20
  • 37
lapots
  • 12,553
  • 32
  • 121
  • 242
  • 3
    Is there a reason why you're trying to avoid `filter`? – Timothy T. Jan 29 '19 at 09:30
  • 2
    this is really un-clear, didn't you say yourself that you what to count both `FINISHED` and all games? – Eugene Jan 29 '19 at 09:31
  • 2
    You could use [`Collectors.filtering()`](https://docs.oracle.com/javase/9/docs/api/java/util/stream/Collectors.html#filtering-java.util.function.Predicate-java.util.stream.Collector-) in Java 9, but why you'd want to is beyond me. – shmosel Jan 29 '19 at 09:32
  • @Eugene Yes. I need to count them both. But I store it in a separate maps. – lapots Jan 29 '19 at 09:32
  • @TimothyT. well `filter` is the first idea I came up with, but I thought there is some other way – lapots Jan 29 '19 at 09:33
  • 3
    so you want to achieve that using a single `stream` operation? your code looks perfectly fine btw – Eugene Jan 29 '19 at 09:33
  • Could you update your question and add your rationale about why you want to avoid filtering so we would know that this is not some sort of [XY problem](https://meta.stackexchange.com/q/66377)? – Pshemo Jan 29 '19 at 09:41
  • @Pshemo there is no solid reason not to use filtering. I just want to do the same thing another way. – lapots Jan 29 '19 at 10:57
  • This is not https://codegolf.stackexchange.com/ Don’t waste our time with artificial problems. – Holger Jan 29 '19 at 16:46

2 Answers2

3

You can use multiple groupingBy to get this, e.g.:

Map<String, Map<String, Long>> gameStatuses = games
   .stream()
   .collect(Collectors.groupingBy(Game::getUserId, 
   Collectors.groupingBy(Game::getBattleStatus, Collectors.counting())));
System.out.println(gameStatuses);

For this, you need to add a getter to Game class for battleStatus, e.g.:

private static class Game {
    String userId;
    String battleStatus;

    public String getUserId() {
        return userId;
    }

    public String getBattleStatus() {
        return battleStatus;
    }
}

This would give you the following output:

{user-1={STARTED=1, FINISHED=2}, user-2={STARTED=1, FINISHED=2}}

You can use nested map.get to get the counts for any user and any status.

Darshan Mehta
  • 30,102
  • 11
  • 68
  • 102
1

If you need to count both finished and unfinished games, then another groupingBy can help you:

Function<Game, Boolean> isFinished = game -> "FINISHED".equals(game.battleStatus);

Map<String, Map<Boolean, Long>> groupedGames = 
    games.stream()
         .collect(groupingBy(Game::getUserId, 
                      groupingBy(isFinished, counting())));

Or you may want to group by battleStatus itself:

Map<String, Map<String, Long>> groupedGames = 
    games.stream()
         .collect(groupingBy(Game::getUserId, 
                      groupingBy(game -> game.battleStatus, counting())));

I would even create a getter getBattleStatus() and then replace game -> game.battleStatus with method reference Game::getBattleStatus:

Map<String, Map<String, Long>> groupedGames = 
    games.stream()
         .collect(groupingBy(Game::getUserId, 
                      groupingBy(Game::getBattleStatus, counting())));
ETO
  • 6,970
  • 1
  • 20
  • 37
  • 1
    this isn't that bad IMO with the caveat that the OP said *I want to collect this list into two maps*; if we don't take that literally, I guess this looks nice. 1+ – Eugene Jan 29 '19 at 09:43
  • @Eugene oh! I didn't know it was possible to group into the same map as I didn't think about class "true/false" keys – lapots Jan 29 '19 at 10:58