Reputation: 73
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
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
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
Reputation: 25903
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.
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]);
}
...
}
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());
}
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.
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