JAVA8 : convert list of raw data to map of key and list of calculated values

Viewed 339

I have a list of raw student data as mentioned below.

  [
    {
    section : 1A
    branch : science
    studentName : abc
    marks : 50
    },
    {
    section : 1A
    branch : science
    studentName : sss
    marks : 20
    },
    {
    section : 1A
    branch : science
    studentName : asd
    marks : 40
    },
    {
    section : 1A
    branch : Maths
    studentName : www
    marks : 10
    },
    {
    section : 1A
    branch : Maths
    studentName : abc
    marks : 70
    },
    {
    section : 1A
    branch : Maths
    studentName : abc
    marks : 86
    },
    {
    section : 2A
    branch : Maths
    studentName : abc
    marks : 55
    },
    {
    section : 2A
    branch : Maths
    studentName : abc
    marks : 89
    },
    {
    section : 2A
    branch : Maths
    studentName : abc
    marks :12
    },
  {
    section : 2A
    branch : physical
    studentName : abc
    marks :passed
    },
  {
    section : 2A
    branch : physical
    studentName : abc
    marks :failed
    },
    ]

Now I want to transform this data into following format section wise: I am want the minimum marks and max marks for all students for particular subject. Same i want to do across all section. Basically I want to see what is the min and max marks for all the subject across all sections.

I have one more subject "physical test" and value is in pass and fail. I want to know how many are failed including above details. Now i have to declare this variable as String as I may contain actual marks or pass/fail. do you have any suggestions?

section 1A:
science : 20-50(min-max)
math : (10-86)(min-max)
section 2S:
science : 20-50(min-max)
math : (12-89)(min-max)

I am able to map sec to listofstudents using below code.

Map<String, List<Student>> map2 = studentsList.stream().collect(Collectors
                    .groupingBy(Student::getSection, HashMap::new, Collectors.toCollection(ArrayList::new)));

I am receiving that value as passed/failed from my source system and I can not change that. I have defined that variable as String as it may contain marks as Integer or string(passed/failed). I have been trying to write clean code using streams with avoiding too many loops and if statements but I am not able to do so. If subject have marks than group them as min-max if subject have passed/failed value than noOfFailed_totalstudents.
I am not able to do it using java streams. Please help. I am also trying it and will update if done.

2 Answers

Here is my best try. The Student class is not really clean. It would be better to model it with two different classes for points related subjects and failed/passed related subjects and use a Factory, but anyway:

@Data
public static class Student implements Comparable<Student>{
    private String section;
    private String branch;
    private String studentName;
    private String marks;
    private Integer points;

    public Student(String section, String branch, String studentName, String marks) {
        this.section = section;
        this.branch = branch;
        this.studentName = studentName;
        try {
            this.points = Integer.valueOf(marks);
        } catch (Exception e) {
            this.marks = marks;
        }
    }

    @Override
    public int compareTo(Student o) {
        if (o != null){
           if (this.getPoints() != null && o.getPoints() != null){
               return this.getPoints() - o.getPoints();
           } else if (this.getMarks() != null && o.getMarks() != null){
               if (this.getMarks().equalsIgnoreCase(o.getMarks())){
                   return 0;
               } else if (this.getMarks().equals("passed")){
                   return 1;
               } else {
                   return -1;
               }
           }
        }
        return 1;
    }

then I map the json list in a Map<String, Map<String, List<Student>>> instance, where the first map contains the different sections and the second map contains all branches of a given section:

Map<String, Map<String, List<Student>>> map2 = studentList.stream()
.collect(Collectors.groupingBy(Student::getSection, HashMap::new, 
  Collectors.groupingBy(Student::getBranch, TreeMap::new, Collectors.toCollection(ArrayList::new))));

eventually, the map is iterated and for points releated subjects the min/max calculation is done and for failed/passed subjects counting of failed is performed:

         map2.entrySet().stream()
            .flatMap(sm -> sm.getValue().entrySet().stream()) // iterate the sections
            .forEach(b -> {                                   //for each subject
                List<Student> value = b.getValue();
                if (value.size() > 0) {
                    if (value.get(0).getPoints() != null) {
                        //points subjects
                        Collections.sort(value);
                        String out = "Section " + value.get(0).section + ": " + b.getKey() + ": " + value.get(0).getPoints() + "-" + value.get(value.size() - 1).getPoints() + "(min-max)";
                        System.out.println(out);
                    } else if (value.get(0).getMarks() != null) {
                        //failed/passed subjects
                        long failed = value.stream()
                                .filter(s -> s.getMarks().equalsIgnoreCase("failed"))
                                .count();
                        String out = "Section " + value.get(0).section + ": " + b.getKey() + ": " + failed + "-" + value.size() + "(failed-total)";
                        System.out.println(out);
                    }
                }
            });

the output is a little bit different from the required one:

Section 1A: math: 12-89(min-max)
Section 1A: physics: 2-3(failed-total)

Actually doing this in a single step by stream API is difficult. so did it in several steps. I supposed you have a json string and converted it to an array of Student objects.

step1: convert json string to object:

class Student {
    private String section;
    private String branch;
    private String studentName;
    private Object marks;
}  

String jsonData = "[{\"section\":\"1A\", \"branch\":\"science\", \"studentName\":\"abc\", \"marks\":50}, {\"section\":\"1A\", \"branch\":\"science\", \"studentName\":\"sss\", \"marks\":20}, {\"section\":\"1A\", \"branch\":\"science\", \"studentName\":\"asd\", \"marks\":40}, {\"section\":\"1A\", \"branch\":\"Maths\", \"studentName\":\"www\", \"marks\":10}, {\"section\":\"1A\", \"branch\":\"Maths\", \"studentName\":\"abc\", \"marks\":70}, {\"section\":\"1A\", \"branch\":\"Maths\", \"studentName\":\"abc\", \"marks\":86}, {\"section\":\"2A\", \"branch\":\"Maths\", \"studentName\":\"abc\", \"marks\":55}, {\"section\":\"2A\", \"branch\":\"Maths\", \"studentName\":\"abc\", \"marks\":89}, {\"section\":\"2A\", \"branch\":\"Maths\", \"studentName\":\"abc\", \"marks\":12},{\"section\":\"2A\", \"branch\":\"physical\", \"studentName\":\"abc\", \"marks\":\"failed\"},{\"section\":\"2A\", \"branch\":\"physical\", \"studentName\":\"abc\", \"marks\":\"passed\"}]";

ObjectMapper objectMapper = new ObjectMapper();

step2: groupping by section and branch properties.

Map<String, Map<String, List<Object>>> map = 
Arrays.stream(objectMapper.readValue(jsonData, Student[].class))
            .collect(Collectors.groupingBy(Student::getSection,
                    Collectors.groupingBy(Student::getBranch,
                            Collectors.mapping(Student::getMarks, Collectors.toList()))));

step3:

List<Statistics> result = map.entrySet().stream()
            .map(Statistics::new)
            .collect(Collectors.toList());

for simplifying the logic I've created some classes.

Statistics class takes an Map.Entry<> as an argument and initial its properties with this argument as bellow.

class Statistics {
  private String section;
  private MinMax science;
  private MinMax maths;
  private long totalFailed;

  public Statistics(Map.Entry<String, Map<String, List<Object>>> entry) {
     this.section = entry.getKey();
     IntSummaryStatistics maths = entry.getValue().getOrDefault("Maths",emptyList())
            .stream()
            .map(o -> Integer.valueOf(o.toString()))
            .collect(Collectors.summarizingInt(value -> value));
     this.maths = new MinMax(maths.getMin(), maths.getMax());

     IntSummaryStatistics science = entry.getValue().getOrDefault("science",emptyList())
            .stream()
            .map(o -> Integer.valueOf(o.toString()))
            .collect(Collectors.summarizingInt(value -> value));
     this.science = new MinMax(science.getMin(), science.getMax());

     this.totalFailed = entry.getValue()
         .getOrDefault("physical",emptyList())
         .stream()
         .filter(o -> o.toString().equals("failed"))
         .count();
  }
}

and

class MinMax {
  private Integer min;
  private Integer max;
}
Related