Skip to content
Advertisement

Java streams average

I need to create two methods using streams. A method that returns an average score of each task.

    public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {}

and a method that returns a task with the highest average score.

    public String easiestTask(Stream<CourseResult> results) {}

I can only modify those 2 methods.

Here is CourseResult class

public class CourseResult {
    private final Person person;
    private final Map<String, Integer> taskResults;

    public CourseResult(final Person person, final Map<String, Integer> taskResults) {
        this.person = person;
        this.taskResults = taskResults;
    }

    public Person getPerson() {
        return person;
    }

    public Map<String, Integer> getTaskResults() {
        return taskResults;
    }
}

And methods that create CourseResult objects.

private final String[] programTasks = {"Lab 1. Figures", "Lab 2. War and Peace", "Lab 3. File Tree"};
private final String[] practicalHistoryTasks = {"Shieldwalling", "Phalanxing", "Wedging", "Tercioing"};

private Stream<CourseResult> programmingResults(final Random random) {
    int n = random.nextInt(names.length);
    int l = random.nextInt(lastNames.length);

    return IntStream.iterate(0, i -> i + 1)
            .limit(3)
            .mapToObj(i -> new Person(
                    names[(n + i) % names.length],
                    lastNames[(l + i) % lastNames.length],
                    18 + random.nextInt(20)))
            .map(p -> new CourseResult(p, Arrays.stream(programTasks).collect(toMap(
                    task -> task,
                    task -> random.nextInt(51) + 50))));
}

private Stream<CourseResult> historyResults(final Random random) {
    int n = random.nextInt(names.length);
    int l = random.nextInt(lastNames.length);
    AtomicInteger t = new AtomicInteger(practicalHistoryTasks.length);

    return IntStream.iterate(0, i -> i + 1)
            .limit(3)
            .mapToObj(i -> new Person(
                    names[(n + i) % names.length],
                    lastNames[(l + i) % lastNames.length],
                    18 + random.nextInt(20)))
            .map(p -> new CourseResult(p,
                    IntStream.iterate(t.getAndIncrement(), i -> t.getAndIncrement())
                            .map(i -> i % practicalHistoryTasks.length)
                            .mapToObj(i -> practicalHistoryTasks[i])
                            .limit(3)
                            .collect(toMap(
                                    task -> task,
                                    task -> random.nextInt(51) + 50))));
}

Based on these methods I can calculate an average of each task by dividing sum of scores of this task by 3, because there are only 3 Persons tho I can make it so it divides by a number equal to number of CourseResult objects in a stream if these methods get their .limit(3) changed. I don’t know how to access keys of taskResults Map. I think I need them to then return a map of unique keys. A value for each unique key should be an average of values from taskResults map assigend to those keys.

Advertisement

Answer

For your first question: map each CourseResult to taskResults, flatmap to get all entries of each taskResults map form all CourseResults, group by map keys (task names) and collect averaging the values for same keys:

public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {
    return results.map(CourseResult::getTaskResults)
            .flatMap(m -> m.entrySet().stream())
            .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.averagingInt(Map.Entry::getValue)));
}

You can use the same approach for your second question to calculate the average for each task and finaly stream over the entries of the resulting map to find the task with the highest average.

public String easiestTask(Stream<CourseResult> results) {
    return results.map(CourseResult::getTaskResults)
            .flatMap(m -> m.entrySet().stream())
            .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.averagingInt(Map.Entry::getValue)))
            .entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .map(Map.Entry::getKey)
            .orElse("No easy task found");
}

To avoid code duplication you can call the first method within the second:

public String easiestTask(Stream<CourseResult> results) {
    return averageScoresPerTask(results).entrySet()
            .stream()
            .max(Map.Entry.comparingByValue())
            .map(Map.Entry::getKey)
            .orElse("No easy task found");
}

EDIT

To customize the calculation of the average regardless how many items your maps contain, don’t use the inbuilt operations like Collectors.averagingInt or Collectors.averagingDouble. Instead wrap your collector in collectingAndThen and sum the scores using Collectors.summingInt and finally after collecting divide using a divisor according if the task name starts with Lab or not:

public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {
    return results.map(CourseResult::getTaskResults)
            .flatMap(m -> m.entrySet().stream())
            .collect(Collectors.collectingAndThen(
                    Collectors.groupingBy(Map.Entry::getKey, Collectors.summingInt(Map.Entry::getValue)),
                    map -> map.entrySet()
                            .stream()
                            .collect(Collectors.toMap(
                                    Map.Entry::getKey,
                                    e -> e.getKey().startsWith("Lab") ? e.getValue() / 3. : e.getValue() / 4.))
                    ));
}
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement