Home > Enterprise >  Java streams average
Java streams average

Time:07-03

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.

CodePudding user response:

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

CodePudding user response:

To create a map containing an average score for each task, you need to flatten the map taskResults of every CourseResult result object in the stream and group the data by key (i.e. by task name).

For that you can use collector groupingBy(), as its downstream collector that would be responsible for calculation the average from the score-values mapped to the same task you can use averagingDouble().

That's how it might look like:

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

To find the easiest task, you can use this map instead of passing the stream as an argument because the logic of this method requires applying the same operations. It would make in the real life scenario when you're retrieving the data stored somewhere and your case you can't pass the stream because your stream data is random (remainder: you can execute a stream pipeline only once, when it hits the terminal operation - it's done, you can't use it anymore, hence you can't pass the same stream in these two methods).

public String easiestTask(Map<String, Double>  averageByTask) {
    
    return averageByTask.entrySet().stream()
        .max(Map.Entry.comparingByValue())
        .map(Map.Entry::getKey)
        .orElse("no data"); // or orElseThrow() depending on your needs
}
  • Related