Nico
Nico

Reputation: 73

How to parse a text file that is formatted like a gradebook?

I'm trying to read a text file that has data formatted as follows:

Name|Test1|Test2|Test3|Test4|Test5|Test6|Test7|Test8|Test9|Test10   
John Smith|82|89|90|78|89|96|75|88|90|96
Jane Doe|90|92|93|90|89|84|97|91|87|91
Joseph Cruz|68|74|78|81|79|86|80|81|82|87

My goal is to be able to get each student's average test score, as well as the average score per test (column) and the average score overall. I am having trouble "separating" the first columns (the names of the students) from their test scores. Is there a way to ignore or skip the first column? Also, what is the best way to store those test scores so that I will be able to do those calculations I mentioned?

I have successfully read the contents of the file by using the below method:

in.useDelimiter("\\|");
for(int i = 0; in.hasNextLine(); i++){
    System.out.println(in.next());}

Upvotes: 2

Views: 215

Answers (3)

GameDroids
GameDroids

Reputation: 5662

My idea would be to store the data you read in a Map. Where each student's name is the "key" and the scores are stored in an List<Integer> which you put as the value in you map.

Like so:

Map<String, List<Integer>> scores = new HashMap<>();

List<Integer> studentScores = new ArrayList<>();
// then you read the scores one by one and add them 
studentScores.add(82);
studentScores.add(89);
....
// when you are finished with the student you add him to the map
scores.put("John Smith", studentScores);

// in the end, when you need the values (for your calculation for example) you can get them like this:

scores.get("John Smith").get(0)   // which will be the 1st value from John's list => 82

Now to the actual reading: I don't think you need a delimiter, just read the whole line and split it afterwards:

scanner.nextLine();                      // I almost forgot: this reads and forgets the very first line of your file

while(scanner.hasNextLine()){
     String line = scanner.nextLine();   // this is a whole line like "John Smith|82|89|....."
     // now you need to split it
     String[] columns = line.split("|"); // straight forward way to get an array that looks like this: ["John Smith", "82", "89", ...]

    
     String studentName = columns[0];   // first we get the name
     List<Integer> studentScores = new ArrayList<>();
     for(int i=1;i<columns; i++){       // now we get the scores
        studentScores.add(Integer.valueOf(columns[i])); // will read the score at index i, cast it to an Integer and add it to the score list
     }
     // finally you put everything in your map
     scores.put(studentName, studentScores);
}

Upvotes: 1

Ela Łabaj
Ela Łabaj

Reputation: 74

Maybe try using in.nextLine():

//to skip first line with headers
in.nextLine();

while (in.hasNextLine()) {
        String studentLine = in.nextLine();
        int firstColumnEnd = studentLine.indexOf("|");

        String name = studentLine.substring(0, firstColumnEnd - 1);
        String[] tests = studentLine.substring(firstColumnEnd + 1).split("\\|");
}

Upvotes: 0

Zabuzard
Zabuzard

Reputation: 25903

Solution

You can achieve what you want by fully consuming the first line before you enter your loop, just call

in.nextLine();

before and the first line is consumed.


Splitting

However, I would approach this differently, parsing line by line and then splitting on |, that way it is easier to work with the data given per line.

in.nextLine();
while (in.hasNextLine()) {
    String line = in.nextLine();
    String[] data = line.split("\\|");

    String name = data[0];
    int[] testResults = new int[data.length - 1];
    for (int i = 0; i < testResults.length; i++) {
        testResults[i] = Integer.parseInt(data[i + 1]);
    }

    ...
}

Proper OOP

Ideally you would add some OOP to that, create a class Student with fields like

public class Student {
    private final String name;
    private final int[] testResults;

    // constructor, getter, ...
}

and then give it a parseLine method like:

public static Student parseLine(String line) {
    String[] data = line.split("\\|");

    String name = data[0];
    int[] testResults = new int[data.length - 1];
    for (int i = 0; i < testResults.length; i++) {
        testResults[i] = Integer.parseInt(data[i + 1]);
    }

    return new Student(name, testResults);
}

Then your parsing simplifies heavily to just:

List<Student> students = new ArrayList<>();
in.nextLine();
while (in.hasNextLine()) {
    students.add(Student.parseLine(in.nextLine());
}

Streams and NIO

Or if you like streams, just read the file using NIO:

List<Student> students = Files.lines(Path.of("myFile.txt"))
    .skip(1)
    .map(Student::parseLine)
    .collect(Collectors.toList());

very clear, compact and readable.


Average score

My goal is to be able to get each student's average test score, as well as the average score per test (column) and the average score overall.

With the proper OOP structure, as shown, this is fairly simple. First, a students average score, just add a method to the Student class:

public double getAverageScore() {
    double total = 0.0;
    for (int testResult : testResults) {
        total += testResult;
    }
    return total / testResults.length;
}

Alternative stream solution:

return IntStream.of(testResults).average().orElseThrow();

Next, the average score per column:

public static double averageTestScore(List<Student> students, int testId) {
    double total = 0.0;
    for (Student student : students) {
        total += student.getTestScores()[testId];
    }
    return total / students.size();
}

And the stream solution:

 return students.stream()
       .mapToInt(student -> student.getTestScores[testId])
       .average().orElseThrow();

And finally the average score overall, which can be computed by taking the average of each students average score:

public static double averageTestScore(List<Student> students) {
    double total = 0.0;
    for (Student student : students) {
        total += student.getAverageScore();
    }
    return total / students.size();
}

and the stream variant:

return students.stream()
    .mapToDouble(Student::getAverageScore)
    .average().orElseThrow();

Upvotes: 2

Related Questions