style(code): unify formatting across io and search components

This commit is contained in:
2026-09-02 13:14:28 +04:00
parent d7f4d30cc3
commit 1ec1d68a02
8 changed files with 261 additions and 250 deletions
@@ -13,76 +13,89 @@ import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import ru.project.student.Student;
import ru.project.student.StudentBuilder;
public class StudentCsvReader implements Iterator<Student>, AutoCloseable {
public class StudentCsvReader implements Iterator<Student>, AutoCloseable {
private static final String GROUP_NUMBER = "groupNumber";
private static final String AVERAGE_GRADE = "averageGrade";
private static final String RECORD_BOOK_NUMBER = "recordBookNumber";
private final CSVParser parser;
private final Iterator<CSVRecord> records;
private int lineOfFile=0;
public StudentCsvReader(Path path) {
if (!path.isAbsolute()) {
throw new IllegalArgumentException("File path must be absolute");
}
try {
CSVFormat format = CSVFormat.DEFAULT.builder()
.setHeader()
.setSkipHeaderRecord(true)
.get();
this.parser = CSVParser.parse(path, StandardCharsets.UTF_8, format);
validateHeader(parser.getHeaderMap());
this.records = parser.iterator();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
private int lineOfFile = 0;
public StudentCsvReader(Path path) {
if (!path.isAbsolute()) {
throw new IllegalArgumentException("File path must be absolute");
}
@Override
public boolean hasNext() {
return records.hasNext();
}
@Override
public Student next() {
if (!records.hasNext()) {
throw new NoSuchElementException("No more records at line " + (lineOfFile + 1));
}
lineOfFile++;
CSVRecord record = records.next();
if (record.size() != 3) {
throw new IllegalArgumentException(
"Problem at line " + lineOfFile + ". Expected 3 columns, but got " + record.size());
}
String groupNumber = record.get(GROUP_NUMBER);
String averageGradeStr = record.get(AVERAGE_GRADE);
String recordBookNumberStr = record.get(RECORD_BOOK_NUMBER);
double averageGrade;
int recordBookNumber;
try {
averageGrade = Double.parseDouble(averageGradeStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Problem at line " + lineOfFile + ": cannot parse averageGrade '" + averageGradeStr + "' as double", e);
}
try {
recordBookNumber = Integer.parseInt(recordBookNumberStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Problem at line " + lineOfFile + ": cannot parse recordBookNumber '" + recordBookNumberStr + "' as int", e);
}
return new StudentBuilder()
.setGroupNumber(groupNumber)
.setAverageGrade(averageGrade)
.setRecordBookNumber(recordBookNumber)
.build();
}
@Override
public void close() throws IOException {
parser.close();
}
private static void validateHeader(Map<String, Integer> headerMap) {
String[] expected = {GROUP_NUMBER, AVERAGE_GRADE, RECORD_BOOK_NUMBER};
if (headerMap.size() != expected.length
|| !headerMap.keySet().containsAll(Arrays.asList(expected))) {
throw new IllegalArgumentException("Invalid CSV header");
}
try {
CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).get();
this.parser = CSVParser.parse(path, StandardCharsets.UTF_8, format);
validateHeader(parser.getHeaderMap());
this.records = parser.iterator();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public boolean hasNext() {
return records.hasNext();
}
@Override
public Student next() {
if (!records.hasNext()) {
throw new NoSuchElementException("No more records at line " + (lineOfFile + 1));
}
lineOfFile++;
CSVRecord record = records.next();
if (record.size() != 3) {
throw new IllegalArgumentException(
"Problem at line " + lineOfFile + ". Expected 3 columns, but got " + record.size());
}
String groupNumber = record.get(GROUP_NUMBER);
String averageGradeStr = record.get(AVERAGE_GRADE);
String recordBookNumberStr = record.get(RECORD_BOOK_NUMBER);
double averageGrade;
int recordBookNumber;
try {
averageGrade = Double.parseDouble(averageGradeStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Problem at line "
+ lineOfFile
+ ": cannot parse averageGrade '"
+ averageGradeStr
+ "' as double",
e);
}
try {
recordBookNumber = Integer.parseInt(recordBookNumberStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Problem at line "
+ lineOfFile
+ ": cannot parse recordBookNumber '"
+ recordBookNumberStr
+ "' as int",
e);
}
return new StudentBuilder()
.setGroupNumber(groupNumber)
.setAverageGrade(averageGrade)
.setRecordBookNumber(recordBookNumber)
.build();
}
@Override
public void close() throws IOException {
parser.close();
}
private static void validateHeader(Map<String, Integer> headerMap) {
String[] expected = {GROUP_NUMBER, AVERAGE_GRADE, RECORD_BOOK_NUMBER};
if (headerMap.size() != expected.length
|| !headerMap.keySet().containsAll(Arrays.asList(expected))) {
throw new IllegalArgumentException("Invalid CSV header");
}
}
}
@@ -3,20 +3,20 @@ package ru.project.search;
import ru.project.student.Student;
public class AverageGradeRangeCriteria implements StudentSearchCriteria {
private final double min;
private final double max;
private final double min;
private final double max;
public AverageGradeRangeCriteria(double min, double max) {
if (min > max) {
throw new IllegalArgumentException("Min grade must be <= max grade");
}
this.min = min;
this.max = max;
public AverageGradeRangeCriteria(double min, double max) {
if (min > max) {
throw new IllegalArgumentException("Min grade must be <= max grade");
}
this.min = min;
this.max = max;
}
@Override
public boolean matches(Student student) {
double grade = student.getAverageGrade();
return grade >= min && grade <= max;
}
}
@Override
public boolean matches(Student student) {
double grade = student.getAverageGrade();
return grade >= min && grade <= max;
}
}
@@ -1,51 +1,51 @@
package ru.project.search;
import ru.project.student.Student;
import java.util.ArrayList;
import java.util.List;
import ru.project.student.Student;
public class CompositeStudentSearchCriteria implements StudentSearchCriteria {
private final SearchOperation operation;
private final List<StudentSearchCriteria> criteriaList = new ArrayList<>();
private final SearchOperation operation;
private final List<StudentSearchCriteria> criteriaList = new ArrayList<>();
public CompositeStudentSearchCriteria(SearchOperation operation) {
if (operation == null) {
throw new IllegalArgumentException("Operation must not be null");
}
this.operation = operation;
public CompositeStudentSearchCriteria(SearchOperation operation) {
if (operation == null) {
throw new IllegalArgumentException("Operation must not be null");
}
this.operation = operation;
}
public void add(StudentSearchCriteria criteria) {
if (criteria == null) {
throw new IllegalArgumentException("Criteria must not be null");
}
criteriaList.add(criteria);
}
public void remove(StudentSearchCriteria criteria) {
criteriaList.remove(criteria);
}
@Override
public boolean matches(Student student) {
if (criteriaList.isEmpty()) {
return operation == SearchOperation.INTERSECTION;
}
public void add(StudentSearchCriteria criteria) {
if (criteria == null) {
throw new IllegalArgumentException("Criteria must not be null");
if (operation == SearchOperation.UNION) {
for (StudentSearchCriteria c : criteriaList) {
if (c.matches(student)) {
return true;
}
criteriaList.add(criteria);
}
public void remove(StudentSearchCriteria criteria) {
criteriaList.remove(criteria);
}
@Override
public boolean matches(Student student) {
if (criteriaList.isEmpty()) {
return operation == SearchOperation.INTERSECTION;
}
if (operation == SearchOperation.UNION) {
for (StudentSearchCriteria c : criteriaList) {
if (c.matches(student)) {
return true;
}
}
return false;
} else {
for (StudentSearchCriteria c : criteriaList) {
if (!c.matches(student)) {
return false;
}
}
return true;
}
return false;
} else {
for (StudentSearchCriteria c : criteriaList) {
if (!c.matches(student)) {
return false;
}
}
return true;
}
}
}
}
@@ -3,23 +3,23 @@ package ru.project.search;
import ru.project.student.Student;
public class GroupNumberRangeCriteria implements StudentSearchCriteria {
private final String min;
private final String max;
private final String min;
private final String max;
public GroupNumberRangeCriteria(String min, String max) {
if (min == null || max == null) {
throw new IllegalArgumentException("Min and max must not be null");
}
if (min.compareTo(max) > 0) {
throw new IllegalArgumentException("Min group must be <= max group");
}
this.min = min;
this.max = max;
public GroupNumberRangeCriteria(String min, String max) {
if (min == null || max == null) {
throw new IllegalArgumentException("Min and max must not be null");
}
if (min.compareTo(max) > 0) {
throw new IllegalArgumentException("Min group must be <= max group");
}
this.min = min;
this.max = max;
}
@Override
public boolean matches(Student student) {
String group = student.getGroupNumber();
return group.compareTo(min) >= 0 && group.compareTo(max) <= 0;
}
}
@Override
public boolean matches(Student student) {
String group = student.getGroupNumber();
return group.compareTo(min) >= 0 && group.compareTo(max) <= 0;
}
}
@@ -3,20 +3,20 @@ package ru.project.search;
import ru.project.student.Student;
public class RecordBookNumberRangeCriteria implements StudentSearchCriteria {
private final int min;
private final int max;
private final int min;
private final int max;
public RecordBookNumberRangeCriteria(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("Min record number must be <= max");
}
this.min = min;
this.max = max;
public RecordBookNumberRangeCriteria(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("Min record number must be <= max");
}
this.min = min;
this.max = max;
}
@Override
public boolean matches(Student student) {
int number = student.getRecordBookNumber();
return number >= min && number <= max;
}
}
@Override
public boolean matches(Student student) {
int number = student.getRecordBookNumber();
return number >= min && number <= max;
}
}
@@ -1,6 +1,6 @@
package ru.project.search;
public enum SearchOperation {
UNION,
INTERSECTION
}
UNION,
INTERSECTION
}
@@ -3,5 +3,5 @@ package ru.project.search;
import ru.project.student.Student;
public interface StudentSearchCriteria {
boolean matches(Student student);
}
boolean matches(Student student);
}
@@ -1,4 +1,7 @@
package ru.project.io;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -8,111 +11,106 @@ import java.util.Spliterators;
import java.util.stream.StreamSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import ru.project.student.Student;
import ru.project.collection.MyList;
import ru.project.collectors.MyCollectors;
import static org.junit.jupiter.api.Assertions.*;
import ru.project.student.Student;
class StudentCsvReaderTest {
@TempDir
Path tempDir;
@Test
void shouldTestreadValidCsvIntoMyList() throws IOException {
Path csv = tempDir.resolve("students.csv");
String content = String.join("\n",
"groupNumber,averageGrade,recordBookNumber",
"M10,4.5,12345",
"B05,3.8,12346"
);
Files.writeString(csv, content, StandardCharsets.UTF_8);
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
var studentStream = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(reader, 0),
false
);
MyList<Student> students = studentStream
.collect(MyCollectors.toMyList());
assertEquals(2, students.size());
assertEquals("M10", students.get(0).getGroupNumber());
assertEquals(4.5, students.get(0).getAverageGrade(), 0.0);
assertEquals(12345, students.get(0).getRecordBookNumber());
assertEquals("B05", students.get(1).getGroupNumber());
assertEquals(3.8, students.get(1).getAverageGrade(), 0.0);
assertEquals(12346, students.get(1).getRecordBookNumber());
}
}
@Test
void shouldTestinvalidHeaderThrowsException() throws IOException {
Path csv = tempDir.resolve("bad-header.csv");
String content = """
badGroup,badGrade,badBook
M101,4.5,12345
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> new StudentCsvReader(csv.toAbsolutePath())
);
assertTrue(ex.getMessage().contains("Invalid CSV header"));
}
@Test
void shouldTestwrongColumnCountThrowsException() throws IOException {
Path csv = tempDir.resolve("wrong-columns.csv");
String content = """
groupNumber,averageGrade,recordBookNumber
M101,4.5
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
assertTrue(reader.hasNext());
@TempDir Path tempDir;
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
reader::next
);
assertTrue(ex.getMessage().contains("Expected 3 columns"));
}
@Test
void shouldTestreadValidCsvIntoMyList() throws IOException {
Path csv = tempDir.resolve("students.csv");
String content =
String.join(
"\n", "groupNumber,averageGrade,recordBookNumber", "M10,4.5,12345", "B05,3.8,12346");
Files.writeString(csv, content, StandardCharsets.UTF_8);
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
var studentStream =
StreamSupport.stream(Spliterators.spliteratorUnknownSize(reader, 0), false);
MyList<Student> students = studentStream.collect(MyCollectors.toMyList());
assertEquals(2, students.size());
assertEquals("M10", students.get(0).getGroupNumber());
assertEquals(4.5, students.get(0).getAverageGrade(), 0.0);
assertEquals(12345, students.get(0).getRecordBookNumber());
assertEquals("B05", students.get(1).getGroupNumber());
assertEquals(3.8, students.get(1).getAverageGrade(), 0.0);
assertEquals(12346, students.get(1).getRecordBookNumber());
}
@Test
void shouldTestparseErrorThrowsExceptionWithDetails() throws IOException {
Path csv = tempDir.resolve("parse-error.csv");
String content = """
groupNumber,averageGrade,recordBookNumber
M101,not-a-number,12345
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
assertTrue(reader.hasNext());
}
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
reader::next
);
assertTrue(ex.getMessage().contains("cannot parse averageGrade"));
}
@Test
void shouldTestinvalidHeaderThrowsException() throws IOException {
Path csv = tempDir.resolve("bad-header.csv");
String content =
"""
badGroup,badGrade,badBook
M101,4.5,12345
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
IllegalArgumentException ex =
assertThrows(
IllegalArgumentException.class, () -> new StudentCsvReader(csv.toAbsolutePath()));
assertTrue(ex.getMessage().contains("Invalid CSV header"));
}
@Test
void shouldTestwrongColumnCountThrowsException() throws IOException {
Path csv = tempDir.resolve("wrong-columns.csv");
String content =
"""
groupNumber,averageGrade,recordBookNumber
M101,4.5
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
assertTrue(reader.hasNext());
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, reader::next);
assertTrue(ex.getMessage().contains("Expected 3 columns"));
}
@Test
void shouldTestemptyDataAfterHeaderThrowsNoSuchElement() throws IOException {
Path csv = tempDir.resolve("empty-data.csv");
String content = """
groupNumber,averageGrade,recordBookNumber
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
}
// ИСПРАВЛЕНО: добавлен try-with-resources
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
assertFalse(reader.hasNext());
@Test
void shouldTestparseErrorThrowsExceptionWithDetails() throws IOException {
Path csv = tempDir.resolve("parse-error.csv");
String content =
"""
groupNumber,averageGrade,recordBookNumber
M101,not-a-number,12345
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
assertTrue(reader.hasNext());
assertThrows(NoSuchElementException.class, reader::next);
}
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, reader::next);
assertTrue(ex.getMessage().contains("cannot parse averageGrade"));
}
@Test
void shouldTestrelativePathThrowsException() {
Path relative = Path.of("relative-path.csv");
}
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> new StudentCsvReader(relative)
);
assertTrue(ex.getMessage().contains("absolute"));
@Test
void shouldTestemptyDataAfterHeaderThrowsNoSuchElement() throws IOException {
Path csv = tempDir.resolve("empty-data.csv");
String content =
"""
groupNumber,averageGrade,recordBookNumber
""";
Files.writeString(csv, content, StandardCharsets.UTF_8);
// ИСПРАВЛЕНО: добавлен try-with-resources
try (var reader = new StudentCsvReader(csv.toAbsolutePath())) {
assertFalse(reader.hasNext());
assertThrows(NoSuchElementException.class, reader::next);
}
}
}
@Test
void shouldTestrelativePathThrowsException() {
Path relative = Path.of("relative-path.csv");
IllegalArgumentException ex =
assertThrows(IllegalArgumentException.class, () -> new StudentCsvReader(relative));
assertTrue(ex.getMessage().contains("absolute"));
}
}