diff --git a/app/pom.xml b/app/pom.xml
index 5afca24..d5108b2 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -42,12 +42,24 @@
test
+
+ org.apache.commons
+ commons-csv
+ 1.14.1
+
+
com.googlecode.lanterna
lanterna
3.1.5
+
+ org.apache.commons
+ commons-csv
+ 1.14.1
+
+
diff --git a/app/src/main/java/ru/project/io/StudentCsvReader.java b/app/src/main/java/ru/project/io/StudentCsvReader.java
new file mode 100644
index 0000000..db2a4a9
--- /dev/null
+++ b/app/src/main/java/ru/project/io/StudentCsvReader.java
@@ -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, 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 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 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");
+ }
+ }
+ }
diff --git a/app/src/main/java/ru/project/io/StudentCsvWriter.java b/app/src/main/java/ru/project/io/StudentCsvWriter.java
new file mode 100644
index 0000000..8795fbc
--- /dev/null
+++ b/app/src/main/java/ru/project/io/StudentCsvWriter.java
@@ -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[] students) throws IOException {
+ ensureFileExists();
+
+ boolean fileIsEmpty = isFileEmpty();
+
+ try (CSVPrinter printer = createPrinter()) {
+ if (fileIsEmpty) {
+ writeHeader(printer);
+ }
+
+ for (MyList 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;
+ }
+}
diff --git a/app/src/main/java/ru/project/random/RandomStudentGenerator.java b/app/src/main/java/ru/project/random/RandomStudentGenerator.java
new file mode 100644
index 0000000..526156a
--- /dev/null
+++ b/app/src/main/java/ru/project/random/RandomStudentGenerator.java
@@ -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 generate(int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("size must be non-negative");
+ }
+ validateRanges();
+ MyList 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);
+ }
+}
diff --git a/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java b/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java
index b928992..3e5c710 100644
--- a/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java
+++ b/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java
@@ -1,138 +1,137 @@
package ru.project.finder;
+import static org.junit.jupiter.api.Assertions.*;
+
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-
import ru.project.collection.MyLinkedList;
import ru.project.collection.MyList;
import ru.project.student.Student;
-import static org.junit.jupiter.api.Assertions.*;
+public class StudentOccurrenceIndexFinderTest {
+ private StudentOccurrenceIndexFinder finder;
+ private MyList students;
-public class StudentOccurrenceIndexFinderTest
-{
- private StudentOccurrenceIndexFinder finder;
- private MyList students;
+ @BeforeEach
+ void setUp() {
+ finder = new StudentOccurrenceIndexFinder();
+ students = new MyLinkedList<>();
+ }
- @BeforeEach
- void setUp() {
- finder = new StudentOccurrenceIndexFinder();
- students = new MyLinkedList<>();
- }
+ @Test
+ void shouldTesfindOccurrences_emptyList_returnsEmptyArray() {
+ int[] result = finder.findOccurrences(students, new Student("A12", 4.5, 1));
+ assertEquals(0, result.length);
+ }
- @Test
- void shouldTesfindOccurrences_emptyList_returnsEmptyArray() {
- int[] result = finder.findOccurrences(students, new Student("A12", 4.5, 1));
- assertEquals(0, result.length);
- }
- @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); // такого нет
+ @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);
- 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));
+ int[] result = finder.findOccurrences(students, target);
+ assertEquals(0, result.length);
+ }
- Student target = new Student("B23", 3.5, 102);
- int[] result = finder.findOccurrences(students, target);
+ @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));
- assertEquals(1, result.length);
- assertEquals(1, result[0]);
- }
- @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("B23", 3.5, 102);
+ int[] result = finder.findOccurrences(students, target);
- Student target = new Student("A12", 4.5, 101);
- int[] result = finder.findOccurrences(students, target);
+ assertEquals(1, result.length);
+ assertEquals(1, result[0]);
+ }
- assertEquals(3, result.length);
- assertArrayEquals(new int[]{0, 2, 4}, result);
- }
+ @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
- @Test
- void shouldTesfindOccurrences_allElementsMatch_returnsAllIndices() {
- Student s = new Student("X99", 5.0, 999);
- students.add(s);
- students.add(s);
- students.add(s);
+ Student target = new Student("A12", 4.5, 101);
+ int[] result = finder.findOccurrences(students, target);
- int[] result = finder.findOccurrences(students, s);
- assertEquals(3, result.length);
- assertArrayEquals(new int[]{0, 1, 2}, result);
- }
+ assertEquals(3, result.length);
+ assertArrayEquals(new int[] {0, 2, 4}, result);
+ }
- @Test
- void shouldTesfindOccurrences_differentAverageGrade_noMatch() {
- students.add(new Student("A12", 4.50, 101));
- Student target = new Student("A12", 4.51, 101); // отличается балл
+ @Test
+ void shouldTesfindOccurrences_allElementsMatch_returnsAllIndices() {
+ Student s = new Student("X99", 5.0, 999);
+ students.add(s);
+ students.add(s);
+ students.add(s);
- int[] result = finder.findOccurrences(students, target);
- assertEquals(0, result.length);
- }
+ int[] result = finder.findOccurrences(students, s);
+ 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));
- Student target = new Student("B12", 4.5, 101); // другая группа
-
- int[] result = finder.findOccurrences(students, target);
- assertEquals(0, result.length);
+ } else {
+ students.add(new Student(group, grade, 200 + i));
+ }
}
+ Student target = new Student("A12", 4.5, 101);
+ int[] result = finder.findOccurrences(students, target);
- @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) {
-
- 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], "Результат должен быть отсортирован");
- }
+ for (int i = 1; i < result.length; i++) {
+ assertTrue(result[i] > result[i - 1], "Результат должен быть отсортирован");
}
+ }
}
diff --git a/app/src/test/java/ru/project/io/StudentCsvReaderTest.java b/app/src/test/java/ru/project/io/StudentCsvReaderTest.java
new file mode 100644
index 0000000..3541f35
--- /dev/null
+++ b/app/src/test/java/ru/project/io/StudentCsvReaderTest.java
@@ -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 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"));
+ }
+ }
\ No newline at end of file
diff --git a/app/src/test/java/ru/project/io/StudentCsvWriterTest.java b/app/src/test/java/ru/project/io/StudentCsvWriterTest.java
new file mode 100644
index 0000000..d5da367
--- /dev/null
+++ b/app/src/test/java/ru/project/io/StudentCsvWriterTest.java
@@ -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 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 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 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 firstList = new MyLinkedList<>();
+ firstList.add(student1);
+ firstList.add(student2);
+
+ MyList secondList = new MyLinkedList<>();
+ secondList.add(student3);
+
+ MyList thirdList = new MyLinkedList<>();
+ thirdList.add(student4);
+ thirdList.add(student5);
+
+ @SuppressWarnings("unchecked")
+ MyList[] students = new MyList[] {firstList, secondList, thirdList};
+
+ StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
+
+ writer.write(students);
+
+ List 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 emptyList = new MyLinkedList<>();
+
+ MyList nonEmptyList = new MyLinkedList<>();
+ nonEmptyList.add(student);
+
+ @SuppressWarnings("unchecked")
+ MyList[] students = new MyList[] {emptyList, nonEmptyList, new MyLinkedList<>()};
+
+ StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
+
+ writer.write(students);
+
+ List 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 initialContent =
+ List.of("groupNumber,averageGrade,recordBookNumber", "A12,4.5,10001");
+
+ Files.write(file, initialContent);
+
+ @SuppressWarnings("unchecked")
+ MyList[] 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[] students = new MyList[0];
+
+ StudentCsvWriter writer = new StudentCsvWriter(file.toAbsolutePath().toString());
+
+ assertThrows(IOException.class, () -> writer.write(students));
+
+ assertFalse(Files.exists(file));
+ }
+}
diff --git a/app/src/test/java/ru/project/random/RandomStudentGeneratorTest.java b/app/src/test/java/ru/project/random/RandomStudentGeneratorTest.java
new file mode 100644
index 0000000..b91471a
--- /dev/null
+++ b/app/src/test/java/ru/project/random/RandomStudentGeneratorTest.java
@@ -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 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 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 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 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 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);
+ }
+ }
+}
diff --git a/app/src/test/java/ru/project/sorting/SortStrategyTest.java b/app/src/test/java/ru/project/sorting/SortStrategyTest.java
index 7a6724c..36db74c 100644
--- a/app/src/test/java/ru/project/sorting/SortStrategyTest.java
+++ b/app/src/test/java/ru/project/sorting/SortStrategyTest.java
@@ -78,8 +78,9 @@ class SortStrategyTest {
list.add(new Student("A05", 4.5, 5));
list.add(new Student("A06", 4.5, 4));
- EvenOnlyMergeSortStrategy strategy =
- new EvenOnlyMergeSortStrategy<>(Student::getRecordBookNumber);
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy(), Student::getRecordBookNumber);
+
strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber));
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("A03", 4.5, 5));
- EvenOnlyMergeSortStrategy strategy =
- new EvenOnlyMergeSortStrategy<>(Student::getRecordBookNumber);
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy(), Student::getRecordBookNumber);
strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber));
int[] expected = {1, 3, 5};
@@ -112,8 +113,8 @@ class SortStrategyTest {
list.add(new Student("A02", 4.5, 2));
list.add(new Student("A03", 4.5, 6));
- EvenOnlyMergeSortStrategy strategy =
- new EvenOnlyMergeSortStrategy<>(Student::getRecordBookNumber);
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy(), Student::getRecordBookNumber);
strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber));
int[] expected = {2, 4, 6};
diff --git a/docs/tasks/CsvStudentIO.md b/docs/tasks/CsvStudentIO.md
index 89e2ad5..76ea466 100644
--- a/docs/tasks/CsvStudentIO.md
+++ b/docs/tasks/CsvStudentIO.md
@@ -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`.
-2. Запись выполняется в режиме добавления.
-3. Существующие записи не изменяются.
-4. Если файл существует, заголовок повторно не записывается.
-5. Если файл не существует, необходимо создать файл.
-6. При создании нового файла имя должно соответствовать формату:
+2. Указанный файл должен уже существовать.
+3. Запись выполняется в режиме добавления.
+4. Существующие записи не должны изменяться.
+5. Заголовок CSV не должен записываться повторно.
+6. Если файл не существует, новый файл **не создавать**.
+7. Если файл не существует, метод должен выбросить `IOException`.
+8. При успешной записи метод завершается без исключения.
+9. Метод не возвращает значение.
+10. Исключения, связанные с записью файла, не обрабатывать внутри метода.
+11. Исключения `Student` и `StudentBuilder` также не обрабатывать.
+
+## Невозможность записи
+
+Если указанный файл не существует, метод должен выбросить `IOException`.
+
+Также `IOException` должен возникать и передаваться вызывающему коду, если:
+
+- файл недоступен для записи;
+- отсутствуют права на запись;
+- произошла ошибка файловой системы;
+- запись в файл невозможна по другой причине.
+
+Создание файла с именем вида:
```text
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[] students) throws IOException;
+```
+
+### Контракт
+
+Метод должен:
+
+1. Принимать массив `MyList`.
+2. Последовательно обрабатывать все переданные коллекции.
+3. Записывать каждого студента в указанный CSV-файл.
+4. Сохранять порядок:
+ - коллекций в массиве;
+ - студентов внутри каждой `MyList`.
+5. Использовать тот же формат CSV и те же правила записи, что и `write(Student)`.
+6. Работать только с уже существующим файлом.
+7. Не создавать файл, если он отсутствует.
+8. При отсутствии файла выбрасывать `IOException`.
+9. Не изменять переданные `MyList`.
+10. Не обрабатывать исключения, возникающие при создании или валидации `Student`.
+11. При ошибке записи прекращать операцию и передавать исключение вызывающему коду.
+
+### Пример
+
+```java
+MyList[] 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` внутри массива не должны приводить к ошибке и не должны добавлять записей в файл.
+
+### Проверка пустого массива
+
+Для:
+
+```java
+MyList[] studentLists = ...; // размер 0
+```
+
+метод не должен добавлять никаких записей и не должен изменять файл.
+
+### Проверка отсутствующего файла
+
+Для несуществующего файла:
+
+```java
+writer.write(studentLists);
+```
+
+должен выбрасываться `IOException`. Файл создавать запрещено.
---