Nari
Nari

Reputation: 49

Java] How to calculate average score by subjects (using ArrayList)

I'm trying to figure out if there's a way to calculate total and average scores by subjects (not by students!). FYI, I wrote this code while I was studying ArrayList.

Here's the codes I'm working on. The result I wanna get is mathTotal: 180, korTotal: 150, engTotal: 90 & mathAvg: 90, korAvg: 75, engAvg: 90.

public class Subject {

    private String subjectName;
    private int score;
    
    public Subject(String subjectName, int score) {
        this.subjectName = subjectName;
        this.score = score;
    }
    
    public String getSubName() {
        return subjectName;
    }
    
    public int getScore() {
        return score;
    }   
}
import java.util.ArrayList;

public class Student {
    private int studentID;
    private String studentName;
    ArrayList<Subject> subjectList;
    
    public Student(int studentID, String studentName) {
        this.studentID = studentID;
        this.studentName = studentName;
        subjectList = new ArrayList<Subject>();
    }
    
    public void addSubject(String subjectName, int score) {
        Subject subject = new Subject(subjectName, score);
        
        subjectList.add(subject);
    }
    
    public void calcScore() {
        int total = 0;
        double subCnt = 0;
        
        System.out.println("< "+studentName+"'s score >");
        for (Subject subject: subjectList) {
            total += subject.getScore();
            System.out.println(subject.getSubName()+": "+subject.getScore());
            subCnt++;
        }
        double average = total/subCnt;
        String averageCut = String.format("%.2f", average);
        System.out.println("total: "+total);
        System.out.println("average: "+averageCut);
    }
}
public class StudentTest {
    public static void main(String[] args) {
        
        Student student1 = new Student(1001, "Amy Park");
        Student student2 = new Student(1005, "Steven Yeun");
        
        student1.addSubject("Math", 80);
        student1.addSubject("Korean", 90);
        
        student2.addSubject("Math", 100);
        student2.addSubject("Korean", 60);
        student2.addSubject("English", 90);
        
        student1.calcScore();
        student2.calcScore();
    }
}

Upvotes: 0

Views: 2203

Answers (5)

Edgar Magallon
Edgar Magallon

Reputation: 559

You can add a static HashMap inside Subject class. And each time you add a new subject, you will have add the subject to the HashMap as a key, and as value will be an ArrayList which will contain each score you are adding.

The Subject class:

class Subject {
private String subjectName;
private int score;
public static HashMap<String,ArrayList<Integer>> subjectsScores = new HashMap<>();

public Subject(String subjectName, int score) {
    
    
    if(subjectsScores.containsKey(subjectName))
    {
        subjectsScores.get(subjectName).add(score);
    }
    else{
        subjectsScores.put(subjectName,new ArrayList<>(){{add(score);}});
    }
    
    this.subjectName = subjectName;
    this.score = score;
}

public String getSubName() {
    return subjectName;
}

public int getScore() {
    return score;
}   
}

Inside the Constructor, you are adding the correct values to the hashmap.What it does is checking if you already added the subject, if you did it, then, the score will be added to the ArrayList of the current subject: subjectsScores.get(subjectName).add(score);. The subjectName variable is the current subject you are adding from the main, and add(score) corresponds to the ArrayList of the key subjectName.

And inside your main class, you can iterate over the hashmap with a foreach, like this:

public class StudentTest {
public static void main(String[] args) {
    
    Student student1 = new Student(1001, "Amy Park");
    Student student2 = new Student(1005, "Steven Yeun");
    
    student1.addSubject("Math", 80);
    student1.addSubject("Korean", 90);
    
    student2.addSubject("Math", 100);
    student2.addSubject("Korean", 60);
    student2.addSubject("English", 90);
    
    student1.calcScore();
    student2.calcScore();
    
    Subject.subjectsScores.forEach((k,v)->{
        
        int total=0;
        
        for(int sc : v)
        {
            total+=sc;
        }
        
        int av = total / v.size();
        
        System.out.printf("Subject: %s -- Total: %d -- Average: %d\n",k,total,av);
        
    }); 
   }
}

Upvotes: 3

Oleg Cherednik
Oleg Cherednik

Reputation: 18255

I think first of all you have to modify your design. I offer to add School abstraction to hold Student list and calculations:

public final class School {

    private final List<Student> students = new ArrayList<>();

    public void addStudent(Student student) {
        students.add(student);
    }

    public List<Student> getStudents() {
        return Collections.unmodifiableList(students);
    }

    public Map<String, Double> calcAverageScoreBySubject() {
        Map<String, Double> map = new HashMap<>();

        getScoresBySubject().forEach((subjectName, scores) ->
                map.put(subjectName, scores.stream()
                                           .mapToDouble(Integer::doubleValue)
                                           .average().orElse(0)));

        return map;
    }

    public Map<String, Integer> calcTotalScoreBySubject() {
        Map<String, Integer> map = new HashMap<>();

        getScoresBySubject().forEach((subjectName, scores) ->
                map.put(subjectName, scores.stream().mapToInt(i -> i).sum()));

        return map;
    }

    private Map<String, List<Integer>> getScoresBySubject() {
        Map<String, List<Integer>> map = new HashMap<>();

        students.stream()
                .map(Student::getSubjects)
                .flatMap(List::stream)
                .forEach(subject -> {
                    if (!map.containsKey(subject.getName()))
                        map.put(subject.getName(), new ArrayList<>());
                    map.get(subject.getName()).add(subject.score);
                });

        return map;
    }
}

Then you should use unmodifiable objects for Student and Subject:

public static final class Subject {

    private final String name;
    private final int score;

    public Subject(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }
}
public final class Student {

    private final int id;
    private final String name;
    private final List<Subject> subjects = new ArrayList<>();

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void addSubject(Subject subject) {
        subjects.add(subject);
    }

    public List<Subject> getSubjects() {
        return Collections.unmodifiableList(subjects);
    }
}

And finally your test could look like this:

public class StudentTest {

    public static void main(String... args) {
        School school = new School();
        school.addStudent(amyPark());
        school.addStudent(stevenYeun());

        Map<String, Double> subjectNameAverageScore = school.calcAverageScoreBySubject();
        Map<String, Integer> subjectNameTotalScore = school.calcTotalScoreBySubject();

        school.getStudents().forEach(student -> {
            System.out.format("< %d: %s's core >\n", student.getId(), student.getName());

            student.getSubjects().forEach(subject -> {
                String subjectName = subject.getName();
                int score = subject.getScore();
                int total = subjectNameTotalScore.get(subjectName);
                double average = subjectNameAverageScore.get(subjectName);
                System.out.format(Locale.ENGLISH, "%s: %d (subject - total: %d, average: %.1f)\n", subjectName, score, total, average);
            });

            System.out.println();
        });
    }

    private static Student amyPark() {
        Student student = new Student(1001, "Amy Park");
        student.addSubject(new Subject("Math", 80));
        student.addSubject(new Subject("Korean", 90));
        return student;
    }

    private static Student stevenYeun() {
        Student student = new Student(1005, "Steven Yeun");
        student.addSubject(new Subject("Math", 100));
        student.addSubject(new Subject("Korean", 60));
        student.addSubject(new Subject("English", 90));
        return student;
    }
}

Output:

< 1001: Amy Park's core >
Math: 80 (subject - total: 180, average: 90.0)
Korean: 90 (subject - total: 150, average: 75.0)

< 1005: Steven Yeun's core >
Math: 100 (subject - total: 180, average: 90.0)
Korean: 60 (subject - total: 150, average: 75.0)
English: 90 (subject - total: 90, average: 90.0)

Upvotes: 0

Nowhere Man
Nowhere Man

Reputation: 19575

As explained in earlier answers, in order to get the stats per subject, a static Map may be added to Subject class to store the scores and facilitate calculation of the total and average scores.

However, since Java 8 there is class IntSummaryStatistics which can be efficiently used with Stream API to collect such statistics as count, min, max, total, and average values by using Collectors.summarizingInt

For example, method Student::calcScore may be modified to use this class as follows:

public void calcScore() {
    System.out.printf("< %s's score >%n", studentName);

    IntSummaryStatistics stats = subjectList.stream() // Stream<Subject>
            // print each subject while collecting stats
            .peek(subject -> System.out.printf("%-10s:\t%4d%n",    
                    subject.getSubjectName(), subject.getScore()))
            .collect(Collectors.summarizingInt(Subject::getScore));

    // print stats
    System.out.println("-".repeat(12 + studentName.length()));
    System.out.printf("%-10s:\t%4d%n", "TOTAL", stats.getSum());
    System.out.printf("%-10s:\t%7.2f%n", "AVERAGE", stats.getAverage());
    System.out.println("=".repeat(12 + studentName.length()));
}

And class Subject may be updated as follows to utilize IntSummaryStatistics:

public class Subject {
    private String subjectName;
    private int score;
    private static Map<String, List<Integer>> rawScoresBySubject = new TreeMap<>();

    public Subject(String subjectName, int score) {
        this.subjectName = subjectName;
        this.score = score;

        rawScoresBySubject
            .computeIfAbsent(subjectName, (key) -> new ArrayList<>())
            .add(score);
    }

    public static Map<String, IntSummaryStatistics> getStats() {
        return rawScoresBySubject.entrySet().stream() // Stream<Map.Entry<String, Integer>>
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue().stream() // Stream<Integer>
                    .collect(Collectors.summarizingInt(Integer::intValue))
                ));
    }

    // getters/setters
}

Here a TreeMap is used to provide sorting by subjectName. Also, the map is populated using computeIfAbsent method which allows to add an initial value (in this case it's an empty ArrayList) and use this value immediately by adding the score.

The test:

public static void main(String[] args) {
    Student student1 = new Student(1001, "Amy Park");
    Student student2 = new Student(1005, "Steven Yeun");

    student1.addSubject("Math", 80);
    student1.addSubject("Korean", 90);

    student2.addSubject("Math", 100);
    student2.addSubject("Korean", 60);
    student2.addSubject("English", 90);

    student1.calcScore();
    student2.calcScore();

    Subject.getStats()
            .forEach((subject, stats) ->
                System.out.printf("%-10s:\t%7.2f\t%4d / %d%n",
                    subject, stats.getAverage(), stats.getSum(), stats.getCount()
            ));
}

Output:

< Amy Park's score >
Math      :   80
Korean    :   90
--------------------
TOTAL     :  170
AVERAGE   :   85.00
====================
< Steven Yeun's score >
Math      :  100
Korean    :   60
English   :   90
-----------------------
TOTAL     :  250
AVERAGE   :   83.33
=======================
English   :   90.00   90 / 1
Korean    :   75.00  150 / 2
Math      :   90.00  180 / 2

Upvotes: 0

Koushlendra
Koushlendra

Reputation: 290

If you could add getter/setter for Subject List in your Student class, the below code would work for you:

public class StudentTest {
    public static void main(String[] args) {

        Student student1 = new Student(1001, "Amy Park");
        Student student2 = new Student(1005, "Steven Yeun");

        student1.addSubject("Math", 80);
        student1.addSubject("Korean", 90);

        student2.addSubject("Math", 100);
        student2.addSubject("Korean", 60);
        student2.addSubject("English", 90);

        List<Student> students =new ArrayList<>();
        students.add(student1);
        students.add(student2);

        List<Subject> subjectList = students.stream()
                .flatMap(student -> student.getSubjectList().stream())
                .collect(Collectors.toList());

        Collector<Subject, ?, Double> avgScoreCollector = Collectors.averagingInt(Subject::getScore);
        Collector<Subject, ?, Integer> totalScoreCollector = Collectors.summingInt(Subject::getScore);
        Map<String, Integer> groupedSubjectsSum =
                subjectList.stream().collect(Collectors.groupingBy(Subject::getSubName, totalScoreCollector));
        Map<String, Double> groupedSubjectsAvg =
                subjectList.stream().collect(Collectors.groupingBy(Subject::getSubName, avgScoreCollector));

        for (Map.Entry<String, Integer> entry : groupedSubjectsSum.entrySet()) {
            System.out.println("Sum of " + entry.getKey() + ":" + entry.getValue());
        }
        System.out.println("\n*************************\n");
        for (Map.Entry<String, Double> entry : groupedSubjectsAvg.entrySet()) {
            System.out.println("Avg of " + entry.getKey() + ":" + entry.getValue());
        }
    }
}

Output:

Sum of English:90
Sum of Korean:150
Sum of Math:180

*************************

Avg of English:90.0
Avg of Korean:75.0
Avg of Math:90.0

In this solution, we are merging all the list of subjects together to form one list and then performing the avg and sum functions on it. Using java8 lambda provides better flexibility to modify and maintain.

Upvotes: 2

geco17
geco17

Reputation: 5294

From inside the Student class you can't get average scores by subjects since each student has a list of subjects. One possibility would be to define an accessor method for the list of subjects (i.e. Student#getSubjects() and use that outside of the student class to calculate the statistics by subject. You could also use a map to keep track of your totals, and counts, by subject.

For example:

Map<String, Integer> gradeTotalsBySubject = new HashMap<>();
Map<String, Integer> countsBySubject = new HashMap<>();
for (student : students) {
    for (subject : student.getSubjects()) {
        int currentGradeTotal = gradeTotalsBySubject.getOrDefault(subject.getName(), 0);
        currentGradeTotal += subject.getScore();
        gradeTotalsBySubject.put(subject.getName(), current);
        int currentCount = countsBySubject.getOrDefault(subject.getName(), 0);
        currentCount++;
        countsBySubject.put(subject.getName(), currentCount);
    }
}

This will give you the totals you need, and the numbers to divide by, grouped by subject. At that point you can iterate over the map entries in gradeTotalsBySubject and dividing each value by the count in the corresponding counts map you can get the averages.

Upvotes: 3

Related Questions