Merge branch 'develop' into feature/ui

This commit is contained in:
2026-09-01 19:37:46 +04:00
10 changed files with 1096 additions and 144 deletions
+12
View File
@@ -42,12 +42,24 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency> <dependency>
<groupId>com.googlecode.lanterna</groupId> <groupId>com.googlecode.lanterna</groupId>
<artifactId>lanterna</artifactId> <artifactId>lanterna</artifactId>
<version>3.1.5</version> <version>3.1.5</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -0,0 +1,88 @@
package ru.project.io;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import org.apache.commons.csv.CSVFormat;
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 {
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);
}
}
@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");
}
}
}
@@ -0,0 +1,88 @@
package ru.project.io;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import ru.project.collection.MyList;
import ru.project.student.Student;
public class StudentCsvWriter {
private static final String GROUP_NUMBER_HEADER = "groupNumber";
private static final String AVERAGE_GRADE_HEADER = "averageGrade";
private static final String RECORD_BOOK_NUMBER_HEADER = "recordBookNumber";
private final String absoluteFilePath;
public StudentCsvWriter(String absoluteFilePath) {
this.absoluteFilePath = absoluteFilePath;
}
public void write(Student student) throws IOException {
ensureFileExists();
boolean fileIsEmpty = isFileEmpty();
try (CSVPrinter printer = createPrinter()) {
if (fileIsEmpty) {
writeHeader(printer);
}
writeStudent(printer, student);
}
}
public void write(MyList<Student>[] students) throws IOException {
ensureFileExists();
boolean fileIsEmpty = isFileEmpty();
try (CSVPrinter printer = createPrinter()) {
if (fileIsEmpty) {
writeHeader(printer);
}
for (MyList<Student> studentList : students) {
for (int i = 0; i < studentList.size(); i++) {
writeStudent(printer, studentList.get(i));
}
}
}
}
private CSVPrinter createPrinter() throws IOException {
BufferedWriter writer =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(absoluteFilePath, true), StandardCharsets.UTF_8));
return new CSVPrinter(writer, CSVFormat.DEFAULT);
}
private void writeHeader(CSVPrinter printer) throws IOException {
printer.printRecord(GROUP_NUMBER_HEADER, AVERAGE_GRADE_HEADER, RECORD_BOOK_NUMBER_HEADER);
}
private void writeStudent(CSVPrinter printer, Student student) throws IOException {
printer.printRecord(
student.getGroupNumber(), student.getAverageGrade(), student.getRecordBookNumber());
}
private void ensureFileExists() throws IOException {
File file = new File(absoluteFilePath);
if (!file.isFile()) {
throw new IOException("CSV file does not exist: " + absoluteFilePath);
}
}
private boolean isFileEmpty() {
return new File(absoluteFilePath).length() == 0;
}
}
@@ -0,0 +1,172 @@
package ru.project.random;
import java.util.Random;
import java.util.regex.Pattern;
import ru.project.collection.MyLinkedList;
import ru.project.collection.MyList;
import ru.project.student.Student;
import ru.project.student.StudentBuilder;
public class RandomStudentGenerator {
private static final Random RANDOM = new Random();
private String minGroupNumber;
private String maxGroupNumber;
private Double minAverageGrade;
private Double maxAverageGrade;
private Integer minRecordBookNumber;
private Integer maxRecordBookNumber;
private static final double DEFAULT_MIN_GRADE = 0.0;
private static final double DEFAULT_MAX_GRADE = 5.0;
private static final int DEFAULT_MIN_RECORD_BOOK = 1;
private static final String GROUP_PATTERN_REGEX = "^[A-Z][0-9]{2}$";
private static final Pattern GROUP_PATTERN = Pattern.compile(GROUP_PATTERN_REGEX);
public RandomStudentGenerator setMinGroupNumber(String minGroupNumber) {
if (minGroupNumber != null && !GROUP_PATTERN.matcher(minGroupNumber).matches()) {
throw new IllegalArgumentException(
"minGroupNumber does not match Student group pattern: " + GROUP_PATTERN_REGEX);
}
this.minGroupNumber = minGroupNumber;
return this;
}
public RandomStudentGenerator setMaxGroupNumber(String maxGroupNumber) {
if (maxGroupNumber != null && !GROUP_PATTERN.matcher(maxGroupNumber).matches()) {
throw new IllegalArgumentException(
"maxGroupNumber does not match Student group pattern: " + GROUP_PATTERN_REGEX);
}
this.maxGroupNumber = maxGroupNumber;
return this;
}
public RandomStudentGenerator setMinAverageGrade(double minAverageGrade) {
if (minAverageGrade < DEFAULT_MIN_GRADE || minAverageGrade > DEFAULT_MAX_GRADE) {
throw new IllegalArgumentException(
"minAverageGrade out of Student allowed range ["
+ DEFAULT_MIN_GRADE
+ ", "
+ DEFAULT_MAX_GRADE
+ "]");
}
this.minAverageGrade = minAverageGrade;
return this;
}
public RandomStudentGenerator setMaxAverageGrade(double maxAverageGrade) {
if (maxAverageGrade < DEFAULT_MIN_GRADE || maxAverageGrade > DEFAULT_MAX_GRADE) {
throw new IllegalArgumentException(
"maxAverageGrade out of Student allowed range ["
+ DEFAULT_MIN_GRADE
+ ", "
+ DEFAULT_MAX_GRADE
+ "]");
}
this.maxAverageGrade = maxAverageGrade;
return this;
}
public RandomStudentGenerator setMinRecordBookNumber(int minRecordBookNumber) {
if (minRecordBookNumber < DEFAULT_MIN_RECORD_BOOK) {
throw new IllegalArgumentException(
"minRecordBookNumber must be >= " + DEFAULT_MIN_RECORD_BOOK);
}
this.minRecordBookNumber = minRecordBookNumber;
return this;
}
public RandomStudentGenerator setMaxRecordBookNumber(int maxRecordBookNumber) {
if (maxRecordBookNumber < DEFAULT_MIN_RECORD_BOOK) {
throw new IllegalArgumentException(
"maxRecordBookNumber must be >= " + DEFAULT_MIN_RECORD_BOOK);
}
this.maxRecordBookNumber = maxRecordBookNumber;
return this;
}
public MyList<Student> generate(int size) {
if (size < 0) {
throw new IllegalArgumentException("size must be non-negative");
}
validateRanges();
MyList<Student> result = new MyLinkedList<>();
for (int i = 0; i < size; i++) {
String group = generateGroupNumber();
double grade = generateAverageGrade();
int recordBook = generateRecordBookNumber();
Student student =
new StudentBuilder()
.setGroupNumber(group)
.setAverageGrade(grade)
.setRecordBookNumber(recordBook)
.build();
result.add(student);
}
return result;
}
private void validateRanges() {
double effectiveMinGrade = (minAverageGrade != null) ? minAverageGrade : DEFAULT_MIN_GRADE;
double effectiveMaxGrade = (maxAverageGrade != null) ? maxAverageGrade : DEFAULT_MAX_GRADE;
if (effectiveMinGrade > effectiveMaxGrade) {
throw new IllegalArgumentException("minAverageGrade cannot be greater than maxAverageGrade");
}
int effectiveMinRecord =
(minRecordBookNumber != null) ? minRecordBookNumber : DEFAULT_MIN_RECORD_BOOK;
int effectiveMaxRecord =
(maxRecordBookNumber != null) ? maxRecordBookNumber : Integer.MAX_VALUE;
if (effectiveMaxRecord == Integer.MAX_VALUE) {
effectiveMaxRecord = effectiveMinRecord + 999_999;
}
if (effectiveMinRecord > effectiveMaxRecord) {
throw new IllegalArgumentException(
"minRecordBookNumber cannot be greater than maxRecordBookNumber");
}
if (minGroupNumber != null && maxGroupNumber != null) {
if (minGroupNumber.compareTo(maxGroupNumber) > 0) {
throw new IllegalArgumentException("minGroupNumber cannot be greater than maxGroupNumber");
}
}
}
private String generateGroupNumber() {
String min = (minGroupNumber != null) ? minGroupNumber : "A00";
String max = (maxGroupNumber != null) ? maxGroupNumber : "Z99";
char minLetter = min.charAt(0);
char maxLetter = max.charAt(0);
int minDigit = Integer.parseInt(min.substring(1));
int maxDigit = Integer.parseInt(max.substring(1));
char letter = (char) (minLetter + RANDOM.nextInt((maxLetter - minLetter) + 1));
int digit;
if (letter == minLetter && letter == maxLetter) {
digit = minDigit + RANDOM.nextInt((maxDigit - minDigit) + 1);
} else if (letter == minLetter) {
digit = minDigit + RANDOM.nextInt(100 - minDigit);
} else if (letter == maxLetter) {
digit = RANDOM.nextInt(maxDigit + 1);
} else {
digit = RANDOM.nextInt(100);
}
return letter + String.format("%02d", digit);
}
private double generateAverageGrade() {
double min = (minAverageGrade != null) ? minAverageGrade : DEFAULT_MIN_GRADE;
double max = (maxAverageGrade != null) ? maxAverageGrade : DEFAULT_MAX_GRADE;
double range = max - min;
return min + (range * RANDOM.nextDouble());
}
private int generateRecordBookNumber() {
int min = (minRecordBookNumber != null) ? minRecordBookNumber : DEFAULT_MIN_RECORD_BOOK;
int max =
(maxRecordBookNumber != null) ? maxRecordBookNumber : min + 999_999; // разумный максимум
if (max < min) {
max = min;
}
return min + RANDOM.nextInt((max - min) + 1);
}
}
@@ -1,138 +1,137 @@
package ru.project.finder; package ru.project.finder;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import ru.project.collection.MyLinkedList; import ru.project.collection.MyLinkedList;
import ru.project.collection.MyList; import ru.project.collection.MyList;
import ru.project.student.Student; import ru.project.student.Student;
import static org.junit.jupiter.api.Assertions.*; public class StudentOccurrenceIndexFinderTest {
private StudentOccurrenceIndexFinder finder;
private MyList<Student> students;
public class StudentOccurrenceIndexFinderTest @BeforeEach
{ void setUp() {
private StudentOccurrenceIndexFinder finder; finder = new StudentOccurrenceIndexFinder();
private MyList<Student> students; students = new MyLinkedList<>();
}
@BeforeEach @Test
void setUp() { void shouldTesfindOccurrences_emptyList_returnsEmptyArray() {
finder = new StudentOccurrenceIndexFinder(); int[] result = finder.findOccurrences(students, new Student("A12", 4.5, 1));
students = new MyLinkedList<>(); assertEquals(0, result.length);
} }
@Test @Test
void shouldTesfindOccurrences_emptyList_returnsEmptyArray() { void shouldTesfindOccurrences_noMatches_returnsEmptyArray() {
int[] result = finder.findOccurrences(students, new Student("A12", 4.5, 1)); students.add(new Student("A12", 4.0, 101));
assertEquals(0, result.length); students.add(new Student("B23", 3.5, 102));
} Student target = new Student("C34", 4.8, 103); // такого нет
@Test
void shouldTesfindOccurrences_noMatches_returnsEmptyArray() {
students.add(new Student("A12", 4.0, 101));
students.add(new Student("B23", 3.5, 102));
Student target = new Student("C34", 4.8, 103); // такого нет
int[] result = finder.findOccurrences(students, target); int[] result = finder.findOccurrences(students, target);
assertEquals(0, result.length); assertEquals(0, result.length);
} }
@Test
void shouldTesfindOccurrences_singleMatch_returnsCorrectIndex() {
students.add(new Student("A12", 4.0, 101));
students.add(new Student("B23", 3.5, 102)); // target здесь
students.add(new Student("C34", 4.8, 103));
Student target = new Student("B23", 3.5, 102); @Test
int[] result = finder.findOccurrences(students, target); void shouldTesfindOccurrences_singleMatch_returnsCorrectIndex() {
students.add(new Student("A12", 4.0, 101));
students.add(new Student("B23", 3.5, 102)); // target здесь
students.add(new Student("C34", 4.8, 103));
assertEquals(1, result.length); Student target = new Student("B23", 3.5, 102);
assertEquals(1, result[0]); int[] result = finder.findOccurrences(students, target);
}
@Test
void shouldTesfindOccurrences_multipleMatches_returnsAllIndicesSorted() {
// индексы: 0, 2, 4 — должны вернуться в порядке возрастания
students.add(new Student("A12", 4.5, 101)); // match
students.add(new Student("B23", 3.8, 102));
students.add(new Student("A12", 4.5, 101)); // match
students.add(new Student("C34", 4.0, 103));
students.add(new Student("A12", 4.5, 101)); // match
Student target = new Student("A12", 4.5, 101); assertEquals(1, result.length);
int[] result = finder.findOccurrences(students, target); assertEquals(1, result[0]);
}
assertEquals(3, result.length); @Test
assertArrayEquals(new int[]{0, 2, 4}, result); void shouldTesfindOccurrences_multipleMatches_returnsAllIndicesSorted() {
} // индексы: 0, 2, 4 — должны вернуться в порядке возрастания
students.add(new Student("A12", 4.5, 101)); // match
students.add(new Student("B23", 3.8, 102));
students.add(new Student("A12", 4.5, 101)); // match
students.add(new Student("C34", 4.0, 103));
students.add(new Student("A12", 4.5, 101)); // match
@Test Student target = new Student("A12", 4.5, 101);
void shouldTesfindOccurrences_allElementsMatch_returnsAllIndices() { int[] result = finder.findOccurrences(students, target);
Student s = new Student("X99", 5.0, 999);
students.add(s);
students.add(s);
students.add(s);
int[] result = finder.findOccurrences(students, s); assertEquals(3, result.length);
assertEquals(3, result.length); assertArrayEquals(new int[] {0, 2, 4}, result);
assertArrayEquals(new int[]{0, 1, 2}, result); }
}
@Test @Test
void shouldTesfindOccurrences_differentAverageGrade_noMatch() { void shouldTesfindOccurrences_allElementsMatch_returnsAllIndices() {
students.add(new Student("A12", 4.50, 101)); Student s = new Student("X99", 5.0, 999);
Student target = new Student("A12", 4.51, 101); // отличается балл students.add(s);
students.add(s);
students.add(s);
int[] result = finder.findOccurrences(students, target); int[] result = finder.findOccurrences(students, s);
assertEquals(0, result.length); assertEquals(3, result.length);
} assertArrayEquals(new int[] {0, 1, 2}, result);
}
@Test
void shouldTesfindOccurrences_differentAverageGrade_noMatch() {
students.add(new Student("A12", 4.50, 101));
Student target = new Student("A12", 4.51, 101); // отличается балл
int[] result = finder.findOccurrences(students, target);
assertEquals(0, result.length);
}
@Test
void shouldTesfindOccurrences_differentGroupNumber_noMatch() {
students.add(new Student("A12", 4.5, 101));
Student target = new Student("B12", 4.5, 101); // другая группа
int[] result = finder.findOccurrences(students, target);
assertEquals(0, result.length);
}
@Test
void shouldTesfindOccurrences_differentRecordBookNumber_noMatch() {
students.add(new Student("A12", 4.5, 101));
Student target = new Student("A12", 4.5, 102); // другой номер зачетки
int[] result = finder.findOccurrences(students, target);
assertEquals(0, result.length);
}
@Test
void shouldTesfindOccurrences_nullList_throwsException() {
Student target = new Student("A12", 4.5, 101);
assertThrows(IllegalArgumentException.class, () -> finder.findOccurrences(null, target));
}
@Test
void shouldTesfindOccurrences_nullTarget_throwsException() {
assertThrows(IllegalArgumentException.class, () -> finder.findOccurrences(students, null));
}
@Test
void shouldTesfindOccurrences_resultIsSorted() {
String[] validGroups = {"A00", "A01", "B12", "C99", "X55"};
for (int i = 0; i < 50; i++) {
String group = validGroups[i % validGroups.length];
double grade = (3.0 + (i % 3)) / 2.0;
if (i % 3 == 0) {
@Test
void shouldTesfindOccurrences_differentGroupNumber_noMatch() {
students.add(new Student("A12", 4.5, 101)); students.add(new Student("A12", 4.5, 101));
Student target = new Student("B12", 4.5, 101); // другая группа } else {
students.add(new Student(group, grade, 200 + i));
int[] result = finder.findOccurrences(students, target); }
assertEquals(0, result.length);
} }
Student target = new Student("A12", 4.5, 101);
int[] result = finder.findOccurrences(students, target);
@Test for (int i = 1; i < result.length; i++) {
void shouldTesfindOccurrences_differentRecordBookNumber_noMatch() { assertTrue(result[i] > result[i - 1], "Результат должен быть отсортирован");
students.add(new Student("A12", 4.5, 101));
Student target = new Student("A12", 4.5, 102); // другой номер зачетки
int[] result = finder.findOccurrences(students, target);
assertEquals(0, result.length);
}
@Test
void shouldTesfindOccurrences_nullList_throwsException() {
Student target = new Student("A12", 4.5, 101);
assertThrows(IllegalArgumentException.class, () -> finder.findOccurrences(null, target));
}
@Test
void shouldTesfindOccurrences_nullTarget_throwsException() {
assertThrows(IllegalArgumentException.class, () -> finder.findOccurrences(students, null));
}
@Test
void shouldTesfindOccurrences_resultIsSorted() {
String[] validGroups = {"A00", "A01", "B12", "C99", "X55"};
for (int i = 0; i < 50; i++) {
String group = validGroups[i % validGroups.length];
double grade = (3.0 + (i % 3)) / 2.0;
if (i % 3 == 0) {
students.add(new Student("A12", 4.5, 101));
} else {
students.add(new Student(group, grade, 200 + i));
}
}
Student target = new Student("A12", 4.5, 101);
int[] result = finder.findOccurrences(students, target);
for (int i = 1; i < result.length; i++) {
assertTrue(result[i] > result[i - 1], "Результат должен быть отсортирован");
}
} }
}
} }
@@ -0,0 +1,118 @@
package ru.project.io;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.NoSuchElementException;
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.*;
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());
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
reader::next
);
assertTrue(ex.getMessage().contains("Expected 3 columns"));
}
}
@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 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"));
}
}
@@ -0,0 +1,194 @@
package ru.project.io;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import ru.project.collection.MyLinkedList;
import ru.project.collection.MyList;
import ru.project.student.Student;
class StudentCsvWriterTest {
@TempDir Path tempDir;
@Test
void writeShouldWriteHeaderAndStudentToEmptyFile() throws Exception {
Path file = tempDir.resolve("students.csv");
Files.createFile(file);
Student student = new Student("A12", 4.5, 10001);
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
writer.write(student);
List<String> lines = Files.readAllLines(file);
assertEquals(List.of("groupNumber,averageGrade,recordBookNumber", "A12,4.5,10001"), lines);
}
@Test
void writeShouldAppendStudentToExistingFile() throws Exception {
Path file = tempDir.resolve("students.csv");
Files.write(file, List.of("groupNumber,averageGrade,recordBookNumber", "A12,4.5,10001"));
Student student = new Student("B07", 3.8, 10002);
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
writer.write(student);
List<String> lines = Files.readAllLines(file);
assertEquals(
List.of("groupNumber,averageGrade,recordBookNumber", "A12,4.5,10001", "B07,3.8,10002"),
lines);
}
@Test
void writeShouldNotCreateMissingFile() {
Path file = tempDir.resolve("students.csv");
Student student = new Student("A12", 4.5, 10001);
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
assertThrows(IOException.class, () -> writer.write(student));
assertFalse(Files.exists(file));
}
@Test
void writeShouldAppendSeveralStudentsInOrder() throws Exception {
Path file = tempDir.resolve("students.csv");
Files.createFile(file);
Student student1 = new Student("A12", 4.5, 10001);
Student student2 = new Student("B07", 3.8, 10002);
Student student3 = new Student("C25", 4.9, 10003);
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
writer.write(student1);
writer.write(student2);
writer.write(student3);
List<String> lines = Files.readAllLines(file);
assertEquals(
List.of(
"groupNumber,averageGrade,recordBookNumber",
"A12,4.5,10001",
"B07,3.8,10002",
"C25,4.9,10003"),
lines);
}
@Test
void writeArrayShouldAppendStudentsFromAllListsInOrder() throws Exception {
Path file = tempDir.resolve("students.csv");
Files.createFile(file);
Student student1 = new Student("A12", 4.5, 10001);
Student student2 = new Student("B07", 3.8, 10002);
Student student3 = new Student("C25", 4.9, 10003);
Student student4 = new Student("D15", 4.2, 10004);
Student student5 = new Student("E20", 3.6, 10005);
MyList<Student> firstList = new MyLinkedList<>();
firstList.add(student1);
firstList.add(student2);
MyList<Student> secondList = new MyLinkedList<>();
secondList.add(student3);
MyList<Student> thirdList = new MyLinkedList<>();
thirdList.add(student4);
thirdList.add(student5);
@SuppressWarnings("unchecked")
MyList<Student>[] students = new MyList[] {firstList, secondList, thirdList};
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
writer.write(students);
List<String> lines = Files.readAllLines(file);
assertEquals(
List.of(
"groupNumber,averageGrade,recordBookNumber",
"A12,4.5,10001",
"B07,3.8,10002",
"C25,4.9,10003",
"D15,4.2,10004",
"E20,3.6,10005"),
lines);
}
@Test
void writeArrayShouldSkipEmptyLists() throws Exception {
Path file = tempDir.resolve("students.csv");
Files.createFile(file);
Student student = new Student("A12", 4.5, 10001);
MyList<Student> emptyList = new MyLinkedList<>();
MyList<Student> nonEmptyList = new MyLinkedList<>();
nonEmptyList.add(student);
@SuppressWarnings("unchecked")
MyList<Student>[] students = new MyList[] {emptyList, nonEmptyList, new MyLinkedList<>()};
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
writer.write(students);
List<String> lines = Files.readAllLines(file);
assertEquals(List.of("groupNumber,averageGrade,recordBookNumber", "A12,4.5,10001"), lines);
}
@Test
void writeEmptyArrayShouldNotModifyFile() throws Exception {
Path file = tempDir.resolve("students.csv");
List<String> initialContent =
List.of("groupNumber,averageGrade,recordBookNumber", "A12,4.5,10001");
Files.write(file, initialContent);
@SuppressWarnings("unchecked")
MyList<Student>[] students = new MyList[0];
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
writer.write(students);
assertEquals(initialContent, Files.readAllLines(file));
}
@Test
void writeEmptyArrayShouldNotCreateMissingFile() {
Path file = tempDir.resolve("students.csv");
@SuppressWarnings("unchecked")
MyList<Student>[] students = new MyList[0];
StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
assertThrows(IOException.class, () -> writer.write(students));
assertFalse(Files.exists(file));
}
}
@@ -0,0 +1,163 @@
package ru.project.random;
import static org.junit.jupiter.api.Assertions.*;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import ru.project.collection.MyList;
import ru.project.student.Student;
public class RandomStudentGeneratorTest {
private static final Pattern GROUP_PATTERN = Pattern.compile("^[A-Z][0-9]{2}$");
private static final double MIN_GRADE = 0.0;
private static final double MAX_GRADE = 5.0;
@Test
void shouldTestgenerate_withoutConstraints_returnsCorrectSizeAndValidStudents() {
int size = 100;
RandomStudentGenerator generator = new RandomStudentGenerator();
MyList<Student> students = generator.generate(size);
assertEquals(size, students.size(), "Размер списка должен совпадать с запрошенным");
for (int i = 0; i < students.size(); i++) {
Student s = students.get(i);
assertTrue(
GROUP_PATTERN.matcher(s.getGroupNumber()).matches(),
"groupNumber должен соответствовать шаблону A00–Z99");
assertTrue(
s.getAverageGrade() >= MIN_GRADE && s.getAverageGrade() <= MAX_GRADE,
"averageGrade должен быть в диапазоне [0.0, 5.0]");
assertTrue(s.getRecordBookNumber() >= 1, "recordBookNumber должен быть >= 1");
}
}
@Test
void shouldTestgenerate_withGroupRange_respectsLexicographicalBounds() {
RandomStudentGenerator generator =
new RandomStudentGenerator().setMinGroupNumber("A00").setMaxGroupNumber("B99");
MyList<Student> students = generator.generate(200);
assertEquals(200, students.size());
for (int i = 0; i < students.size(); i++) {
Student s = students.get(i);
String g = s.getGroupNumber();
assertTrue(g.compareTo("A00") >= 0, "Группа не должна быть меньше A00");
assertTrue(g.compareTo("B99") <= 0, "Группа не должна быть больше B99");
assertTrue(GROUP_PATTERN.matcher(g).matches(), "Группа должна соответствовать шаблону");
}
}
@Test
void shouldTestgenerate_withGradeRange_respectsBounds() {
double minGrade = 3.0;
double maxGrade = 4.0;
RandomStudentGenerator generator =
new RandomStudentGenerator().setMinAverageGrade(minGrade).setMaxAverageGrade(maxGrade);
MyList<Student> students = generator.generate(100);
for (int i = 0; i < students.size(); i++) {
Student s = students.get(i);
double g = s.getAverageGrade();
assertTrue(g >= minGrade && g <= maxGrade, "averageGrade должен быть в заданном диапазоне");
}
}
@Test
void shouldTestgenerate_withRecordBookRange_respectsBounds() {
int minRec = 10_000;
int maxRec = 20_000;
RandomStudentGenerator generator =
new RandomStudentGenerator().setMinRecordBookNumber(minRec).setMaxRecordBookNumber(maxRec);
MyList<Student> students = generator.generate(100);
for (int i = 0; i < students.size(); i++) {
Student s = students.get(i);
int r = s.getRecordBookNumber();
assertTrue(r >= minRec && r <= maxRec, "recordBookNumber должен быть в заданном диапазоне");
}
}
@Test
void shouldTestgenerate_negativeSize_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(IllegalArgumentException.class, () -> generator.generate(-1));
}
@Test
void shouldTestsetMinGroupNumber_invalidPattern_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(IllegalArgumentException.class, () -> generator.setMinGroupNumber("invalid"));
}
@Test
void shouldTestsetMaxGroupNumber_invalidPattern_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(
IllegalArgumentException.class, () -> generator.setMaxGroupNumber("ZZ")); // не 2 цифры
}
@Test
void shouldTestsetMinMaxGroupNumber_minGreaterThanMax_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(
IllegalArgumentException.class,
() -> generator.setMinGroupNumber("C00").setMaxGroupNumber("A99").generate(1));
}
@Test
void shouldTestsetMinAverageGrade_outOfRange_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(IllegalArgumentException.class, () -> generator.setMinAverageGrade(-0.1));
assertThrows(IllegalArgumentException.class, () -> generator.setMinAverageGrade(5.1));
}
@Test
void shouldTestsetMaxAverageGrade_outOfRange_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(IllegalArgumentException.class, () -> generator.setMaxAverageGrade(-0.1));
assertThrows(IllegalArgumentException.class, () -> generator.setMaxAverageGrade(5.1));
}
@Test
void shouldTestsetMinMaxAverageGrade_minGreaterThanMax_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(
IllegalArgumentException.class,
() -> generator.setMinAverageGrade(4.0).setMaxAverageGrade(3.0).generate(1));
}
@Test
void shouldTestsetMinRecordBookNumber_lessThanAllowed_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(IllegalArgumentException.class, () -> generator.setMinRecordBookNumber(0));
}
@Test
void shouldTestsetMinMaxRecordBookNumber_minGreaterThanMax_throwsException() {
RandomStudentGenerator generator = new RandomStudentGenerator();
assertThrows(
IllegalArgumentException.class,
() -> generator.setMinRecordBookNumber(20_000).setMaxRecordBookNumber(10_000).generate(1));
}
@Test
void shouldTestchainedBuilderStyle_worksAsExpected() {
RandomStudentGenerator generator =
new RandomStudentGenerator()
.setMinGroupNumber("A00")
.setMaxGroupNumber("Z99")
.setMinAverageGrade(2.0)
.setMaxAverageGrade(4.0)
.setMinRecordBookNumber(1_000)
.setMaxRecordBookNumber(9_999);
MyList<Student> students = generator.generate(50);
assertEquals(50, students.size());
for (int i = 0; i < students.size(); i++) {
Student s = students.get(i);
assertTrue(
s.getGroupNumber().compareTo("A00") >= 0 && s.getGroupNumber().compareTo("Z99") <= 0);
assertTrue(s.getAverageGrade() >= 2.0 && s.getAverageGrade() <= 4.0);
assertTrue(s.getRecordBookNumber() >= 1_000 && s.getRecordBookNumber() <= 9_999);
}
}
}
@@ -78,8 +78,9 @@ class SortStrategyTest {
list.add(new Student("A05", 4.5, 5)); list.add(new Student("A05", 4.5, 5));
list.add(new Student("A06", 4.5, 4)); list.add(new Student("A06", 4.5, 4));
EvenOnlyMergeSortStrategy<Student> strategy = EvenOnlySortStrategy<Student> strategy =
new EvenOnlyMergeSortStrategy<>(Student::getRecordBookNumber); new EvenOnlySortStrategy<>(new MergeSortStrategy<Student>(), Student::getRecordBookNumber);
strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber)); strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber));
int[] expected = {7, 2, 3, 4, 5, 8}; int[] expected = {7, 2, 3, 4, 5, 8};
@@ -95,8 +96,8 @@ class SortStrategyTest {
list.add(new Student("A02", 4.5, 3)); list.add(new Student("A02", 4.5, 3));
list.add(new Student("A03", 4.5, 5)); list.add(new Student("A03", 4.5, 5));
EvenOnlyMergeSortStrategy<Student> strategy = EvenOnlySortStrategy<Student> strategy =
new EvenOnlyMergeSortStrategy<>(Student::getRecordBookNumber); new EvenOnlySortStrategy<>(new MergeSortStrategy<Student>(), Student::getRecordBookNumber);
strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber)); strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber));
int[] expected = {1, 3, 5}; int[] expected = {1, 3, 5};
@@ -112,8 +113,8 @@ class SortStrategyTest {
list.add(new Student("A02", 4.5, 2)); list.add(new Student("A02", 4.5, 2));
list.add(new Student("A03", 4.5, 6)); list.add(new Student("A03", 4.5, 6));
EvenOnlyMergeSortStrategy<Student> strategy = EvenOnlySortStrategy<Student> strategy =
new EvenOnlyMergeSortStrategy<>(Student::getRecordBookNumber); new EvenOnlySortStrategy<>(new MergeSortStrategy<Student>(), Student::getRecordBookNumber);
strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber)); strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber));
int[] expected = {2, 4, 6}; int[] expected = {2, 4, 6};
+143 -26
View File
@@ -42,13 +42,13 @@ C25,4.9,10003
--- ---
# `StudentCsvWriter` ## `StudentCsvWriter`
## Назначение ### Назначение
Дозаписывать одного студента в указанный CSV-файл. Дозаписывать одного студента в **существующий** CSV-файл.
## Контракт ### Контракт
Конструктор: Конструктор:
@@ -65,41 +65,158 @@ public void write(Student student) throws IOException
### Требования ### Требования
1. Метод принимает одного `Student`. 1. Метод принимает одного `Student`.
2. Запись выполняется в режиме добавления. 2. Указанный файл должен уже существовать.
3. Существующие записи не изменяются. 3. Запись выполняется в режиме добавления.
4. Если файл существует, заголовок повторно не записывается. 4. Существующие записи не должны изменяться.
5. Если файл не существует, необходимо создать файл. 5. Заголовок CSV не должен записываться повторно.
6. При создании нового файла имя должно соответствовать формату: 6. Если файл не существует, новый файл **не создавать**.
7. Если файл не существует, метод должен выбросить `IOException`.
8. При успешной записи метод завершается без исключения.
9. Метод не возвращает значение.
10. Исключения, связанные с записью файла, не обрабатывать внутри метода.
11. Исключения `Student` и `StudentBuilder` также не обрабатывать.
## Невозможность записи
Если указанный файл не существует, метод должен выбросить `IOException`.
Также `IOException` должен возникать и передаваться вызывающему коду, если:
- файл недоступен для записи;
- отсутствуют права на запись;
- произошла ошибка файловой системы;
- запись в файл невозможна по другой причине.
Создание файла с именем вида:
```text ```text
students-yyyy-MM-dd-HH-mm.csv students-yyyy-MM-dd-HH-mm.csv
``` ```
Например: `StudentCsvWriter` **не выполняет**.
```text ## Пример теста: несуществующий файл
students-2026-08-28-20-45.csv
Передать путь к несуществующему файлу:
```java
writer.write(student);
``` ```
1. Новый файл создаётся в директории, указанной абсолютным путём. Проверить:
2. При создании нового файла сначала записывается заголовок.
3. При успешной записи метод завершается без исключения.
4. Метод не возвращает значение.
5. Исключения, связанные с записью файла, не обрабатывать внутри метода — они должны передаваться вызывающему коду.
6. Исключения `Student` и `StudentBuilder` также не обрабатывать.
## Невозможность записи ```text
IOException
```
Если студент не может быть записан, например: Проверить также, что новый файл не был создан.
- отсутствуют права на запись; ## Изменение теста существующего файла
- невозможно создать файл;
- файл недоступен;
- произошла ошибка файловой системы;
метод должен выбросить соответствующее исключение, например `IOException`. Для существующего файла:
Запись не должна возвращать специальное значение вроде `0` для обозначения ошибки. ```text
groupNumber,averageGrade,recordBookNumber
A12,4.5,10001
```
выполнить:
```java
writer.write(student2);
```
Проверить, что результат:
```text
groupNumber,averageGrade,recordBookNumber
A12,4.5,10001
B07,3.8,10002
```
Заголовок присутствует только один раз.
## Дополнительный метод `StudentCsvWriter`
Кроме метода записи одного студента:
```java
public void write(Student student) throws IOException;
```
необходимо реализовать метод:
```java
public void write(MyList<Student>[] students) throws IOException;
```
### Контракт
Метод должен:
1. Принимать массив `MyList<Student>`.
2. Последовательно обрабатывать все переданные коллекции.
3. Записывать каждого студента в указанный CSV-файл.
4. Сохранять порядок:
- коллекций в массиве;
- студентов внутри каждой `MyList`.
5. Использовать тот же формат CSV и те же правила записи, что и `write(Student)`.
6. Работать только с уже существующим файлом.
7. Не создавать файл, если он отсутствует.
8. При отсутствии файла выбрасывать `IOException`.
9. Не изменять переданные `MyList<Student>`.
10. Не обрабатывать исключения, возникающие при создании или валидации `Student`.
11. При ошибке записи прекращать операцию и передавать исключение вызывающему коду.
### Пример
```java
MyList<Student>[] studentLists = ...;
writer.write(studentLists);
```
Если:
```text
studentLists[0] = [Student A, Student B]
studentLists[1] = [Student C]
studentLists[2] = [Student D, Student E]
```
в файл должны последовательно добавиться:
```text
Student A
Student B
Student C
Student D
Student E
```
### Проверка пустых коллекций
Пустые `MyList<Student>` внутри массива не должны приводить к ошибке и не должны добавлять записей в файл.
### Проверка пустого массива
Для:
```java
MyList<Student>[] studentLists = ...; // размер 0
```
метод не должен добавлять никаких записей и не должен изменять файл.
### Проверка отсутствующего файла
Для несуществующего файла:
```java
writer.write(studentLists);
```
должен выбрасываться `IOException`. Файл создавать запрещено.
--- ---