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`. Файл создавать запрещено. ---