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 CourseResult
s, 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.)) )); }