英文:
How to parse a text file that is formatted like a gradebook?
问题
我正试图读取一个文本文件,其中的数据格式如下:
姓名|测试1|测试2|测试3|测试4|测试5|测试6|测试7|测试8|测试9|测试10
约翰·史密斯|82|89|90|78|89|96|75|88|90|96
简·多e|90|92|93|90|89|84|97|91|87|91
约瑟夫·克鲁兹|68|74|78|81|79|86|80|81|82|87
我的目标是能够获得每位学生的平均测试成绩,以及每个测试(列)的平均分和总体平均分。我在尝试“分离”第一列(学生的姓名)和他们的测试分数方面遇到了困难。有没有办法忽略或跳过第一列?此外,存储这些测试分数的最佳方法是什么,以便我能够进行我提到的那些计算?
我已成功使用以下方法读取了文件的内容:
in.useDelimiter("\\|");
for(int i = 0; in.hasNextLine(); i++){
System.out.println(in.next());
英文:
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());}
答案1
得分: 2
解决方案
你可以通过在进入循环之前完全消耗第一行,只需调用
in.nextLine();
然后第一行就会被消耗掉。
分割
然而,我会采取不同的方法,逐行解析,然后在 |
上进行分割,这样更容易处理每行给定的数据。
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]);
}
...
}
正确的面向对象编程(OOP)方法
理想情况下,你应该对此添加一些面向对象编程(OOP)方法,创建一个名为 Student
的类,其中包含诸如:
public class Student {
private final String name;
private final int[] testResults;
// 构造方法,获取器,...
}
然后给它一个 parseLine
方法,如下:
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);
}
然后你的解析过程大大简化为:
List<Student> students = new ArrayList<>();
in.nextLine();
while (in.hasNextLine()) {
students.add(Student.parseLine(in.nextLine()));
}
流与 NIO
或者,如果你喜欢使用流,可以使用 NIO 读取文件:
List<Student> students = Files.lines(Path.of("myFile.txt"))
.skip(1)
.map(Student::parseLine)
.collect(Collectors.toList());
非常清晰、紧凑且易读。
平均分数
> 我的目标是能够获得每个学生的平均考试成绩,以及每个考试(列)的平均成绩和总体平均成绩。
通过适当的面向对象结构,如上所示,这是相当简单的。首先,计算学生的平均成绩,只需在 Student
类中添加一个方法:
public double getAverageScore() {
double total = 0.0;
for (int testResult : testResults) {
total += testResult;
}
return total / testResults.length;
}
替代的流解决方案:
return IntStream.of(testResults).average().orElseThrow();
接下来,每列的平均分数:
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();
}
以及流解决方案:
return students.stream()
.mapToInt(student -> student.getTestScores[testId])
.average().orElseThrow();
最后,总体平均分数,可以通过计算每个学生的平均分数的平均值来得出:
public static double averageTestScore(List<Student> students) {
double total = 0.0;
for (Student student : students) {
total += student.getAverageScore();
}
return total / students.size();
}
以及流变体:
return students.stream()
.mapToDouble(Student::getAverageScore)
.average().orElseThrow();
英文:
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();
答案2
得分: 1
我的想法是将您读取的数据存储在一个Map
中。其中,每个学生的名字是“键”,分数存储在一个List<Integer>
中,您将其作为值放入映射中。
就像这样:
Map<String, List<Integer>> scores = new HashMap<>();
List<Integer> studentScores = new ArrayList<>();
// 然后逐个读取分数并添加
studentScores.add(82);
studentScores.add(89);
// ....
// 当您完成一个学生的读取后,将其添加到映射中
scores.put("John Smith", studentScores);
// 最后,当您需要这些值(例如进行计算)时,可以这样获取:
scores.get("John Smith").get(0) // 这将是John的列表中的第一个值 => 82
现在针对实际的读取操作:我认为您不需要分隔符,只需读取整行,然后在之后使用split
函数进行拆分:
scanner.nextLine(); // 我差点忘记了:这会读取并忽略文件的第一行
while(scanner.hasNextLine()){
String line = scanner.nextLine(); // 这是一整行,例如"John Smith|82|89|....."
// 现在您需要拆分它
String[] columns = line.split("\\|"); // 直接的方法,得到一个类似这样的数组:["John Smith", "82", "89", ...]
String studentName = columns[0]; // 首先获取姓名
List<Integer> studentScores = new ArrayList<>();
for(int i=1; i<columns.length; i++){ // 现在获取分数
studentScores.add(Integer.valueOf(columns[i])); // 读取索引为i处的分数,将其转换为整数,并添加到分数列表中
}
// 最后将所有内容放入映射中
scores.put(studentName, studentScores);
}
英文:
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);
}
答案3
得分: 0
也许尝试使用 in.nextLine()
:
// 跳过包含标题的第一行
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("\\|");
}
英文:
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("\\|");
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论