diff --git a/README b/README new file mode 100644 index 0000000..d587acd --- /dev/null +++ b/README @@ -0,0 +1,256 @@ +# Student Management Console Application + +Консольное Java-приложение для создания, хранения, сортировки, поиска и сохранения коллекций студентов. + +Проект разработан командой в рамках учебного задания и демонстрирует применение ООП, собственных коллекций, алгоритмов сортировки, Stream API, паттернов проектирования, CSV-ввода/вывода и unit-тестирования. + +--- + +## Возможности + +Приложение позволяет: + +- создавать коллекции студентов вручную; +- генерировать случайные данные; +- загружать студентов из CSV-файла; +- просматривать текущую коллекцию; +- сохранять коллекцию в CSV; +- сортировать студентов по номеру группы, среднему баллу и номеру зачётной книжки; +- выбирать алгоритм сортировки; +- выполнять специальную even-only сортировку; +- искать студентов по нескольким критериям; +- объединять критерии поиска через `И` / `ИЛИ`; +- подсчитывать количество вхождений студента; +- получать индексы найденных элементов. + +--- + +## Архитектура + +Основной поток данных: + +```text +Manual / CSV / Random + ↓ + Student + ↓ + MyList + ↓ + ┌─────┴─────┐ + ↓ ↓ + Sorting Search + │ │ + └─────┬─────┘ + ↓ + Console / CSV +``` + +Логика приложения разделена на отдельные функциональные области: модель данных, создание коллекции, хранение, сортировка, поиск, CSV-ввод/вывод и пользовательский интерфейс. + +--- + +## Основные компоненты + +### Student + +`Student` — основная модель данных приложения. + +Модель содержит данные студента, выполняет валидацию входных значений и определяет правила сравнения объектов через `equals()` и `hashCode()`. + +Для пошагового создания объектов используется `StudentBuilder`. + +### Custom Collection + +Для хранения студентов используются собственные: + +- `MyList`; +- `MyLinkedList`. + +Коллекция поддерживает основные операции работы с данными и интеграцию со Stream API. + +Для сбора результатов Stream обратно в `MyList` реализован собственный `MyListCollector`. + +### Sorting + +Система сортировки построена на паттерне **Strategy**. + +Доступны: + +- Merge Sort; +- Bubble Sort; +- even-only sorting. + +`SortStrategy` определяет алгоритм сортировки, а `Comparator` — поле, по которому сравниваются студенты. + +```text +SortStrategy + ↓ +КАК сортировать + +Comparator + ↓ +ПО КАКОМУ ПОЛЮ сортировать +``` + +### Search + +Поиск поддерживает несколько критериев: + +- номер группы; +- средний балл; +- номер зачётной книжки. + +Несколько условий могут объединяться через `И` / `ИЛИ`. + +Для составных условий используется паттерн **Composite**. + +Также реализован поиск количества вхождений студента и индексов найденных элементов. + +### CSV + +За работу с CSV отвечают: + +- `StudentCsvReader`; +- `StudentCsvWriter`. + +Загрузка данных: + +```text +CSV + ↓ +StudentCsvReader + ↓ +Student + ↓ +MyList +``` + +Сохранение: + +```text +MyList + ↓ +StudentCsvWriter + ↓ + CSV +``` + +### Создание коллекции + +Поддерживаются разные способы заполнения коллекции: + +- ручной ввод; +- случайная генерация; +- загрузка из файла. + +Для этого используются отдельные стратегии заполнения. + +--- + +## Использованные паттерны и подходы + +| Паттерн / подход | Использование | +|---|---| +| Builder | создание объектов `Student` | +| Strategy | выбор алгоритма сортировки и способа заполнения коллекции | +| Composite | объединение критериев поиска | +| Custom Collector | преобразование Stream в `MyList` | + +--- + +## Технологии + +- Java 21 +- Maven +- JUnit 5 +- Apache Commons CSV +- Java Stream API +- Git +- GitHub + +--- + +## Команда + +| Участник | GitHub | Основная зона ответственности | +|---|---|---| +| **Володин Герман** | — | архитектура приложения, Console UI, интеграция компонентов | +| **Артющик Анастасия** | `AnastasiArchi` | `Student`, `StudentBuilder`, even-only sorting | +| **Дюрягин Данил** | — | сортировка, `Comparator`, Composite Search | +| **Гаджикурбанов Темирлан** | `t1-f` | собственная коллекция, Collector, Random Generator, поиск вхождений | +| **Ершов Владислав** | `Vladislav-E-N` | CSV Reader/Writer, DataSource, обработка файлов | + +> Компоненты проекта разрабатывались параллельно и в ходе интеграции дорабатывались несколькими участниками. В таблице указаны основные зоны ответственности. + +--- + +## Тестирование + +Основные компоненты проекта покрыты unit-тестами с использованием **JUnit 5**. + +Проверяются: + +- модель `Student` и валидация; +- `equals()` / `hashCode()`; +- алгоритмы сортировки; +- even-only sorting; +- собственная коллекция; +- Custom Collector; +- поиск; +- генерация данных; +- CSV Reader/Writer. + +--- + +## Документация + +### Задания и компоненты + +- [Общее задание](TASK.ru.md) +- [Student](docs/tasks/Student.md) +- [Обновление Student](docs/tasks/StudentUpdate.md) +- [Собственная коллекция](docs/tasks/CustomList.md) +- [Сортировка](docs/tasks/Sorting.md) +- [Even-only sorting](docs/tasks/EvenOnlySortStrategy.md) +- [Composite Search](docs/tasks/CompositeStudentSearch.md) +- [Поиск вхождений](docs/tasks/StudentOccurrenceIndexFinder.md) +- [CSV Reader / Writer](docs/tasks/CsvStudentIO.md) +- [Stream и собственная коллекция](docs/tasks/StreamCollection.md) +- [Тестирование](docs/tasks/Testing.md) + +### Git и командная разработка + +- [Правила создания коммитов](CONTRIBUTING.ru.md) +- [Работа с зависимостями между ветками](GIT_BRANCH_DEPENDENCIES.ru.md) +- [Настройка Git](GIT_CONFIG.ru.md) +- [Восстановление после force-with-lease](GIT_FORCE_WITH_LEASE_RECOVERY.ru.md) + +--- + +## Запуск + +`pom.xml` находится в директории `app`. + +Перейдите в директорию приложения: + +```bash +cd app +``` + +Соберите проект: + +```bash +mvn clean package +``` + +Запустите собранное приложение: + +```bash +java -jar target/app-1.0-SNAPSHOT.jar +``` + +Основной класс приложения: + +```text +ru.project.App +``` diff --git a/app/pom.xml b/app/pom.xml index 509e563..1ce9aa3 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -41,6 +41,25 @@ junit-jupiter-params test + + + org.apache.commons + commons-csv + 1.14.1 + + + + com.googlecode.lanterna + lanterna + 3.1.5 + + + + org.apache.commons + commons-csv + 1.14.1 + + @@ -91,5 +110,41 @@ + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + ru.project.App + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + ru.project.App + + + + + + + + + diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index 8fd4c39..2e2df6b 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -1,10 +1,45 @@ package ru.project; -/** - * Hello world! - */ -public class App { - public static void main(String[] args) { - System.out.println("Hello World!"); +import com.googlecode.lanterna.gui2.MultiWindowTextGUI; +import com.googlecode.lanterna.screen.Screen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import ru.project.model.AppState; +import ru.project.ui.MainMenu; + +public final class App { + + private App() {} + + public static void main(String[] args) { + Screen screen = null; + + try { + DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory(); + + screen = terminalFactory.createScreen(); + screen.startScreen(); + + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen); + + AppState state = new AppState(); + + MainMenu mainMenu = new MainMenu(gui, state); + + mainMenu.showModal(); + + } catch (Exception e) { + System.err.println("Произошла ошибка при выполнении программы:"); + e.printStackTrace(); + + } finally { + if (screen != null) { + try { + screen.stopScreen(); + } catch (Exception e) { + System.err.println("Не удалось корректно завершить работу терминала:"); + e.printStackTrace(); + } + } } + } } diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java new file mode 100644 index 0000000..abbd9c5 --- /dev/null +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -0,0 +1,141 @@ +package ru.project.collection; + +import java.util.Arrays; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class MyLinkedList implements MyList { + private Node head; + private int size; + + @Override + public void add(T value) { + if (head == null) { + this.head = new Node<>(value); + } else { + Node temp = head; + while (temp.getNext() != null) { + temp = temp.getNext(); + } + temp.setNext(new Node<>(value)); + } + size++; + } + + @Override + public T get(int index) { + int currentIndex = 0; + Node temp = head; + while (temp != null) { + if (currentIndex == index) { + return temp.getValue(); + + } else { + temp = temp.getNext(); + currentIndex++; + } + } + throw new IllegalArgumentException(); + } + + @Override + public void remove(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); + } + if (index == 0) { + head = head.getNext(); + size--; + return; + } + int currentIndex = 0; + Node temp = head; + while (temp != null) { + if (currentIndex == index - 1) { + temp.setNext(temp.getNext().getNext()); + size--; + return; + } else { + temp = temp.getNext(); + currentIndex++; + } + } + } + + @Override + public T set(int index, T value) { + if (index < 0 || index >= size) { + throw new IllegalArgumentException(); + } + + Node temp = head; + for (int i = 0; i < index; i++) { + temp = temp.getNext(); + } + + T oldValue = temp.getValue(); + temp.setValue(value); + return oldValue; + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return size == 0; + } + + @Override + public void addAll(MyList collection) { + if (collection == null || collection.isEmpty()) { + return; + } + for (int i = 0; i < collection.size(); i++) { + add(collection.get(i)); + } + } + + @Override + public String toString() { + Object[] res = new Object[size]; + int index = 0; + Node temp = head; + while (temp != null) { + res[index] = temp.getValue(); + index++; + temp = temp.getNext(); + } + return Arrays.toString(res); + } + + @Override + public Stream stream() { + return IntStream.range(0, size()).mapToObj(this::get); + } + + private static class Node { + private T value; + private Node next; + + public Node(T value) { + this.value = value; + } + + public void setValue(T value) { + this.value = value; + } + + public void setNext(Node next) { + this.next = next; + } + + public T getValue() { + return value; + } + + public Node getNext() { + return next; + } + } +} diff --git a/app/src/main/java/ru/project/collection/MyList.java b/app/src/main/java/ru/project/collection/MyList.java new file mode 100644 index 0000000..70411d1 --- /dev/null +++ b/app/src/main/java/ru/project/collection/MyList.java @@ -0,0 +1,22 @@ +package ru.project.collection; + +import java.util.stream.Stream; + +public interface MyList { + + void add(T element); + + T get(int index); + + T set(int index, T element); + + void remove(int index); + + int size(); + + boolean isEmpty(); + + void addAll(MyList collection); + + Stream stream(); +} diff --git a/app/src/main/java/ru/project/collectors/MyCollectors.java b/app/src/main/java/ru/project/collectors/MyCollectors.java new file mode 100644 index 0000000..83b7ae8 --- /dev/null +++ b/app/src/main/java/ru/project/collectors/MyCollectors.java @@ -0,0 +1,12 @@ +package ru.project.collectors; + +import java.util.stream.Collector; +import ru.project.collection.MyList; + +public class MyCollectors { + private MyCollectors() {} + + public static Collector, MyList> toMyList() { + return new MyListCollector<>(); + } +} diff --git a/app/src/main/java/ru/project/collectors/MyListCollector.java b/app/src/main/java/ru/project/collectors/MyListCollector.java new file mode 100644 index 0000000..c5958b0 --- /dev/null +++ b/app/src/main/java/ru/project/collectors/MyListCollector.java @@ -0,0 +1,41 @@ +package ru.project.collectors; + +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; + +public class MyListCollector implements Collector, MyList> { + @Override + public Supplier> supplier() { + return () -> new MyLinkedList<>(); + } + + @Override + public BiConsumer, T> accumulator() { + + return (list, element) -> list.add(element); + } + + @Override + public BinaryOperator> combiner() { + return (list1, list2) -> { + list1.addAll(list2); + return list1; + }; + } + + @Override + public Function, MyList> finisher() { + return list -> list; + } + + @Override + public Set characteristics() { + return java.util.Set.of(Collector.Characteristics.IDENTITY_FINISH); + } +} diff --git a/app/src/main/java/ru/project/finder/StudentOccurrenceIndexFinder.java b/app/src/main/java/ru/project/finder/StudentOccurrenceIndexFinder.java new file mode 100644 index 0000000..45ac263 --- /dev/null +++ b/app/src/main/java/ru/project/finder/StudentOccurrenceIndexFinder.java @@ -0,0 +1,132 @@ +package ru.project.finder; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +public class StudentOccurrenceIndexFinder { + public int[] findOccurrences(MyList students, Student target) { + if (students == null) { + throw new IllegalArgumentException(); + } + if (target == null) { + throw new IllegalArgumentException(); + } + if (students.isEmpty()) { + return new int[0]; + } + int size = students.size(); + int threadCount = calculateThreadCount(size); + int chunkSize = countChunkSize(size, threadCount); + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + MyList>> futures = new MyLinkedList<>(); + + for (int i = 0; i < threadCount; i++) { + int fromIndex = i * chunkSize; + int toIndex = Math.min(fromIndex + chunkSize, size); + + if (fromIndex >= toIndex) { + break; + } + + final int start = fromIndex; + final int end = toIndex; + + Future> future = + executor.submit( + () -> { + return findInRange(students, target, start, end); + }); + futures.add(future); + } + MyList allIndices = new MyLinkedList<>(); + try { + for (int i = 0; i < futures.size(); i++) { + Future> future = futures.get(i); + MyList partialResult = future.get(); + + if (partialResult != null) { + for (int j = 0; j < partialResult.size(); j++) { + allIndices.add(partialResult.get(j)); + } + } + } + } catch (Exception e) { + throw new RuntimeException("Ошибка при поиске", e); + } finally { + executor.shutdown(); + } + + sortList(allIndices); + + int[] resultArray = new int[allIndices.size()]; + for (int i = 0; i < allIndices.size(); i++) { + resultArray[i] = allIndices.get(i); + } + + return resultArray; + } + + private void sortList(MyList list) { + if (list.size() <= 1) return; + + MyList sorted = new MyLinkedList<>(); + + for (int i = 0; i < list.size(); i++) { + int value = list.get(i); + int insertIndex = 0; + while (insertIndex < sorted.size()) { + if (sorted.get(insertIndex) > value) { + break; + } + insertIndex++; + } + + MyList temp = new MyLinkedList<>(); + for (int k = 0; k < insertIndex; k++) { + temp.add(sorted.get(k)); + } + temp.add(value); + for (int k = insertIndex; k < sorted.size(); k++) { + temp.add(sorted.get(k)); + } + sorted = temp; + } + + while (!list.isEmpty()) { + list.remove(0); + } + + for (int i = 0; i < sorted.size(); i++) { + list.add(sorted.get(i)); + } + } + + private int calculateThreadCount(int size) { + int processors = Runtime.getRuntime().availableProcessors(); + return Math.min(processors, size); + } + + private int countChunkSize(int size, int threadCount) { + if (threadCount == 0) return 0; + return (size + threadCount - 1) / threadCount; + } + + private MyList findInRange( + MyList students, Student target, int fromIndex, int toIndex) { + MyList foundIndices = new MyLinkedList<>(); + + for (int i = fromIndex; i < toIndex; i++) { + Student current = students.get(i); + if (current != null && current.equals(target)) { + foundIndices.add(i); + } + } + + return foundIndices; + } +} 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..7cc6deb --- /dev/null +++ b/app/src/main/java/ru/project/io/StudentCsvReader.java @@ -0,0 +1,101 @@ +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/model/AppState.java b/app/src/main/java/ru/project/model/AppState.java new file mode 100644 index 0000000..70f3beb --- /dev/null +++ b/app/src/main/java/ru/project/model/AppState.java @@ -0,0 +1,22 @@ +package ru.project.model; + +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +public class AppState { + + private MyList mainCollection; + + public AppState() { + this.mainCollection = new MyLinkedList<>(); + } + + public MyList getMainCollection() { + return mainCollection; + } + + public void setMainCollection(MyList currentCollection) { + this.mainCollection = currentCollection; + } +} 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..3e3c705 --- /dev/null +++ b/app/src/main/java/ru/project/random/RandomStudentGenerator.java @@ -0,0 +1,231 @@ +package ru.project.random; + +import java.util.Random; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import ru.project.collection.MyList; +import ru.project.collectors.MyCollectors; +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(); + + return IntStream.range(0, size) + .mapToObj(i -> generateStudent()) + .collect(MyCollectors.toMyList()); + } + + private Student generateStudent() { + String groupNumber = generateGroupNumber(); + + double averageGrade = generateAverageGrade(); + + int recordBookNumber = generateRecordBookNumber(); + + return new StudentBuilder() + .setGroupNumber(groupNumber) + .setAverageGrade(averageGrade) + .setRecordBookNumber(recordBookNumber) + .build(); + } + + 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 + && 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; + + double grade = min + (range * RANDOM.nextDouble()); + + return Math.round(grade * 100.0) / 100.0; + } + + 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/main/java/ru/project/search/AverageGradeRangeCriteria.java b/app/src/main/java/ru/project/search/AverageGradeRangeCriteria.java new file mode 100644 index 0000000..dd7ea4b --- /dev/null +++ b/app/src/main/java/ru/project/search/AverageGradeRangeCriteria.java @@ -0,0 +1,22 @@ +package ru.project.search; + +import ru.project.student.Student; + +public class AverageGradeRangeCriteria implements StudentSearchCriteria { + private final double min; + private final double max; + + public AverageGradeRangeCriteria(double min, double max) { + if (min > max) { + throw new IllegalArgumentException("Min grade must be <= max grade"); + } + this.min = min; + this.max = max; + } + + @Override + public boolean matches(Student student) { + double grade = student.getAverageGrade(); + return grade >= min && grade <= max; + } +} diff --git a/app/src/main/java/ru/project/search/CompositeStudentSearchCriteria.java b/app/src/main/java/ru/project/search/CompositeStudentSearchCriteria.java new file mode 100644 index 0000000..3b87850 --- /dev/null +++ b/app/src/main/java/ru/project/search/CompositeStudentSearchCriteria.java @@ -0,0 +1,51 @@ +package ru.project.search; + +import java.util.ArrayList; +import java.util.List; +import ru.project.student.Student; + +public class CompositeStudentSearchCriteria implements StudentSearchCriteria { + private final SearchOperation operation; + private final List criteriaList = new ArrayList<>(); + + public CompositeStudentSearchCriteria(SearchOperation operation) { + if (operation == null) { + throw new IllegalArgumentException("Operation must not be null"); + } + this.operation = operation; + } + + public void add(StudentSearchCriteria criteria) { + if (criteria == null) { + throw new IllegalArgumentException("Criteria must not be null"); + } + criteriaList.add(criteria); + } + + public void remove(StudentSearchCriteria criteria) { + criteriaList.remove(criteria); + } + + @Override + public boolean matches(Student student) { + if (criteriaList.isEmpty()) { + return operation == SearchOperation.INTERSECTION; + } + + if (operation == SearchOperation.UNION) { + for (StudentSearchCriteria c : criteriaList) { + if (c.matches(student)) { + return true; + } + } + return false; + } else { + for (StudentSearchCriteria c : criteriaList) { + if (!c.matches(student)) { + return false; + } + } + return true; + } + } +} diff --git a/app/src/main/java/ru/project/search/GroupNumberRangeCriteria.java b/app/src/main/java/ru/project/search/GroupNumberRangeCriteria.java new file mode 100644 index 0000000..636b64d --- /dev/null +++ b/app/src/main/java/ru/project/search/GroupNumberRangeCriteria.java @@ -0,0 +1,25 @@ +package ru.project.search; + +import ru.project.student.Student; + +public class GroupNumberRangeCriteria implements StudentSearchCriteria { + private final String min; + private final String max; + + public GroupNumberRangeCriteria(String min, String max) { + if (min == null || max == null) { + throw new IllegalArgumentException("Min and max must not be null"); + } + if (min.compareTo(max) > 0) { + throw new IllegalArgumentException("Min group must be <= max group"); + } + this.min = min; + this.max = max; + } + + @Override + public boolean matches(Student student) { + String group = student.getGroupNumber(); + return group.compareTo(min) >= 0 && group.compareTo(max) <= 0; + } +} diff --git a/app/src/main/java/ru/project/search/RecordBookNumberRangeCriteria.java b/app/src/main/java/ru/project/search/RecordBookNumberRangeCriteria.java new file mode 100644 index 0000000..39998f7 --- /dev/null +++ b/app/src/main/java/ru/project/search/RecordBookNumberRangeCriteria.java @@ -0,0 +1,22 @@ +package ru.project.search; + +import ru.project.student.Student; + +public class RecordBookNumberRangeCriteria implements StudentSearchCriteria { + private final int min; + private final int max; + + public RecordBookNumberRangeCriteria(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("Min record number must be <= max"); + } + this.min = min; + this.max = max; + } + + @Override + public boolean matches(Student student) { + int number = student.getRecordBookNumber(); + return number >= min && number <= max; + } +} diff --git a/app/src/main/java/ru/project/search/SearchOperation.java b/app/src/main/java/ru/project/search/SearchOperation.java new file mode 100644 index 0000000..badfc12 --- /dev/null +++ b/app/src/main/java/ru/project/search/SearchOperation.java @@ -0,0 +1,6 @@ +package ru.project.search; + +public enum SearchOperation { + UNION, + INTERSECTION +} diff --git a/app/src/main/java/ru/project/search/StudentSearchCriteria.java b/app/src/main/java/ru/project/search/StudentSearchCriteria.java new file mode 100644 index 0000000..283dc65 --- /dev/null +++ b/app/src/main/java/ru/project/search/StudentSearchCriteria.java @@ -0,0 +1,7 @@ +package ru.project.search; + +import ru.project.student.Student; + +public interface StudentSearchCriteria { + boolean matches(Student student); +} diff --git a/app/src/main/java/ru/project/search/StudentSearchService.java b/app/src/main/java/ru/project/search/StudentSearchService.java new file mode 100644 index 0000000..dc3a366 --- /dev/null +++ b/app/src/main/java/ru/project/search/StudentSearchService.java @@ -0,0 +1,30 @@ +package ru.project.search; + +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +public class StudentSearchService { + private final StudentSearchCriteria criteria; + + public StudentSearchService(StudentSearchCriteria criteria) { + if (criteria == null) { + throw new IllegalArgumentException("Criteria must not be null"); + } + this.criteria = criteria; + } + + public MyList find(MyList students) { + if (students == null) { + return new MyLinkedList<>(); + } + MyList result = new MyLinkedList<>(); + for (int i = 0; i < students.size(); i++) { + Student s = students.get(i); + if (criteria.matches(s)) { + result.add(s); + } + } + return result; + } +} diff --git a/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java new file mode 100644 index 0000000..3c482eb --- /dev/null +++ b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java @@ -0,0 +1,24 @@ +package ru.project.sorting; + +import java.util.Comparator; +import ru.project.collection.MyList; + +public class BubbleSortStrategy implements SortStrategy { + + @Override + public void sort(MyList list, Comparator comparator) { + if (list == null || list.size() <= 1) { + return; + } + int n = list.size(); + for (int i = 0; i < n - 1; i++) { + for (int j = 0; j < n - i - 1; j++) { + if (comparator.compare(list.get(j), list.get(j + 1)) > 0) { + T temp = list.get(j); + list.set(j, list.get(j + 1)); + list.set(j + 1, temp); + } + } + } + } +} diff --git a/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java new file mode 100644 index 0000000..8d179ec --- /dev/null +++ b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java @@ -0,0 +1,89 @@ +package ru.project.sorting; + +import java.util.Comparator; +import java.util.function.ToIntFunction; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; + +public class EvenOnlySortStrategy implements SortStrategy { + + private SortStrategy sortStrategy; + private ToIntFunction valueExtractor; + + public EvenOnlySortStrategy() {} + + public EvenOnlySortStrategy(SortStrategy sortStrategy) { + setSortStrategy(sortStrategy); + } + + public EvenOnlySortStrategy(SortStrategy sortStrategy, ToIntFunction valueExtractor) { + setSortStrategy(sortStrategy); + setValueExtractor(valueExtractor); + } + + public void setSortStrategy(SortStrategy sortStrategy) { + if (sortStrategy == null) { + throw new IllegalArgumentException("Sort strategy must not be null"); + } + + this.sortStrategy = sortStrategy; + } + + public void setValueExtractor(ToIntFunction valueExtractor) { + if (valueExtractor == null) { + throw new IllegalArgumentException("Value extractor must not be null"); + } + + this.valueExtractor = valueExtractor; + } + + @Override + public void sort(MyList list, Comparator comparator) { + if (sortStrategy == null) { + throw new IllegalStateException("Sort strategy is not set"); + } + + if (valueExtractor == null) { + throw new IllegalStateException("Value extractor is not set"); + } + + if (list == null || list.size() <= 1) { + return; + } + + int evenCount = 0; + + for (int i = 0; i < list.size(); i++) { + T element = list.get(i); + + if (valueExtractor.applyAsInt(element) % 2 == 0) { + evenCount++; + } + } + + if (evenCount <= 1) { + return; + } + + MyList evenElements = new MyLinkedList<>(); + int[] evenIndexes = new int[evenCount]; + + int evenIndex = 0; + + for (int i = 0; i < list.size(); i++) { + T element = list.get(i); + + if (valueExtractor.applyAsInt(element) % 2 == 0) { + evenElements.add(element); + evenIndexes[evenIndex] = i; + evenIndex++; + } + } + + sortStrategy.sort(evenElements, comparator); + + for (int i = 0; i < evenIndexes.length; i++) { + list.set(evenIndexes[i], evenElements.get(i)); + } + } +} diff --git a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java new file mode 100644 index 0000000..420adee --- /dev/null +++ b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java @@ -0,0 +1,66 @@ +package ru.project.sorting; + +import java.util.Comparator; +import ru.project.collection.MyList; + +public class MergeSortStrategy implements SortStrategy { + + @Override + public void sort(MyList list, Comparator comparator) { + if (list == null || list.size() <= 1) { + return; + } + mergeSort(list, 0, list.size() - 1, comparator); + } + + private void mergeSort(MyList list, int left, int right, Comparator comparator) { + if (left >= right) { + return; + } + int mid = (left + right) / 2; + mergeSort(list, left, mid, comparator); + mergeSort(list, mid + 1, right, comparator); + merge(list, left, mid, right, comparator); + } + + private void merge(MyList list, int left, int mid, int right, Comparator comparator) { + int leftSize = mid - left + 1; + int rightSize = right - mid; + + @SuppressWarnings("unchecked") + T[] leftArray = (T[]) new Object[leftSize]; + @SuppressWarnings("unchecked") + T[] rightArray = (T[]) new Object[rightSize]; + + for (int i = 0; i < leftSize; i++) { + leftArray[i] = list.get(left + i); + } + for (int j = 0; j < rightSize; j++) { + rightArray[j] = list.get(mid + 1 + j); + } + + int i = 0, j = 0, k = left; + + while (i < leftSize && j < rightSize) { + if (comparator.compare(leftArray[i], rightArray[j]) <= 0) { + list.set(k, leftArray[i]); + i++; + } else { + list.set(k, rightArray[j]); + j++; + } + k++; + } + + while (i < leftSize) { + list.set(k, leftArray[i]); + i++; + k++; + } + while (j < rightSize) { + list.set(k, rightArray[j]); + j++; + k++; + } + } +} diff --git a/app/src/main/java/ru/project/sorting/SortStrategy.java b/app/src/main/java/ru/project/sorting/SortStrategy.java new file mode 100644 index 0000000..b9946e8 --- /dev/null +++ b/app/src/main/java/ru/project/sorting/SortStrategy.java @@ -0,0 +1,8 @@ +package ru.project.sorting; + +import java.util.Comparator; +import ru.project.collection.MyList; + +public interface SortStrategy { + void sort(MyList list, Comparator comparator); +} diff --git a/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java new file mode 100644 index 0000000..3553976 --- /dev/null +++ b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java @@ -0,0 +1,12 @@ +package ru.project.sorting; + +import java.util.Comparator; +import ru.project.student.Student; + +public class StudentAverageGradeComparator implements Comparator { + + @Override + public int compare(Student s1, Student s2) { + return Double.compare(s1.getAverageGrade(), s2.getAverageGrade()); + } +} diff --git a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java new file mode 100644 index 0000000..9278e81 --- /dev/null +++ b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java @@ -0,0 +1,12 @@ +package ru.project.sorting; + +import java.util.Comparator; +import ru.project.student.Student; + +public class StudentGroupComparator implements Comparator { + + @Override + public int compare(Student s1, Student s2) { + return s1.getGroupNumber().compareTo(s2.getGroupNumber()); + } +} diff --git a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java new file mode 100644 index 0000000..ce65a5b --- /dev/null +++ b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java @@ -0,0 +1,12 @@ +package ru.project.sorting; + +import java.util.Comparator; +import ru.project.student.Student; + +public class StudentRecordBookComparator implements Comparator { + + @Override + public int compare(Student s1, Student s2) { + return Integer.compare(s1.getRecordBookNumber(), s2.getRecordBookNumber()); + } +} diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java new file mode 100644 index 0000000..111e559 --- /dev/null +++ b/app/src/main/java/ru/project/student/Student.java @@ -0,0 +1,111 @@ +package ru.project.student; + +import java.util.Objects; + +public class Student { + + public static final double MIN_AVERAGE_GRADE = 0.0; + public static final double MAX_AVERAGE_GRADE = 5.0; + public static final int MIN_RECORD_BOOK_NUMBER = 1; + public static final String GROUP_NUMBER_PATTERN = "^[A-Z][0-9]{2}$"; + + private final String groupNumber; + private final double averageGrade; + private final int recordBookNumber; + + public Student(String groupNumber, double averageGrade, int recordBookNumber) { + if (groupNumber == null || !groupNumber.matches(GROUP_NUMBER_PATTERN)) { + throw new IllegalArgumentException( + "Group number must match pattern: " + GROUP_NUMBER_PATTERN); + } + if (averageGrade < MIN_AVERAGE_GRADE || averageGrade > MAX_AVERAGE_GRADE) { + throw new IllegalArgumentException( + "Average grade must be between " + MIN_AVERAGE_GRADE + " and " + MAX_AVERAGE_GRADE); + } + if (recordBookNumber <= 0) { + throw new IllegalArgumentException("Record book number must be positive"); + } + if (recordBookNumber < MIN_RECORD_BOOK_NUMBER) { + throw new IllegalArgumentException( + "Record book number must be at least " + MIN_RECORD_BOOK_NUMBER); + } + this.groupNumber = groupNumber; + this.averageGrade = averageGrade; + this.recordBookNumber = recordBookNumber; + } + + public String getGroupNumber() { + return groupNumber; + } + + public double getAverageGrade() { + return averageGrade; + } + + public int getRecordBookNumber() { + return recordBookNumber; + } + + public Student withGroupNumber(String groupNumber) { + return new Student(groupNumber, this.averageGrade, this.recordBookNumber); + } + + public Student withAverageGrade(double averageGrade) { + return new Student(this.groupNumber, averageGrade, this.recordBookNumber); + } + + public Student withRecordBookNumber(int recordBookNumber) { + return new Student(this.groupNumber, this.averageGrade, recordBookNumber); + } + + public static double getMinAverageGrade() { + return MIN_AVERAGE_GRADE; + } + + public static double getMaxAverageGrade() { + return MAX_AVERAGE_GRADE; + } + + public static int getMinRecordBookNumber() { + return MIN_RECORD_BOOK_NUMBER; + } + + public static String getGroupNumberPattern() { + return GROUP_NUMBER_PATTERN; + } + + public String toString() { + return "Student{" + + "groupNumber='" + + groupNumber + + '\'' + + ", averageGrade=" + + averageGrade + + ", recordBookNumber=" + + recordBookNumber + + '}'; + } + + @Override + public int hashCode() { + return Objects.hash(groupNumber, averageGrade, recordBookNumber); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (this.hashCode() != obj.hashCode()) { + return false; + } + + Student student = (Student) obj; + return Double.compare(averageGrade, student.averageGrade) == 0 + && recordBookNumber == student.recordBookNumber + && Objects.equals(groupNumber, student.groupNumber); + } +} diff --git a/app/src/main/java/ru/project/student/StudentBuilder.java b/app/src/main/java/ru/project/student/StudentBuilder.java new file mode 100644 index 0000000..4a103d0 --- /dev/null +++ b/app/src/main/java/ru/project/student/StudentBuilder.java @@ -0,0 +1,26 @@ +package ru.project.student; + +public class StudentBuilder { + private String groupNumber; + private double averageGrade; + private int recordBookNumber; + + public StudentBuilder setGroupNumber(String groupNumber) { + this.groupNumber = groupNumber; + return this; + } + + public StudentBuilder setAverageGrade(double averageGrade) { + this.averageGrade = averageGrade; + return this; + } + + public StudentBuilder setRecordBookNumber(int recordBookNumber) { + this.recordBookNumber = recordBookNumber; + return this; + } + + public Student build() { + return new Student(groupNumber, averageGrade, recordBookNumber); + } +} diff --git a/app/src/main/java/ru/project/ui/MainMenu.java b/app/src/main/java/ru/project/ui/MainMenu.java new file mode 100644 index 0000000..cf3c57f --- /dev/null +++ b/app/src/main/java/ru/project/ui/MainMenu.java @@ -0,0 +1,38 @@ +package ru.project.ui; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.model.AppState; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.collection.CurrentCollectionMenu; +import ru.project.ui.creation.CreateCollectionMenu; + +public class MainMenu extends BaseModalWindow { + + private final ActionListBox menu; + + private final BaseModalWindow currentCollectionMenu; + private final BaseModalWindow createCollectionMenu; + + public MainMenu(WindowBasedTextGUI gui, AppState state) { + super("Главное меню", gui); + + this.currentCollectionMenu = new CurrentCollectionMenu(gui, state); + + this.createCollectionMenu = new CreateCollectionMenu(gui, state); + + menu = new ActionListBox(); + + menu.addItem("Меню текущей коллекции", currentCollectionMenu::showModal); + + menu.addItem("Меню создания новой коллекции", createCollectionMenu::showModal); + + menu.addItem("Завершить выполнение программы", this::exit); + + setComponent(menu); + } + + private void exit() { + close(); + } +} diff --git a/app/src/main/java/ru/project/ui/base/BaseModalWindow.java b/app/src/main/java/ru/project/ui/base/BaseModalWindow.java new file mode 100644 index 0000000..0f0abde --- /dev/null +++ b/app/src/main/java/ru/project/ui/base/BaseModalWindow.java @@ -0,0 +1,19 @@ +package ru.project.ui.base; + +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; + +public abstract class BaseModalWindow extends BasicWindow { + + protected final WindowBasedTextGUI gui; + + protected BaseModalWindow(String title, WindowBasedTextGUI gui) { + super(title); + + this.gui = gui; + } + + public void showModal() { + gui.addWindowAndWait(this); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java b/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java new file mode 100644 index 0000000..af294af --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java @@ -0,0 +1,60 @@ +package ru.project.ui.collection; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.model.AppState; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.collection.counting.CountOccurrencesMenu; +import ru.project.ui.collection.search.SearchCollectionMenu; +import ru.project.ui.collection.sorting.SortCollectionMenu; +import ru.project.ui.common.CollectionDisplayWindow; +import ru.project.ui.common.CollectionFileSaveWindow; + +public class CurrentCollectionMenu extends BaseModalWindow { + + private final AppState state; + + private final SortCollectionMenu sortCollectionMenu; + private final SearchCollectionMenu searchCollectionMenu; + private final CountOccurrencesMenu countOccurrencesMenu; + + public CurrentCollectionMenu(WindowBasedTextGUI gui, AppState state) { + + super("Меню текущей коллекции", gui); + + this.state = state; + + this.sortCollectionMenu = new SortCollectionMenu(gui, state); + + this.searchCollectionMenu = new SearchCollectionMenu(gui, state); + + this.countOccurrencesMenu = new CountOccurrencesMenu(gui, state); + + ActionListBox menu = new ActionListBox(); + menu.addItem("Показать коллекцию...", this::showCurrentCollection); + + menu.addItem("Сохранить коллекцию в файл...", this::saveCurrentCollection); + + menu.addItem("Меню сортировки коллекции", sortCollectionMenu::showModal); + + menu.addItem("Меню поиска элемента в коллекции", searchCollectionMenu::showModal); + + menu.addItem("Меню подсчёта количества вхождений элемента", countOccurrencesMenu::showModal); + + menu.addItem("Выйти", this::close); + + setComponent(menu); + } + + private void showCurrentCollection() { + CollectionDisplayWindow window = new CollectionDisplayWindow(gui, state.getMainCollection()); + + window.showModal(); + } + + private void saveCurrentCollection() { + CollectionFileSaveWindow window = new CollectionFileSaveWindow(gui, state.getMainCollection()); + + window.showModal(); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/counting/CountOccurrencesMenu.java b/app/src/main/java/ru/project/ui/collection/counting/CountOccurrencesMenu.java new file mode 100644 index 0000000..26cd626 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/counting/CountOccurrencesMenu.java @@ -0,0 +1,109 @@ +package ru.project.ui.collection.counting; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.collection.MyList; +import ru.project.finder.StudentOccurrenceIndexFinder; +import ru.project.model.AppState; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.MessageWindow; + +public class CountOccurrencesMenu extends BaseModalWindow { + + private final AppState state; + + private final OccurrenceStudentSettingsWindow occurrenceStudentSettingsWindow; + + private Student selectedStudent; + private int occurrences; + + private final Label studentLabel; + private final Label occurrencesLabel; + + public CountOccurrencesMenu(WindowBasedTextGUI gui, AppState state) { + + super("Меню подсчёта количества вхождений элемента", gui); + + this.state = state; + this.selectedStudent = null; + this.occurrences = 0; + + this.studentLabel = new Label("Элемент для поиска: не задан"); + + this.occurrencesLabel = new Label("Количество вхождений: не подсчитано"); + + this.occurrenceStudentSettingsWindow = + new OccurrenceStudentSettingsWindow( + gui, + student -> { + selectedStudent = student; + + studentLabel.setText("Элемент для поиска: задан"); + }); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(studentLabel); + panel.addComponent(occurrencesLabel); + + panel.addComponent(new Button("Настроить элемент для поиска...", this::setupStudent)); + + panel.addComponent(new Button("Выполнить подсчёт", this::countOccurrences)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setupStudent() { + occurrenceStudentSettingsWindow.showModal(); + } + + private void countOccurrences() { + MyList collection = state.getMainCollection(); + + if (collection == null || collection.isEmpty()) { + MessageWindow.showModal(gui, "Текущая коллекция пуста."); + + return; + } + + if (selectedStudent == null) { + MessageWindow.showModal(gui, "Сначала настройте элемент для поиска."); + + return; + } + + StudentOccurrenceIndexFinder finder = new StudentOccurrenceIndexFinder(); + + int[] occurrenceIndices = finder.findOccurrences(collection, selectedStudent); + + occurrences = occurrenceIndices.length; + + StringBuilder result = new StringBuilder(); + + result.append("Количество вхождений: ").append(occurrences); + + result.append("\nИндексы: "); + + if (occurrenceIndices.length == 0) { + result.append("нет"); + } else { + for (int i = 0; i < occurrenceIndices.length; i++) { + + if (i > 0) { + result.append(", "); + } + + result.append(occurrenceIndices[i]); + } + } + + occurrencesLabel.setText(result.toString()); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/counting/OccurrenceStudentSettingsWindow.java b/app/src/main/java/ru/project/ui/collection/counting/OccurrenceStudentSettingsWindow.java new file mode 100644 index 0000000..06cd33d --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/counting/OccurrenceStudentSettingsWindow.java @@ -0,0 +1,172 @@ +package ru.project.ui.collection.counting; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.student.Student; +import ru.project.student.StudentBuilder; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.InputValueWindow; +import ru.project.ui.common.MessageWindow; + +public class OccurrenceStudentSettingsWindow extends BaseModalWindow { + + private final StudentBuilder studentBuilder; + private final Consumer onApply; + + private String groupNumber; + private Double averageGrade; + private Integer recordBookNumber; + + private final Label groupNumberLabel; + private final Label averageGradeLabel; + private final Label recordBookNumberLabel; + + public OccurrenceStudentSettingsWindow(WindowBasedTextGUI gui, Consumer onApply) { + + super("Настройка элемента для поиска", gui); + + this.studentBuilder = new StudentBuilder(); + this.onApply = onApply; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + groupNumberLabel = new Label("Номер группы: не задан"); + + averageGradeLabel = new Label("Средний балл: не задан"); + + recordBookNumberLabel = new Label("Номер зачётной книжки: не задан"); + + panel.addComponent(groupNumberLabel); + panel.addComponent(averageGradeLabel); + panel.addComponent(recordBookNumberLabel); + + panel.addComponent(new Button("Задать номер группы...", this::setGroupNumber)); + + panel.addComponent(new Button("Задать средний балл...", this::setAverageGrade)); + + panel.addComponent(new Button("Задать номер зачётной книжки...", this::setRecordBookNumber)); + + panel.addComponent(new Button("Применить", this::apply)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setGroupNumber() { + InputValueWindow window = + new InputValueWindow( + gui, + "Номер группы", + "Введите номер группы (например, A12):", + groupNumber, + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + groupNumber = value; + studentBuilder.setGroupNumber(value); + + groupNumberLabel.setText("Номер группы: " + groupNumber); + + return true; + }); + + window.showModal(); + } + + private void setAverageGrade() { + InputValueWindow window = + new InputValueWindow( + gui, + "Средний балл", + "Введите средний балл от 0.0 до 5.0:", + averageGrade == null ? null : String.valueOf(averageGrade), + value -> { + try { + double parsedValue = Double.parseDouble(value); + + if (parsedValue < Student.getMinAverageGrade() + || parsedValue > Student.getMaxAverageGrade()) { + + MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0."); + + return false; + } + + averageGrade = parsedValue; + studentBuilder.setAverageGrade(parsedValue); + + averageGradeLabel.setText("Средний балл: " + averageGrade); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите корректное число."); + + return false; + } + }); + + window.showModal(); + } + + private void setRecordBookNumber() { + InputValueWindow window = + new InputValueWindow( + gui, + "Номер зачётной книжки", + "Введите номер зачётной книжки:", + recordBookNumber == null ? null : String.valueOf(recordBookNumber), + value -> { + try { + int parsedValue = Integer.parseInt(value); + + if (parsedValue < Student.getMinRecordBookNumber()) { + + MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным."); + + return false; + } + + recordBookNumber = parsedValue; + studentBuilder.setRecordBookNumber(parsedValue); + + recordBookNumberLabel.setText("Номер зачётной книжки: " + recordBookNumber); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + }); + + window.showModal(); + } + + private void apply() { + if (groupNumber == null || averageGrade == null || recordBookNumber == null) { + + MessageWindow.showModal(gui, "Необходимо задать все поля элемента."); + + return; + } + + Student student = studentBuilder.build(); + + onApply.accept(student); + + MessageWindow.showModal(gui, "Элемент для поиска успешно настроен."); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/search/FilterRelation.java b/app/src/main/java/ru/project/ui/collection/search/FilterRelation.java new file mode 100644 index 0000000..71c6cd2 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/FilterRelation.java @@ -0,0 +1,6 @@ +package ru.project.ui.collection.search; + +public enum FilterRelation { + AND, + OR +} diff --git a/app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java b/app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java new file mode 100644 index 0000000..e820849 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java @@ -0,0 +1,191 @@ +package ru.project.ui.collection.search; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.model.AppState; +import ru.project.search.AverageGradeRangeCriteria; +import ru.project.search.CompositeStudentSearchCriteria; +import ru.project.search.GroupNumberRangeCriteria; +import ru.project.search.RecordBookNumberRangeCriteria; +import ru.project.search.SearchOperation; +import ru.project.search.StudentSearchCriteria; +import ru.project.search.StudentSearchService; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.CollectionDisplayWindow; +import ru.project.ui.common.CollectionFileSaveWindow; +import ru.project.ui.common.MessageWindow; + +public class SearchCollectionMenu extends BaseModalWindow { + + private final AppState state; + + private final SearchSettingsWindow searchSettingsWindow; + + private SearchSettings searchSettings; + private MyList searchResultCollection; + + public SearchCollectionMenu(WindowBasedTextGUI gui, AppState state) { + + super("Меню поиска элемента в коллекции", gui); + + this.state = state; + this.searchSettings = new SearchSettings(); + this.searchResultCollection = new MyLinkedList<>(); + + this.searchSettingsWindow = + new SearchSettingsWindow(gui, settings -> this.searchSettings = settings); + + ActionListBox menu = new ActionListBox(); + + menu.addItem("Выполнить поиск", this::updateSearchResult); + + menu.addItem("Показать результат поиска", this::showSearchResult); + + menu.addItem("Сохранить результат поиска в файл", this::saveSearchResult); + + menu.addItem("Настроить процесс поиска...", searchSettingsWindow::showModal); + + menu.addItem("Выйти", this::close); + + setComponent(menu); + } + + private void updateSearchResult() { + MyList collection = state.getMainCollection(); + + if (collection == null || collection.isEmpty()) { + MessageWindow.showModal(gui, "Коллекция для поиска пуста."); + return; + } + + if (!hasEnabledFilters()) { + MessageWindow.showModal(gui, "Не выбран ни один фильтр."); + return; + } + + if (!hasValidFilterRanges()) { + return; + } + + StudentSearchCriteria criteria = createSearchCriteria(); + + StudentSearchService searchService = new StudentSearchService(criteria); + + searchResultCollection = searchService.find(collection); + + MessageWindow.showModal(gui, "Результат поиска обновлён."); + } + + private StudentSearchCriteria createSearchCriteria() { + SearchOperation operation = + searchSettings.getRelation() == FilterRelation.AND + ? SearchOperation.INTERSECTION + : SearchOperation.UNION; + + CompositeStudentSearchCriteria compositeCriteria = + new CompositeStudentSearchCriteria(operation); + + if (searchSettings.isGroupNumberFilterEnabled()) { + compositeCriteria.add( + new GroupNumberRangeCriteria( + searchSettings.getMinGroupNumber(), searchSettings.getMaxGroupNumber())); + } + + if (searchSettings.isAverageGradeFilterEnabled()) { + compositeCriteria.add( + new AverageGradeRangeCriteria( + searchSettings.getMinAverageGrade(), searchSettings.getMaxAverageGrade())); + } + + if (searchSettings.isRecordBookNumberFilterEnabled()) { + compositeCriteria.add( + new RecordBookNumberRangeCriteria( + searchSettings.getMinRecordBookNumber(), searchSettings.getMaxRecordBookNumber())); + } + + return compositeCriteria; + } + + private boolean hasEnabledFilters() { + return searchSettings.isGroupNumberFilterEnabled() + || searchSettings.isAverageGradeFilterEnabled() + || searchSettings.isRecordBookNumberFilterEnabled(); + } + + private boolean hasValidFilterRanges() { + StringBuilder errors = new StringBuilder(); + + if (!hasValidGroupNumberRange()) { + errors.append("Для фильтра по номеру группы задайте минимальное и максимальное значения.\n"); + } + + if (!hasValidAverageGradeRange()) { + errors.append("Для фильтра по среднему баллу задайте минимальное и максимальное значения.\n"); + } + + if (!hasValidRecordBookNumberRange()) { + errors.append( + "Для фильтра по номеру зачётной книжки задайте минимальное и максимальное значения.\n"); + } + + if (errors.length() > 0) { + MessageWindow.showModal(gui, errors.toString()); + return false; + } + + return true; + } + + private boolean hasValidGroupNumberRange() { + if (!searchSettings.isGroupNumberFilterEnabled()) { + return true; + } + + return searchSettings.getMinGroupNumber() != null && searchSettings.getMaxGroupNumber() != null; + } + + private boolean hasValidAverageGradeRange() { + if (!searchSettings.isAverageGradeFilterEnabled()) { + return true; + } + + return searchSettings.getMinAverageGrade() != null + && searchSettings.getMaxAverageGrade() != null; + } + + private boolean hasValidRecordBookNumberRange() { + if (!searchSettings.isRecordBookNumberFilterEnabled()) { + return true; + } + + return searchSettings.getMinRecordBookNumber() != null + && searchSettings.getMaxRecordBookNumber() != null; + } + + private void showSearchResult() { + if (searchResultCollection == null || searchResultCollection.isEmpty()) { + + MessageWindow.showModal(gui, "Результат поиска пуст."); + return; + } + + CollectionDisplayWindow window = new CollectionDisplayWindow(gui, searchResultCollection); + + window.showModal(); + } + + private void saveSearchResult() { + if (searchResultCollection == null || searchResultCollection.isEmpty()) { + + MessageWindow.showModal(gui, "Результат поиска пуст."); + return; + } + + CollectionFileSaveWindow window = new CollectionFileSaveWindow(gui, searchResultCollection); + + window.showModal(); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/search/SearchSettings.java b/app/src/main/java/ru/project/ui/collection/search/SearchSettings.java new file mode 100644 index 0000000..058a59a --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/SearchSettings.java @@ -0,0 +1,116 @@ +package ru.project.ui.collection.search; + +public class SearchSettings { + + private FilterRelation relation; + + private boolean groupNumberFilterEnabled; + private boolean averageGradeFilterEnabled; + private boolean recordBookNumberFilterEnabled; + + private String minGroupNumber; + private String maxGroupNumber; + + private Double minAverageGrade; + private Double maxAverageGrade; + + private Integer minRecordBookNumber; + private Integer maxRecordBookNumber; + + public SearchSettings() { + this.relation = FilterRelation.AND; + + this.groupNumberFilterEnabled = false; + this.averageGradeFilterEnabled = false; + this.recordBookNumberFilterEnabled = false; + + this.minGroupNumber = null; + this.maxGroupNumber = null; + + this.minAverageGrade = null; + this.maxAverageGrade = null; + + this.minRecordBookNumber = null; + this.maxRecordBookNumber = null; + } + + public FilterRelation getRelation() { + return relation; + } + + public void setRelation(FilterRelation relation) { + this.relation = relation; + } + + public boolean isGroupNumberFilterEnabled() { + return groupNumberFilterEnabled; + } + + public void setGroupNumberFilterEnabled(boolean enabled) { + this.groupNumberFilterEnabled = enabled; + } + + public boolean isAverageGradeFilterEnabled() { + return averageGradeFilterEnabled; + } + + public void setAverageGradeFilterEnabled(boolean enabled) { + this.averageGradeFilterEnabled = enabled; + } + + public boolean isRecordBookNumberFilterEnabled() { + return recordBookNumberFilterEnabled; + } + + public void setRecordBookNumberFilterEnabled(boolean enabled) { + this.recordBookNumberFilterEnabled = enabled; + } + + public String getMinGroupNumber() { + return minGroupNumber; + } + + public void setMinGroupNumber(String minGroupNumber) { + this.minGroupNumber = minGroupNumber; + } + + public String getMaxGroupNumber() { + return maxGroupNumber; + } + + public void setMaxGroupNumber(String maxGroupNumber) { + this.maxGroupNumber = maxGroupNumber; + } + + public Double getMinAverageGrade() { + return minAverageGrade; + } + + public void setMinAverageGrade(Double minAverageGrade) { + this.minAverageGrade = minAverageGrade; + } + + public Double getMaxAverageGrade() { + return maxAverageGrade; + } + + public void setMaxAverageGrade(Double maxAverageGrade) { + this.maxAverageGrade = maxAverageGrade; + } + + public Integer getMinRecordBookNumber() { + return minRecordBookNumber; + } + + public void setMinRecordBookNumber(Integer minRecordBookNumber) { + this.minRecordBookNumber = minRecordBookNumber; + } + + public Integer getMaxRecordBookNumber() { + return maxRecordBookNumber; + } + + public void setMaxRecordBookNumber(Integer maxRecordBookNumber) { + this.maxRecordBookNumber = maxRecordBookNumber; + } +} diff --git a/app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java b/app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java new file mode 100644 index 0000000..059352c --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java @@ -0,0 +1,355 @@ +package ru.project.ui.collection.search; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.CheckBox; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.RadioBoxList; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import java.util.function.Predicate; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.InputValueWindow; +import ru.project.ui.common.MessageWindow; + +public class SearchSettingsWindow extends BaseModalWindow { + + private final SearchSettings settings; + private final Consumer onApply; + + private final CheckBox groupNumberFilterCheckBox; + private final CheckBox averageGradeFilterCheckBox; + private final CheckBox recordBookNumberFilterCheckBox; + + private final Label minGroupNumberLabel; + private final Label maxGroupNumberLabel; + + private final Label minAverageGradeLabel; + private final Label maxAverageGradeLabel; + + private final Label minRecordBookNumberLabel; + private final Label maxRecordBookNumberLabel; + + private final RadioBoxList relationList; + + public SearchSettingsWindow(WindowBasedTextGUI gui, Consumer onApply) { + + super("Настройка процесса поиска", gui); + + this.settings = new SearchSettings(); + this.onApply = onApply; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label("Отношение фильтров:")); + + relationList = new RadioBoxList<>(); + relationList.addItem("И"); + relationList.addItem("ИЛИ"); + relationList.setCheckedItemIndex(0); + + panel.addComponent(relationList); + + groupNumberFilterCheckBox = new CheckBox("Фильтр по номеру группы"); + + averageGradeFilterCheckBox = new CheckBox("Фильтр по среднему баллу"); + + recordBookNumberFilterCheckBox = new CheckBox("Фильтр по номеру зачётной книжки"); + + panel.addComponent(groupNumberFilterCheckBox); + + minGroupNumberLabel = + new Label("Минимум номера группы: " + getValueOrDefault(settings.getMinGroupNumber())); + + maxGroupNumberLabel = + new Label("Максимум номера группы: " + getValueOrDefault(settings.getMaxGroupNumber())); + + panel.addComponent(minGroupNumberLabel); + panel.addComponent(maxGroupNumberLabel); + + panel.addComponent(new Button("Задать минимум номера группы...", this::setMinGroupNumber)); + + panel.addComponent(new Button("Задать максимум номера группы...", this::setMaxGroupNumber)); + + panel.addComponent(averageGradeFilterCheckBox); + + minAverageGradeLabel = + new Label("Минимум среднего балла: " + getValueOrDefault(settings.getMinAverageGrade())); + + maxAverageGradeLabel = + new Label("Максимум среднего балла: " + getValueOrDefault(settings.getMaxAverageGrade())); + + panel.addComponent(minAverageGradeLabel); + panel.addComponent(maxAverageGradeLabel); + + panel.addComponent(new Button("Задать минимум среднего балла...", this::setMinAverageGrade)); + + panel.addComponent(new Button("Задать максимум среднего балла...", this::setMaxAverageGrade)); + + panel.addComponent(recordBookNumberFilterCheckBox); + + minRecordBookNumberLabel = + new Label( + "Минимум номера зачётной книжки: " + + getValueOrDefault(settings.getMinRecordBookNumber())); + + maxRecordBookNumberLabel = + new Label( + "Максимум номера зачётной книжки: " + + getValueOrDefault(settings.getMaxRecordBookNumber())); + + panel.addComponent(minRecordBookNumberLabel); + panel.addComponent(maxRecordBookNumberLabel); + + panel.addComponent( + new Button("Задать минимум номера зачётной книжки...", this::setMinRecordBookNumber)); + + panel.addComponent( + new Button("Задать максимум номера зачётной книжки...", this::setMaxRecordBookNumber)); + + panel.addComponent(new Button("Применить", this::apply)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setMinGroupNumber() { + showGroupNumberInput( + "Минимальный номер группы", + settings.getMinGroupNumber(), + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + if (settings.getMaxGroupNumber() != null + && value.compareTo(settings.getMaxGroupNumber()) > 0) { + + MessageWindow.showModal( + gui, "Минимальный номер группы не может быть больше максимального."); + + return false; + } + + settings.setMinGroupNumber(value); + + minGroupNumberLabel.setText("Минимум номера группы: " + value); + + return true; + }); + } + + private void setMaxGroupNumber() { + showGroupNumberInput( + "Максимальный номер группы", + settings.getMaxGroupNumber(), + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + if (settings.getMinGroupNumber() != null + && value.compareTo(settings.getMinGroupNumber()) < 0) { + + MessageWindow.showModal( + gui, "Максимальный номер группы не может быть меньше минимального."); + + return false; + } + + settings.setMaxGroupNumber(value); + + maxGroupNumberLabel.setText("Максимум номера группы: " + value); + + return true; + }); + } + + private void showGroupNumberInput(String title, String initialValue, Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, title, "Введите номер группы (например, A12):", initialValue, onSave); + + window.showModal(); + } + + private void setMinAverageGrade() { + showAverageGradeInput( + "Минимальный средний балл", + settings.getMinAverageGrade(), + value -> { + if (settings.getMaxAverageGrade() != null && value > settings.getMaxAverageGrade()) { + + MessageWindow.showModal( + gui, "Минимальный средний балл не может быть больше максимального."); + + return false; + } + + settings.setMinAverageGrade(value); + + minAverageGradeLabel.setText("Минимум среднего балла: " + value); + + return true; + }); + } + + private void setMaxAverageGrade() { + showAverageGradeInput( + "Максимальный средний балл", + settings.getMaxAverageGrade(), + value -> { + if (settings.getMinAverageGrade() != null && value < settings.getMinAverageGrade()) { + + MessageWindow.showModal( + gui, "Максимальный средний балл не может быть меньше минимального."); + + return false; + } + + settings.setMaxAverageGrade(value); + + maxAverageGradeLabel.setText("Максимум среднего балла: " + value); + + return true; + }); + } + + private void showAverageGradeInput(String title, Double initialValue, Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, + title, + "Введите значение от 0.0 до 5.0:", + initialValue == null ? null : String.valueOf(initialValue), + value -> { + try { + double parsedValue = Double.parseDouble(value); + + if (parsedValue < Student.getMinAverageGrade() + || parsedValue > Student.getMaxAverageGrade()) { + + MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0."); + + return false; + } + + return onSave.test(parsedValue); + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите корректное число."); + + return false; + } + }); + + window.showModal(); + } + + private void setMinRecordBookNumber() { + showRecordBookNumberInput( + "Минимальный номер зачётной книжки", + settings.getMinRecordBookNumber(), + value -> { + if (settings.getMaxRecordBookNumber() != null + && value > settings.getMaxRecordBookNumber()) { + + MessageWindow.showModal( + gui, "Минимальный номер зачётной книжки не может быть больше максимального."); + + return false; + } + + settings.setMinRecordBookNumber(value); + + minRecordBookNumberLabel.setText("Минимум номера зачётной книжки: " + value); + + return true; + }); + } + + private void setMaxRecordBookNumber() { + showRecordBookNumberInput( + "Максимальный номер зачётной книжки", + settings.getMaxRecordBookNumber(), + value -> { + if (settings.getMinRecordBookNumber() != null + && value < settings.getMinRecordBookNumber()) { + + MessageWindow.showModal( + gui, "Максимальный номер зачётной книжки не может быть меньше минимального."); + + return false; + } + + settings.setMaxRecordBookNumber(value); + + maxRecordBookNumberLabel.setText("Максимум номера зачётной книжки: " + value); + + return true; + }); + } + + private void showRecordBookNumberInput( + String title, Integer initialValue, Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, + title, + "Введите номер зачётной книжки:", + initialValue == null ? null : String.valueOf(initialValue), + value -> { + try { + int parsedValue = Integer.parseInt(value); + + if (parsedValue < Student.getMinRecordBookNumber()) { + + MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным."); + + return false; + } + + return onSave.test(parsedValue); + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + }); + + window.showModal(); + } + + private void apply() { + settings.setGroupNumberFilterEnabled(groupNumberFilterCheckBox.isChecked()); + + settings.setAverageGradeFilterEnabled(averageGradeFilterCheckBox.isChecked()); + + settings.setRecordBookNumberFilterEnabled(recordBookNumberFilterCheckBox.isChecked()); + + settings.setRelation( + relationList.getCheckedItemIndex() == 0 ? FilterRelation.AND : FilterRelation.OR); + + onApply.accept(settings); + + MessageWindow.showModal(gui, "Настройки процесса поиска успешно применены."); + } + + private String getValueOrDefault(Object value) { + return value == null ? "не задан" : String.valueOf(value); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/NormalSortSettingsWindow.java b/app/src/main/java/ru/project/ui/collection/sorting/NormalSortSettingsWindow.java new file mode 100644 index 0000000..6e66e45 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/NormalSortSettingsWindow.java @@ -0,0 +1,45 @@ +package ru.project.ui.collection.sorting; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.ui.base.BaseModalWindow; + +public class NormalSortSettingsWindow extends BaseModalWindow { + + private final Consumer onSortFieldSelected; + + public NormalSortSettingsWindow(WindowBasedTextGUI gui, Consumer onSortFieldSelected) { + + super("Обычная сортировка", gui); + + this.onSortFieldSelected = onSortFieldSelected; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + ActionListBox menu = new ActionListBox(); + + menu.addItem("Сортировать по номеру группы", () -> selectSortField(SortField.GROUP_NUMBER)); + + menu.addItem("Сортировать по среднему баллу", () -> selectSortField(SortField.AVERAGE_GRADE)); + + menu.addItem( + "Сортировать по номеру зачётной книжки", + () -> selectSortField(SortField.RECORD_BOOK_NUMBER)); + + panel.addComponent(menu); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void selectSortField(SortField sortField) { + onSortFieldSelected.accept(sortField); + close(); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/ParitySortSettingsWindow.java b/app/src/main/java/ru/project/ui/collection/sorting/ParitySortSettingsWindow.java new file mode 100644 index 0000000..b458b43 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/ParitySortSettingsWindow.java @@ -0,0 +1,24 @@ +package ru.project.ui.collection.sorting; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.ui.base.BaseModalWindow; + +public class ParitySortSettingsWindow extends BaseModalWindow { + + public ParitySortSettingsWindow(WindowBasedTextGUI gui) { + super("Настройка сортировки по чётности", gui); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label("Сортировка выполняется по номеру зачётной книжки.")); + + panel.addComponent(new Button("Назад", this::close)); + + setComponent(panel); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/SortAlgorithm.java b/app/src/main/java/ru/project/ui/collection/sorting/SortAlgorithm.java new file mode 100644 index 0000000..4f193a6 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/SortAlgorithm.java @@ -0,0 +1,6 @@ +package ru.project.ui.collection.sorting; + +public enum SortAlgorithm { + BUBBLE, + MERGE +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/collection/sorting/SortCollectionMenu.java new file mode 100644 index 0000000..fc8c774 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/SortCollectionMenu.java @@ -0,0 +1,215 @@ +package ru.project.ui.collection.sorting; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.Comparator; +import ru.project.model.AppState; +import ru.project.sorting.BubbleSortStrategy; +import ru.project.sorting.EvenOnlySortStrategy; +import ru.project.sorting.MergeSortStrategy; +import ru.project.sorting.SortStrategy; +import ru.project.sorting.StudentAverageGradeComparator; +import ru.project.sorting.StudentGroupComparator; +import ru.project.sorting.StudentRecordBookComparator; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.MessageWindow; + +public class SortCollectionMenu extends BaseModalWindow { + + private final AppState state; + + private final Label sortAlgorithmLabel; + private final Label sortFieldLabel; + private final Label sortMethodLabel; + + private final NormalSortSettingsWindow normalSortSettingsWindow; + private final ParitySortSettingsWindow paritySortSettingsWindow; + private final SortMethodSelectionWindow sortMethodSelectionWindow; + private final SortStrategySelectionWindow sortStrategySelectionWindow; + + private SortAlgorithm selectedSortAlgorithm; + private SortField selectedSortField; + private SortMethod selectedSortMethod; + + public SortCollectionMenu(WindowBasedTextGUI gui, AppState state) { + + super("Меню сортировки текущей коллекции", gui); + + this.state = state; + + this.selectedSortAlgorithm = SortAlgorithm.BUBBLE; + this.selectedSortField = SortField.GROUP_NUMBER; + this.selectedSortMethod = SortMethod.NORMAL; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + this.sortMethodLabel = new Label("Способ сортировки: " + getSortMethodName(selectedSortMethod)); + + this.sortAlgorithmLabel = + new Label("Алгоритм сортировки: " + getSortAlgorithmName(selectedSortAlgorithm)); + + this.sortFieldLabel = + new Label("Сортировать по: " + getSortFieldName(selectedSortMethod, selectedSortField)); + + this.sortMethodSelectionWindow = + new SortMethodSelectionWindow( + gui, + sortMethod -> { + selectedSortMethod = sortMethod; + + sortMethodLabel.setText( + "Способ сортировки: " + getSortMethodName(selectedSortMethod)); + + sortFieldLabel.setText( + "Сортировать по: " + getSortFieldName(selectedSortMethod, selectedSortField)); + }); + + this.sortStrategySelectionWindow = + new SortStrategySelectionWindow( + gui, + sortAlgorithm -> { + selectedSortAlgorithm = sortAlgorithm; + + sortAlgorithmLabel.setText( + "Алгоритм сортировки: " + getSortAlgorithmName(selectedSortAlgorithm)); + }); + + this.normalSortSettingsWindow = + new NormalSortSettingsWindow( + gui, + sortField -> { + selectedSortField = sortField; + + sortFieldLabel.setText( + "Сортировать по: " + getSortFieldName(selectedSortMethod, selectedSortField)); + }); + + this.paritySortSettingsWindow = new ParitySortSettingsWindow(gui); + + panel.addComponent(sortMethodLabel); + panel.addComponent(sortFieldLabel); + panel.addComponent(sortAlgorithmLabel); + + panel.addComponent(new Button("Выбрать способ сортировки...", this::selectSortMethod)); + + panel.addComponent(new Button("Выбрать алгоритм сортировки...", this::selectSortAlgorithm)); + + panel.addComponent( + new Button("Настроить текущий способ сортировки...", this::setupCurrentSortMethod)); + + panel.addComponent(new Button("Сортировать текущую коллекцию", this::sortCurrentCollection)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setupCurrentSortMethod() { + switch (selectedSortMethod) { + case NORMAL -> normalSortSettingsWindow.showModal(); + + case PARITY -> paritySortSettingsWindow.showModal(); + } + } + + private void selectSortMethod() { + sortMethodSelectionWindow.showModal(); + } + + private void selectSortAlgorithm() { + sortStrategySelectionWindow.showModal(); + } + + private void sortCurrentCollection() { + if (state.getMainCollection() == null || state.getMainCollection().isEmpty()) { + + MessageWindow.showModal(gui, "Текущая коллекция пуста."); + + return; + } + + switch (selectedSortMethod) { + case NORMAL -> sortNormally(); + + case PARITY -> sortByParity(); + } + } + + private void sortNormally() { + SortStrategy sortStrategy = getSortStrategy(); + + Comparator comparator = getSortComparator(); + + sortStrategy.sort(state.getMainCollection(), comparator); + + MessageWindow.showModal(gui, "Обычная сортировка успешно выполнена."); + } + + private void sortByParity() { + SortStrategy sortStrategy = + new EvenOnlySortStrategy<>(getSortStrategy(), Student::getRecordBookNumber); + + Comparator comparator = new StudentRecordBookComparator(); + + sortStrategy.sort(state.getMainCollection(), comparator); + + MessageWindow.showModal(gui, "Сортировка по чётности успешно выполнена."); + } + + private SortStrategy getSortStrategy() { + return switch (selectedSortAlgorithm) { + case BUBBLE -> new BubbleSortStrategy<>(); + + case MERGE -> new MergeSortStrategy<>(); + }; + } + + private Comparator getSortComparator() { + return switch (selectedSortField) { + case GROUP_NUMBER -> new StudentGroupComparator(); + + case AVERAGE_GRADE -> new StudentAverageGradeComparator(); + + case RECORD_BOOK_NUMBER -> new StudentRecordBookComparator(); + }; + } + + private String getSortMethodName(SortMethod sortMethod) { + return switch (sortMethod) { + case NORMAL -> "Обычная сортировка"; + + case PARITY -> "Сортировка по чётности"; + }; + } + + private String getSortAlgorithmName(SortAlgorithm sortAlgorithm) { + return switch (sortAlgorithm) { + case BUBBLE -> "Сортировка пузырьком"; + + case MERGE -> "Сортировка слиянием"; + }; + } + + private String getSortFieldName(SortMethod sortMethod, SortField sortField) { + return switch (sortMethod) { + case NORMAL -> getNormalSortFieldName(sortField); + + case PARITY -> "номеру зачётной книжки"; + }; + } + + private String getNormalSortFieldName(SortField sortField) { + return switch (sortField) { + case GROUP_NUMBER -> "номеру группы"; + + case AVERAGE_GRADE -> "среднему баллу"; + + case RECORD_BOOK_NUMBER -> "номеру зачётной книжки"; + }; + } +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/SortField.java b/app/src/main/java/ru/project/ui/collection/sorting/SortField.java new file mode 100644 index 0000000..5b6124b --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/SortField.java @@ -0,0 +1,7 @@ +package ru.project.ui.collection.sorting; + +public enum SortField { + GROUP_NUMBER, + AVERAGE_GRADE, + RECORD_BOOK_NUMBER +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/SortMethod.java b/app/src/main/java/ru/project/ui/collection/sorting/SortMethod.java new file mode 100644 index 0000000..9e56a8d --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/SortMethod.java @@ -0,0 +1,6 @@ +package ru.project.ui.collection.sorting; + +public enum SortMethod { + NORMAL, + PARITY +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/SortMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/collection/sorting/SortMethodSelectionWindow.java new file mode 100644 index 0000000..dc49e68 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/SortMethodSelectionWindow.java @@ -0,0 +1,34 @@ +package ru.project.ui.collection.sorting; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.ui.base.BaseModalWindow; + +public class SortMethodSelectionWindow extends BaseModalWindow { + + private final Consumer onSortMethodSelected; + + public SortMethodSelectionWindow( + WindowBasedTextGUI gui, Consumer onSortMethodSelected) { + + super("Выбрать способ сортировки", gui); + + this.onSortMethodSelected = onSortMethodSelected; + + ActionListBox menu = new ActionListBox(); + + menu.addItem("Обычная сортировка", () -> selectSortMethod(SortMethod.NORMAL)); + + menu.addItem("Сортировка по чётности", () -> selectSortMethod(SortMethod.PARITY)); + + menu.addItem("Выйти", this::close); + + setComponent(menu); + } + + private void selectSortMethod(SortMethod sortMethod) { + onSortMethodSelected.accept(sortMethod); + close(); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/sorting/SortStrategySelectionWindow.java b/app/src/main/java/ru/project/ui/collection/sorting/SortStrategySelectionWindow.java new file mode 100644 index 0000000..2084e4e --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/sorting/SortStrategySelectionWindow.java @@ -0,0 +1,34 @@ +package ru.project.ui.collection.sorting; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.ui.base.BaseModalWindow; + +public class SortStrategySelectionWindow extends BaseModalWindow { + + private final Consumer onSortAlgorithmSelected; + + public SortStrategySelectionWindow( + WindowBasedTextGUI gui, Consumer onSortAlgorithmSelected) { + + super("Выбор алгоритма сортировки", gui); + + this.onSortAlgorithmSelected = onSortAlgorithmSelected; + + ActionListBox menu = new ActionListBox(); + + menu.addItem("Сортировка пузырьком", () -> selectSortAlgorithm(SortAlgorithm.BUBBLE)); + + menu.addItem("Сортировка слиянием", () -> selectSortAlgorithm(SortAlgorithm.MERGE)); + + menu.addItem("Выйти", this::close); + + setComponent(menu); + } + + private void selectSortAlgorithm(SortAlgorithm sortAlgorithm) { + onSortAlgorithmSelected.accept(sortAlgorithm); + close(); + } +} diff --git a/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java b/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java new file mode 100644 index 0000000..1d71808 --- /dev/null +++ b/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java @@ -0,0 +1,102 @@ +package ru.project.ui.common; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.EmptySpace; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.TextBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; +import ru.project.collection.MyList; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; + +public class CollectionDisplayWindow extends BaseModalWindow { + + private final MyList collection; + + private TextBox collectionTextBox; + private Button exitButton; + + public CollectionDisplayWindow(WindowBasedTextGUI gui, MyList collection) { + + super("Просмотр коллекции", gui); + + this.collection = collection; + + Panel panel = createPanel(); + + setComponent(panel); + + exitButton.takeFocus(); + } + + @Override + public boolean handleInput(KeyStroke key) { + if (key.getKeyType() == KeyType.Escape && collectionTextBox.isFocused()) { + + exitButton.takeFocus(); + return true; + } + + return super.handleInput(key); + } + + private Panel createPanel() { + Panel panel = new Panel(); + + panel.setLayoutManager(new LinearLayout(Direction.VERTICAL)); + + addCollectionDisplay(panel); + addExitButton(panel); + + return panel; + } + + private void addCollectionDisplay(Panel panel) { + collectionTextBox = createCollectionScrollBox(); + + panel.addComponent(collectionTextBox); + } + + private TextBox createCollectionScrollBox() { + TextBox textBox = new TextBox(new TerminalSize(80, 20), TextBox.Style.MULTI_LINE); + + textBox.setReadOnly(true); + + if (collection == null || collection.isEmpty()) { + textBox.setText("Коллекция пуста."); + } else { + textBox.setText(buildCollectionText()); + } + + return textBox; + } + + private void addExitButton(Panel panel) { + panel.addComponent(new EmptySpace(new TerminalSize(0, 1))); + + exitButton = new Button("Выйти", this::close); + + panel.addComponent(exitButton); + } + + private String buildCollectionText() { + StringBuilder text = new StringBuilder(); + + for (int i = 0; i < collection.size(); i++) { + Student student = collection.get(i); + + text.append("[") + .append(i) + .append("] ") + .append(student == null ? "null" : student) + .append("\n"); + } + + return text.toString(); + } +} diff --git a/app/src/main/java/ru/project/ui/common/CollectionFileSaveWindow.java b/app/src/main/java/ru/project/ui/common/CollectionFileSaveWindow.java new file mode 100644 index 0000000..a8ce5eb --- /dev/null +++ b/app/src/main/java/ru/project/ui/common/CollectionFileSaveWindow.java @@ -0,0 +1,86 @@ +package ru.project.ui.common; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import com.googlecode.lanterna.gui2.dialogs.FileDialogBuilder; +import com.googlecode.lanterna.gui2.dialogs.MessageDialog; +import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton; +import java.io.File; +import java.io.IOException; +import ru.project.collection.MyList; +import ru.project.io.StudentCsvWriter; +import ru.project.student.Student; + +public class CollectionFileSaveWindow { + + private final WindowBasedTextGUI gui; + private final MyList collection; + + public CollectionFileSaveWindow(WindowBasedTextGUI gui, MyList collection) { + + this.gui = gui; + this.collection = collection; + } + + public void showModal() { + File selectedFile = selectFile(); + + if (selectedFile == null) { + return; + } + + if (selectedFile.exists()) { + saveToFile(selectedFile); + return; + } + + MessageDialogButton result = + MessageDialog.showMessageDialog( + gui, + "Создание файла", + "Файл не существует. Создать новый файл?", + MessageDialogButton.Yes, + MessageDialogButton.No); + + if (result == MessageDialogButton.Yes) { + createAndSaveToFile(selectedFile); + } + } + + private File selectFile() { + return new FileDialogBuilder() + .setTitle("Сохранение коллекции") + .setDescription("Выберите файл:") + .setActionLabel("Сохранить") + .build() + .showDialog(gui); + } + + private void createAndSaveToFile(File file) { + try { + if (!file.createNewFile()) { + MessageWindow.showModal(gui, "Не удалось создать файл."); + return; + } + + saveToFile(file); + + } catch (IOException e) { + MessageWindow.showModal(gui, "Не удалось создать файл."); + } + } + + private void saveToFile(File file) { + try { + StudentCsvWriter writer = new StudentCsvWriter(file.getAbsolutePath()); + + for (int i = 0; i < collection.size(); i++) { + writer.write(collection.get(i)); + } + + MessageWindow.showModal(gui, "Коллекция успешно сохранена."); + + } catch (IOException e) { + MessageWindow.showModal(gui, "Не удалось сохранить коллекцию."); + } + } +} diff --git a/app/src/main/java/ru/project/ui/common/CollectionSizeInputWindow.java b/app/src/main/java/ru/project/ui/common/CollectionSizeInputWindow.java new file mode 100644 index 0000000..57b1d3a --- /dev/null +++ b/app/src/main/java/ru/project/ui/common/CollectionSizeInputWindow.java @@ -0,0 +1,51 @@ +package ru.project.ui.common; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; + +public class CollectionSizeInputWindow { + + private final WindowBasedTextGUI gui; + private final Integer currentSize; + private final Consumer onSizeEntered; + + public CollectionSizeInputWindow( + WindowBasedTextGUI gui, Integer currentSize, Consumer onSizeEntered) { + + this.gui = gui; + this.currentSize = currentSize; + this.onSizeEntered = onSizeEntered; + } + + public void showModal() { + InputValueWindow window = + new InputValueWindow( + gui, + "Размер коллекции", + "Введите размер коллекции:", + currentSize != null && currentSize > 0 ? String.valueOf(currentSize) : null, + this::handleSizeEntered); + + window.showModal(); + } + + private boolean handleSizeEntered(String value) { + try { + int size = Integer.parseInt(value); + + if (size <= 0) { + MessageWindow.showModal(gui, "Размер должен быть больше 0."); + + return false; + } + + onSizeEntered.accept(size); + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + } +} diff --git a/app/src/main/java/ru/project/ui/common/InputValueWindow.java b/app/src/main/java/ru/project/ui/common/InputValueWindow.java new file mode 100644 index 0000000..f3e3200 --- /dev/null +++ b/app/src/main/java/ru/project/ui/common/InputValueWindow.java @@ -0,0 +1,56 @@ +package ru.project.ui.common; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.TextBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Predicate; +import ru.project.ui.base.BaseModalWindow; + +public class InputValueWindow extends BaseModalWindow { + + private final TextBox textBox; + private final Predicate onSave; + + public InputValueWindow( + WindowBasedTextGUI gui, + String title, + String prompt, + String initialValue, + Predicate onSave) { + + super(title, gui); + + this.onSave = onSave; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label(prompt)); + + textBox = new TextBox(initialValue == null ? "" : initialValue); + + panel.addComponent(textBox); + + panel.addComponent(new Button("Сохранить", this::save)); + + panel.addComponent(new Button("Отмена", this::close)); + + setComponent(panel); + } + + private void save() { + String value = textBox.getText().trim(); + + if (value.isEmpty()) { + MessageWindow.showModal(gui, "Значение не может быть пустым."); + return; + } + + if (onSave.test(value)) { + close(); + } + } +} diff --git a/app/src/main/java/ru/project/ui/common/MessageWindow.java b/app/src/main/java/ru/project/ui/common/MessageWindow.java new file mode 100644 index 0000000..3f1905f --- /dev/null +++ b/app/src/main/java/ru/project/ui/common/MessageWindow.java @@ -0,0 +1,28 @@ +package ru.project.ui.common; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.ui.base.BaseModalWindow; + +public class MessageWindow extends BaseModalWindow { + + public MessageWindow(WindowBasedTextGUI gui, String message) { + super("Информация", gui); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label(message)); + + panel.addComponent(new Button("OK", this::close)); + + setComponent(panel); + } + + public static void showModal(WindowBasedTextGUI gui, String message) { + new MessageWindow(gui, message).showModal(); + } +} diff --git a/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java new file mode 100644 index 0000000..76043ea --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java @@ -0,0 +1,120 @@ +package ru.project.ui.creation; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.model.AppState; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.MessageWindow; +import ru.project.ui.creation.file.FileDataSettingsWindow; +import ru.project.ui.creation.file.FileFillSettings; +import ru.project.ui.creation.file.FileFillStrategy; +import ru.project.ui.creation.manual.ManualDataSettingsWindow; +import ru.project.ui.creation.manual.ManualFillSettings; +import ru.project.ui.creation.manual.ManualFillStrategy; +import ru.project.ui.creation.random.RandomDataSettingsWindow; +import ru.project.ui.creation.random.RandomFillSettings; +import ru.project.ui.creation.random.RandomFillStrategy; + +public class CreateCollectionMenu extends BaseModalWindow { + + private final AppState state; + + private FillMethod selectedFillMethod; + private FillStrategy fillStrategy; + + private final Label fillMethodLabel; + + private final ManualDataSettingsWindow manualDataSettingsWindow; + private final FileDataSettingsWindow fileDataSettingsWindow; + private final RandomDataSettingsWindow randomDataSettingsWindow; + + public CreateCollectionMenu(WindowBasedTextGUI gui, AppState state) { + + super("Создание новой коллекции", gui); + + this.state = state; + this.selectedFillMethod = FillMethod.CUSTOM; + this.fillStrategy = null; + + this.manualDataSettingsWindow = new ManualDataSettingsWindow(gui, this::setManualFillStrategy); + this.fileDataSettingsWindow = new FileDataSettingsWindow(gui, this::setFileFillStrategy); + this.randomDataSettingsWindow = new RandomDataSettingsWindow(gui, this::setRandomFillStrategy); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + fillMethodLabel = new Label("Способ заполнения: " + getFillMethodName(selectedFillMethod)); + + panel.addComponent(fillMethodLabel); + + panel.addComponent(new Button("Выбрать способ заполнения...", this::selectFillMethod)); + + panel.addComponent( + new Button("Настроить текущий способ заполнения...", this::setupCurrentFillMethod)); + + panel.addComponent(new Button("Создать новую коллекцию", this::updateMainCollection)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setManualFillStrategy(ManualFillSettings settings) { + fillStrategy = new ManualFillStrategy(settings); + } + + private void setFileFillStrategy(FileFillSettings settings) { + fillStrategy = new FileFillStrategy(settings); + } + + private void setRandomFillStrategy(RandomFillSettings settings) { + fillStrategy = new RandomFillStrategy(settings); + } + + private void setupCurrentFillMethod() { + switch (selectedFillMethod) { + case CUSTOM -> manualDataSettingsWindow.showModal(); + + case FILE -> fileDataSettingsWindow.showModal(); + + case RANDOM -> randomDataSettingsWindow.showModal(); + } + } + + private void selectFillMethod() { + BaseModalWindow menu = + new FillMethodSelectionWindow( + gui, + fillMethod -> { + selectedFillMethod = fillMethod; + + fillMethodLabel.setText( + "Способ заполнения: " + getFillMethodName(selectedFillMethod)); + }); + + menu.showModal(); + } + + private void updateMainCollection() { + if (fillStrategy == null) { + MessageWindow.showModal(gui, "Сначала настройте способ заполнения."); + + return; + } + + state.setMainCollection(fillStrategy.create()); + + MessageWindow.showModal(gui, "Коллекция успешно создана."); + } + + private String getFillMethodName(FillMethod fillMethod) { + return switch (fillMethod) { + case CUSTOM -> "Пользовательские данные"; + case RANDOM -> "Случайные данные"; + case FILE -> "Данные из файла"; + }; + } +} diff --git a/app/src/main/java/ru/project/ui/creation/FillMethod.java b/app/src/main/java/ru/project/ui/creation/FillMethod.java new file mode 100644 index 0000000..28567fe --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/FillMethod.java @@ -0,0 +1,7 @@ +package ru.project.ui.creation; + +public enum FillMethod { + CUSTOM, + RANDOM, + FILE +} diff --git a/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java new file mode 100644 index 0000000..9cf0fc7 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java @@ -0,0 +1,36 @@ +package ru.project.ui.creation; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.ui.base.BaseModalWindow; + +public class FillMethodSelectionWindow extends BaseModalWindow { + + private final Consumer onFillMethodSelected; + + public FillMethodSelectionWindow( + WindowBasedTextGUI gui, Consumer onFillMethodSelected) { + + super("Выбор способа заполнения", gui); + + this.onFillMethodSelected = onFillMethodSelected; + + ActionListBox menu = new ActionListBox(); + + menu.addItem("Пользовательские данные", () -> selectFillMethod(FillMethod.CUSTOM)); + + menu.addItem("Случайные данные", () -> selectFillMethod(FillMethod.RANDOM)); + + menu.addItem("Данные из файла", () -> selectFillMethod(FillMethod.FILE)); + + menu.addItem("Выйти", this::close); + + setComponent(menu); + } + + private void selectFillMethod(FillMethod fillMethod) { + onFillMethodSelected.accept(fillMethod); + close(); + } +} diff --git a/app/src/main/java/ru/project/ui/creation/FillStrategy.java b/app/src/main/java/ru/project/ui/creation/FillStrategy.java new file mode 100644 index 0000000..f4931d8 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/FillStrategy.java @@ -0,0 +1,9 @@ +package ru.project.ui.creation; + +import ru.project.collection.MyList; +import ru.project.student.Student; + +public interface FillStrategy { + + MyList create(); +} diff --git a/app/src/main/java/ru/project/ui/creation/file/FileDataSettingsWindow.java b/app/src/main/java/ru/project/ui/creation/file/FileDataSettingsWindow.java new file mode 100644 index 0000000..bf1b7d4 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/file/FileDataSettingsWindow.java @@ -0,0 +1,78 @@ +package ru.project.ui.creation.file; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import com.googlecode.lanterna.gui2.dialogs.FileDialog; +import com.googlecode.lanterna.gui2.dialogs.FileDialogBuilder; +import java.io.File; +import java.util.function.Consumer; +import ru.project.ui.base.BaseModalWindow; + +public class FileDataSettingsWindow extends BaseModalWindow { + + // private final StudentBuilder studentBuilder; + + private final FileFillSettings settings; + private final Consumer onApply; + + private final Label filePathLabel; + + public FileDataSettingsWindow(WindowBasedTextGUI gui, Consumer onApply) { + + super("Данные из файла", gui); + + this.onApply = onApply; + + this.settings = new FileFillSettings(); + // this.studentBuilder = new StudentBuilder(); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + filePathLabel = new Label("Абсолютный путь: " + getFilePathLabel()); + + panel.addComponent(filePathLabel); + + panel.addComponent(new Button("Выбрать файл", this::selectFile)); + + panel.addComponent(new Button("Применить", this::apply)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void selectFile() { + FileDialog fileDialog = + new FileDialogBuilder() + .setTitle("Выбор файла") + .setDescription("Выберите файл:") + .setActionLabel("Открыть") + .build(); + + File selectedFile = fileDialog.showDialog(gui); + + if (selectedFile == null) { + return; + } + + settings.setAbsoluteFilePath(selectedFile.getAbsolutePath()); + + filePathLabel.setText("Абсолютный путь: " + settings.getAbsoluteFilePath()); + } + + private void apply() { + onApply.accept(settings); + + close(); + } + + private String getFilePathLabel() { + String filePath = settings.getAbsoluteFilePath(); + + return filePath == null || filePath.isBlank() ? "не задан" : filePath; + } +} diff --git a/app/src/main/java/ru/project/ui/creation/file/FileFillSettings.java b/app/src/main/java/ru/project/ui/creation/file/FileFillSettings.java new file mode 100644 index 0000000..db7e9cc --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/file/FileFillSettings.java @@ -0,0 +1,18 @@ +package ru.project.ui.creation.file; + +public class FileFillSettings { + + private String absoluteFilePath; + + public FileFillSettings() { + this.absoluteFilePath = null; + } + + public String getAbsoluteFilePath() { + return absoluteFilePath; + } + + public void setAbsoluteFilePath(String absoluteFilePath) { + this.absoluteFilePath = absoluteFilePath; + } +} diff --git a/app/src/main/java/ru/project/ui/creation/file/FileFillStrategy.java b/app/src/main/java/ru/project/ui/creation/file/FileFillStrategy.java new file mode 100644 index 0000000..aa5feb5 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/file/FileFillStrategy.java @@ -0,0 +1,36 @@ +package ru.project.ui.creation.file; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; +import ru.project.collection.MyList; +import ru.project.collectors.MyCollectors; +import ru.project.io.StudentCsvReader; +import ru.project.student.Student; +import ru.project.ui.creation.FillStrategy; + +public class FileFillStrategy implements FillStrategy { + + private final FileFillSettings settings; + + public FileFillStrategy(FileFillSettings settings) { + this.settings = settings; + } + + @Override + public MyList create() { + Path path = Path.of(settings.getAbsoluteFilePath()); + + try (StudentCsvReader reader = new StudentCsvReader(path)) { + + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize(reader, Spliterator.ORDERED), false) + .collect(MyCollectors.toMyList()); + + } catch (IOException e) { + throw new RuntimeException("Не удалось прочитать коллекцию из файла.", e); + } + } +} diff --git a/app/src/main/java/ru/project/ui/creation/manual/ManualDataSettingsWindow.java b/app/src/main/java/ru/project/ui/creation/manual/ManualDataSettingsWindow.java new file mode 100644 index 0000000..dd02eeb --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/manual/ManualDataSettingsWindow.java @@ -0,0 +1,338 @@ +package ru.project.ui.creation.manual; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; +import ru.project.student.StudentBuilder; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.CollectionDisplayWindow; +import ru.project.ui.common.CollectionSizeInputWindow; +import ru.project.ui.common.InputValueWindow; +import ru.project.ui.common.MessageWindow; + +public class ManualDataSettingsWindow extends BaseModalWindow { + + private final StudentBuilder studentBuilder; + + private Integer lastRequestedCollectionSize; + private String lastRequestedGroupNumber; + private Double lastRequestedAverageGrade; + private Integer lastRequestedRecordBookNumber; + private Integer lastRequestedSelectedObjectIndex; + + private final Label sizeLabel; + private final Label groupNumberLabel; + private final Label averageGradeLabel; + private final Label recordBookNumberLabel; + private final Label indexLabel; + + private MyList preparedCustomDataCollection; + + private final ManualFillSettings settings; + private final Consumer onApply; + + public ManualDataSettingsWindow(WindowBasedTextGUI gui, Consumer onApply) { + + super("Настройка пользовательских данных", gui); + + this.onApply = onApply; + + this.settings = new ManualFillSettings(); + this.studentBuilder = new StudentBuilder(); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + sizeLabel = new Label("Размер: не задан"); + + groupNumberLabel = new Label("Номер группы: не задан"); + + averageGradeLabel = new Label("Средний балл: не задан"); + + recordBookNumberLabel = new Label("Номер зачётной книжки: не задан"); + + indexLabel = new Label("Индекс объекта: не задан"); + + panel.addComponent(sizeLabel); + panel.addComponent(groupNumberLabel); + panel.addComponent(averageGradeLabel); + panel.addComponent(recordBookNumberLabel); + panel.addComponent(indexLabel); + + panel.addComponent(new Button("Задать размер", this::showCollectionSizeInputWindow)); + + panel.addComponent(new Button("Задать номер группы", this::setGroupNumber)); + + panel.addComponent(new Button("Задать средний балл", this::setAverageGrade)); + + panel.addComponent(new Button("Задать номер зачётной книжки", this::setRecordBookNumber)); + + panel.addComponent(new Button("Задать индекс объекта", this::setObjectIndex)); + + panel.addComponent(new Button("Задать данные объекта", this::editObject)); + + panel.addComponent(new Button("Предпросмотр", this::preview)); + + panel.addComponent(new Button("Применить", this::apply)); + + panel.addComponent(new Button("Назад", this::close)); + + setComponent(panel); + } + + private void showCollectionSizeInputWindow() { + CollectionSizeInputWindow window = + new CollectionSizeInputWindow( + gui, preparedCustomDataCollection.size(), this::handleCollectionSizeEntered); + + window.showModal(); + } + + private void handleCollectionSizeEntered(int size) { + MyList resizedCollection = new MyLinkedList<>(); + + int currentCollectionSize = preparedCustomDataCollection.size(); + + int elementsToCopy = Math.min(currentCollectionSize, size); + + for (int i = 0; i < elementsToCopy; i++) { + resizedCollection.add(preparedCustomDataCollection.get(i)); + } + + for (int i = elementsToCopy; i < size; i++) { + resizedCollection.add(null); + } + + preparedCustomDataCollection = resizedCollection; + lastRequestedCollectionSize = size; + + if (lastRequestedSelectedObjectIndex != null + && lastRequestedSelectedObjectIndex >= lastRequestedCollectionSize) { + lastRequestedSelectedObjectIndex = null; + indexLabel.setText("Индекс объекта: не задан"); + } + + sizeLabel.setText("Размер: " + lastRequestedCollectionSize); + } + + @Override + public void showModal() { + prepareCustomDataCollection(); + + super.showModal(); + } + + private void prepareCustomDataCollection() { + MyList currentCustomDataCollection = settings.getCollection(); + + preparedCustomDataCollection = new MyLinkedList<>(); + + int currentCollectionSize = currentCustomDataCollection.size(); + + int targetSize = + lastRequestedCollectionSize == null ? currentCollectionSize : lastRequestedCollectionSize; + + int elementsToCopy = Math.min(currentCollectionSize, targetSize); + + for (int i = 0; i < elementsToCopy; i++) { + preparedCustomDataCollection.add(currentCustomDataCollection.get(i)); + } + + for (int i = elementsToCopy; i < targetSize; i++) { + preparedCustomDataCollection.add(null); + } + } + + private void setGroupNumber() { + InputValueWindow window = + new InputValueWindow( + gui, + "Номер группы", + "Введите номер группы (например, A12):", + lastRequestedGroupNumber, + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + return false; + } + + lastRequestedGroupNumber = value; + studentBuilder.setGroupNumber(value); + + groupNumberLabel.setText("Номер группы: " + lastRequestedGroupNumber); + + return true; + }); + + window.showModal(); + } + + private void setAverageGrade() { + InputValueWindow window = + new InputValueWindow( + gui, + "Средний балл", + "Введите средний балл от 0.0 до 5.0:", + lastRequestedAverageGrade == null ? null : String.valueOf(lastRequestedAverageGrade), + value -> { + try { + double parsedValue = Double.parseDouble(value); + + if (parsedValue < Student.getMinAverageGrade() + || parsedValue > Student.getMaxAverageGrade()) { + + MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0."); + + return false; + } + + lastRequestedAverageGrade = parsedValue; + + studentBuilder.setAverageGrade(parsedValue); + + averageGradeLabel.setText("Средний балл: " + lastRequestedAverageGrade); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите корректное число."); + + return false; + } + }); + + window.showModal(); + } + + private void setRecordBookNumber() { + InputValueWindow window = + new InputValueWindow( + gui, + "Номер зачётной книжки", + "Введите номер зачётной книжки:", + lastRequestedRecordBookNumber == null + ? null + : String.valueOf(lastRequestedRecordBookNumber), + value -> { + try { + int parsedValue = Integer.parseInt(value); + + if (parsedValue < Student.getMinRecordBookNumber()) { + + MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным."); + + return false; + } + + lastRequestedRecordBookNumber = parsedValue; + + studentBuilder.setRecordBookNumber(parsedValue); + + recordBookNumberLabel.setText( + "Номер зачётной книжки: " + lastRequestedRecordBookNumber); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + }); + + window.showModal(); + } + + private void setObjectIndex() { + InputValueWindow window = + new InputValueWindow( + gui, + "Индекс объекта", + "Введите индекс объекта:", + lastRequestedSelectedObjectIndex == null + ? null + : String.valueOf(lastRequestedSelectedObjectIndex), + value -> { + try { + int index = Integer.parseInt(value); + + if (index < 0) { + MessageWindow.showModal(gui, "Индекс объекта не может быть отрицательным."); + return false; + } + + if (index >= preparedCustomDataCollection.size()) { + MessageWindow.showModal(gui, "Индекс должен быть меньше размера коллекции."); + return false; + } + + lastRequestedSelectedObjectIndex = index; + + indexLabel.setText("Индекс объекта: " + lastRequestedSelectedObjectIndex); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + return false; + } + }); + + window.showModal(); + } + + private void editObject() { + if (!hasStudentData()) { + MessageWindow.showModal(gui, "Сначала задайте все поля студента."); + return; + } + + if (lastRequestedSelectedObjectIndex == null) { + MessageWindow.showModal(gui, "Сначала задайте индекс объекта."); + return; + } + + if (lastRequestedSelectedObjectIndex >= preparedCustomDataCollection.size()) { + MessageWindow.showModal(gui, "Индекс объекта выходит за границы подготовленной коллекции."); + return; + } + + Student student = studentBuilder.build(); + + preparedCustomDataCollection.set(lastRequestedSelectedObjectIndex, student); + + MessageWindow.showModal(gui, "Данные объекта успешно заданы."); + } + + private void apply() { + settings.setCollection(preparedCustomDataCollection); + onApply.accept(settings); + + MessageWindow.showModal(gui, "Настройки создания коллекции вручную успешно применены."); + } + + private void preview() { + if (preparedCustomDataCollection == null || preparedCustomDataCollection.isEmpty()) { + + MessageWindow.showModal(gui, "Подготовленная коллекция пуста."); + + return; + } + + CollectionDisplayWindow window = new CollectionDisplayWindow(gui, preparedCustomDataCollection); + + window.showModal(); + } + + private boolean hasStudentData() { + return lastRequestedGroupNumber != null + && lastRequestedAverageGrade != null + && lastRequestedRecordBookNumber != null; + } +} diff --git a/app/src/main/java/ru/project/ui/creation/manual/ManualFillSettings.java b/app/src/main/java/ru/project/ui/creation/manual/ManualFillSettings.java new file mode 100644 index 0000000..ff82f46 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/manual/ManualFillSettings.java @@ -0,0 +1,22 @@ +package ru.project.ui.creation.manual; + +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +public class ManualFillSettings { + + private MyList manualDataCollection; + + public ManualFillSettings() { + this.manualDataCollection = new MyLinkedList<>(); + } + + public MyList getCollection() { + return manualDataCollection; + } + + public void setCollection(MyList collection) { + this.manualDataCollection = collection; + } +} diff --git a/app/src/main/java/ru/project/ui/creation/manual/ManualFillStrategy.java b/app/src/main/java/ru/project/ui/creation/manual/ManualFillStrategy.java new file mode 100644 index 0000000..69d47aa --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/manual/ManualFillStrategy.java @@ -0,0 +1,20 @@ +package ru.project.ui.creation.manual; + +import ru.project.collection.MyList; +import ru.project.collectors.MyCollectors; +import ru.project.student.Student; +import ru.project.ui.creation.FillStrategy; + +public class ManualFillStrategy implements FillStrategy { + + private final ManualFillSettings settings; + + public ManualFillStrategy(ManualFillSettings settings) { + this.settings = settings; + } + + @Override + public MyList create() { + return settings.getCollection().stream().collect(MyCollectors.toMyList()); + } +} diff --git a/app/src/main/java/ru/project/ui/creation/random/RandomDataSettingsWindow.java b/app/src/main/java/ru/project/ui/creation/random/RandomDataSettingsWindow.java new file mode 100644 index 0000000..5cc80c7 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/random/RandomDataSettingsWindow.java @@ -0,0 +1,361 @@ +package ru.project.ui.creation.random; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.InputValueWindow; +import ru.project.ui.common.MessageWindow; + +public class RandomDataSettingsWindow extends BaseModalWindow { + + private final RandomFillSettings settings; + private final Consumer onApply; + + private final Label collectionSizeLabel; + private final Label minGroupNumberLabel; + private final Label maxGroupNumberLabel; + private final Label minAverageGradeLabel; + private final Label maxAverageGradeLabel; + private final Label minRecordBookNumberLabel; + private final Label maxRecordBookNumberLabel; + + public RandomDataSettingsWindow(WindowBasedTextGUI gui, Consumer onApply) { + + super("Настройка случайного заполнения", gui); + + this.settings = new RandomFillSettings(); + this.onApply = onApply; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + collectionSizeLabel = + new Label("Размер коллекции: " + getValueOrDefault(settings.getCollectionSize())); + + panel.addComponent(collectionSizeLabel); + minGroupNumberLabel = + new Label("Минимальный номер группы: " + getValueOrDefault(settings.getMinGroupNumber())); + + maxGroupNumberLabel = + new Label("Максимальный номер группы: " + getValueOrDefault(settings.getMaxGroupNumber())); + + minAverageGradeLabel = + new Label("Минимальный средний балл: " + getValueOrDefault(settings.getMinAverageGrade())); + + maxAverageGradeLabel = + new Label("Максимальный средний балл: " + getValueOrDefault(settings.getMaxAverageGrade())); + + minRecordBookNumberLabel = + new Label( + "Минимальный номер зачётной книжки: " + + getValueOrDefault(settings.getMinRecordBookNumber())); + + maxRecordBookNumberLabel = + new Label( + "Максимальный номер зачётной книжки: " + + getValueOrDefault(settings.getMaxRecordBookNumber())); + + panel.addComponent(minGroupNumberLabel); + panel.addComponent(maxGroupNumberLabel); + panel.addComponent(minAverageGradeLabel); + panel.addComponent(maxAverageGradeLabel); + panel.addComponent(minRecordBookNumberLabel); + panel.addComponent(maxRecordBookNumberLabel); + + panel.addComponent(new Button("Задать размер коллекции...", this::setCollectionSize)); + + panel.addComponent(new Button("Задать минимальный номер группы", this::setMinGroupNumber)); + + panel.addComponent(new Button("Задать максимальный номер группы", this::setMaxGroupNumber)); + + panel.addComponent(new Button("Задать минимальный средний балл", this::setMinAverageGrade)); + + panel.addComponent(new Button("Задать максимальный средний балл", this::setMaxAverageGrade)); + + panel.addComponent( + new Button("Задать минимальный номер зачётной книжки", this::setMinRecordBookNumber)); + + panel.addComponent( + new Button("Задать максимальный номер зачётной книжки", this::setMaxRecordBookNumber)); + + panel.addComponent(new Button("Применить", this::apply)); + + panel.addComponent(new Button("Назад", this::close)); + + setComponent(panel); + } + + private void setCollectionSize() { + InputValueWindow window = + new InputValueWindow( + gui, + "Размер коллекции", + "Введите размер коллекции:", + settings.getCollectionSize() == null + ? null + : String.valueOf(settings.getCollectionSize()), + value -> { + try { + int collectionSize = Integer.parseInt(value); + + if (collectionSize <= 0) { + MessageWindow.showModal(gui, "Размер коллекции должен быть больше 0."); + + return false; + } + + settings.setCollectionSize(collectionSize); + + collectionSizeLabel.setText("Размер коллекции: " + collectionSize); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + }); + + window.showModal(); + } + + private void setMinGroupNumber() { + showGroupNumberInput( + "Минимальный номер группы", + settings.getMinGroupNumber(), + value -> { + if (!value.matches("[A-Z]\\d{2}")) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + if (settings.getMaxGroupNumber() != null + && value.compareTo(settings.getMaxGroupNumber()) > 0) { + + MessageWindow.showModal( + gui, "Минимальный номер группы не может быть больше максимального."); + + return false; + } + + settings.setMinGroupNumber(value); + + minGroupNumberLabel.setText("Минимальный номер группы: " + value); + + return true; + }); + } + + private void setMaxGroupNumber() { + showGroupNumberInput( + "Максимальный номер группы", + settings.getMaxGroupNumber(), + value -> { + if (!value.matches("[A-Z]\\d{2}")) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + if (settings.getMinGroupNumber() != null + && value.compareTo(settings.getMinGroupNumber()) < 0) { + + MessageWindow.showModal( + gui, "Максимальный номер группы не может быть меньше минимального."); + + return false; + } + + settings.setMaxGroupNumber(value); + + maxGroupNumberLabel.setText("Максимальный номер группы: " + value); + + return true; + }); + } + + private void showGroupNumberInput( + String title, String initialValue, java.util.function.Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, title, "Введите номер группы (например, A12):", initialValue, onSave); + + window.showModal(); + } + + private void setMinAverageGrade() { + showAverageGradeInput( + "Минимальный средний балл", + settings.getMinAverageGrade(), + value -> { + if (settings.getMaxAverageGrade() != null && value > settings.getMaxAverageGrade()) { + + MessageWindow.showModal( + gui, "Минимальный средний балл не может быть больше максимального."); + + return false; + } + + settings.setMinAverageGrade(value); + + minAverageGradeLabel.setText("Минимальный средний балл: " + value); + + return true; + }); + } + + private void setMaxAverageGrade() { + showAverageGradeInput( + "Максимальный средний балл", + settings.getMaxAverageGrade(), + value -> { + if (settings.getMinAverageGrade() != null && value < settings.getMinAverageGrade()) { + + MessageWindow.showModal( + gui, "Максимальный средний балл не может быть меньше минимального."); + + return false; + } + + settings.setMaxAverageGrade(value); + + maxAverageGradeLabel.setText("Максимальный средний балл: " + value); + + return true; + }); + } + + private void showAverageGradeInput( + String title, Double initialValue, java.util.function.Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, + title, + "Введите значение от " + "0.0 до 5.0:", + initialValue == null ? null : String.valueOf(initialValue), + value -> { + try { + double parsedValue = Double.parseDouble(value); + + if (parsedValue < ru.project.student.Student.getMinAverageGrade() + || parsedValue > ru.project.student.Student.getMaxAverageGrade()) { + + MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0."); + + return false; + } + + return onSave.test(parsedValue); + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите корректное число."); + + return false; + } + }); + + window.showModal(); + } + + private void setMinRecordBookNumber() { + showRecordBookNumberInput( + "Минимальный номер зачётной книжки", + settings.getMinRecordBookNumber(), + value -> { + if (settings.getMaxRecordBookNumber() != null + && value > settings.getMaxRecordBookNumber()) { + + MessageWindow.showModal( + gui, "Минимальный номер зачётной книжки не может быть больше максимального."); + + return false; + } + + settings.setMinRecordBookNumber(value); + + minRecordBookNumberLabel.setText("Минимальный номер зачётной книжки: " + value); + + return true; + }); + } + + private void setMaxRecordBookNumber() { + showRecordBookNumberInput( + "Максимальный номер зачётной книжки", + settings.getMaxRecordBookNumber(), + value -> { + if (settings.getMinRecordBookNumber() != null + && value < settings.getMinRecordBookNumber()) { + + MessageWindow.showModal( + gui, "Максимальный номер зачётной книжки не может быть меньше минимального."); + + return false; + } + + settings.setMaxRecordBookNumber(value); + + maxRecordBookNumberLabel.setText("Максимальный номер зачётной книжки: " + value); + + return true; + }); + } + + private void showRecordBookNumberInput( + String title, Integer initialValue, java.util.function.Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, + title, + "Введите номер зачётной книжки:", + initialValue == null ? null : String.valueOf(initialValue), + value -> { + try { + int parsedValue = Integer.parseInt(value); + + if (parsedValue < ru.project.student.Student.getMinRecordBookNumber()) { + + MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным."); + + return false; + } + + return onSave.test(parsedValue); + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + }); + + window.showModal(); + } + + private void apply() { + if (settings.getCollectionSize() == null) { + MessageWindow.showModal(gui, "Сначала задайте размер коллекции."); + + return; + } + + onApply.accept(settings); + + MessageWindow.showModal(gui, "Настройки создания коллекции случайно успешно применены."); + } + + private String getValueOrDefault(Object value) { + return value == null ? "не задан" : String.valueOf(value); + } +} diff --git a/app/src/main/java/ru/project/ui/creation/random/RandomFillSettings.java b/app/src/main/java/ru/project/ui/creation/random/RandomFillSettings.java new file mode 100644 index 0000000..22befaf --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/random/RandomFillSettings.java @@ -0,0 +1,84 @@ +package ru.project.ui.creation.random; + +public class RandomFillSettings { + + private Integer collectionSize; + + private String minGroupNumber; + private String maxGroupNumber; + + private Double minAverageGrade; + private Double maxAverageGrade; + + private Integer minRecordBookNumber; + private Integer maxRecordBookNumber; + + public RandomFillSettings() { + this.collectionSize = null; + + this.minGroupNumber = null; + this.maxGroupNumber = null; + + this.minAverageGrade = null; + this.maxAverageGrade = null; + + this.minRecordBookNumber = null; + this.maxRecordBookNumber = null; + } + + public Integer getCollectionSize() { + return collectionSize; + } + + public void setCollectionSize(Integer collectionSize) { + this.collectionSize = collectionSize; + } + + public String getMinGroupNumber() { + return minGroupNumber; + } + + public void setMinGroupNumber(String minGroupNumber) { + this.minGroupNumber = minGroupNumber; + } + + public String getMaxGroupNumber() { + return maxGroupNumber; + } + + public void setMaxGroupNumber(String maxGroupNumber) { + this.maxGroupNumber = maxGroupNumber; + } + + public Double getMinAverageGrade() { + return minAverageGrade; + } + + public void setMinAverageGrade(Double minAverageGrade) { + this.minAverageGrade = minAverageGrade; + } + + public Double getMaxAverageGrade() { + return maxAverageGrade; + } + + public void setMaxAverageGrade(Double maxAverageGrade) { + this.maxAverageGrade = maxAverageGrade; + } + + public Integer getMinRecordBookNumber() { + return minRecordBookNumber; + } + + public void setMinRecordBookNumber(Integer minRecordBookNumber) { + this.minRecordBookNumber = minRecordBookNumber; + } + + public Integer getMaxRecordBookNumber() { + return maxRecordBookNumber; + } + + public void setMaxRecordBookNumber(Integer maxRecordBookNumber) { + this.maxRecordBookNumber = maxRecordBookNumber; + } +} diff --git a/app/src/main/java/ru/project/ui/creation/random/RandomFillStrategy.java b/app/src/main/java/ru/project/ui/creation/random/RandomFillStrategy.java new file mode 100644 index 0000000..77dd072 --- /dev/null +++ b/app/src/main/java/ru/project/ui/creation/random/RandomFillStrategy.java @@ -0,0 +1,46 @@ +package ru.project.ui.creation.random; + +import ru.project.collection.MyList; +import ru.project.random.RandomStudentGenerator; +import ru.project.student.Student; +import ru.project.ui.creation.FillStrategy; + +public class RandomFillStrategy implements FillStrategy { + + private final RandomFillSettings settings; + + public RandomFillStrategy(RandomFillSettings settings) { + this.settings = settings; + } + + @Override + public MyList create() { + RandomStudentGenerator generator = new RandomStudentGenerator(); + + if (settings.getMinGroupNumber() != null) { + generator.setMinGroupNumber(settings.getMinGroupNumber()); + } + + if (settings.getMaxGroupNumber() != null) { + generator.setMaxGroupNumber(settings.getMaxGroupNumber()); + } + + if (settings.getMinAverageGrade() != null) { + generator.setMinAverageGrade(settings.getMinAverageGrade()); + } + + if (settings.getMaxAverageGrade() != null) { + generator.setMaxAverageGrade(settings.getMaxAverageGrade()); + } + + if (settings.getMinRecordBookNumber() != null) { + generator.setMinRecordBookNumber(settings.getMinRecordBookNumber()); + } + + if (settings.getMaxRecordBookNumber() != null) { + generator.setMaxRecordBookNumber(settings.getMaxRecordBookNumber()); + } + + return generator.generate(settings.getCollectionSize()); + } +} diff --git a/app/src/test/java/ru/project/AppTest.java b/app/src/test/java/ru/project/AppTest.java deleted file mode 100644 index c79d156..0000000 --- a/app/src/test/java/ru/project/AppTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.project; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -/** - * Unit test for simple App. - */ -public class AppTest { - - /** - * Rigorous Test :-) - */ - @Test - public void shouldAnswerWithTrue() { - assertTrue(true); - } -} diff --git a/app/src/test/java/ru/project/collection/MyLinkedListTest.java b/app/src/test/java/ru/project/collection/MyLinkedListTest.java new file mode 100644 index 0000000..8137964 --- /dev/null +++ b/app/src/test/java/ru/project/collection/MyLinkedListTest.java @@ -0,0 +1,138 @@ +package ru.project.collection; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class MyLinkedListTest { + @Test + public void shouldTestAdd() { + MyLinkedList list = new MyLinkedList<>(); + list.add(10); + list.add(20); + list.add(30); + + assertEquals(10, list.get(0)); + assertEquals(20, list.get(1)); + assertEquals(30, list.get(2)); + } + + @Test + public void shouldTestGet() { + MyLinkedList list = new MyLinkedList<>(); + list.add(10); + assertEquals(10, list.get(0)); + } + + @Test + public void shouldTestRemove() { + MyLinkedList list = new MyLinkedList<>(); + list.add(10); + list.add(20); + list.add(30); + list.remove(0); + + assertEquals(2, list.size()); + assertEquals(20, list.get(0)); + assertEquals(30, list.get(1)); + } + + @Test + void shouldTestSet() { + MyLinkedList list = new MyLinkedList<>(); + list.add("A"); + list.add("B"); + list.add("C"); + + String oldValue = list.set(1, "Z"); + + assertEquals("B", oldValue); + assertEquals("Z", list.get(1)); + assertEquals(3, list.size()); + } + + @Test + void shouldTestSize() { + MyLinkedList list = new MyLinkedList<>(); + list.add(1); + list.add(2); + list.add(3); + assertEquals(3, list.size()); + } + + @Test + void shouldTestisEmpty() { + MyLinkedList list = new MyLinkedList<>(); + assertTrue(list.isEmpty()); + } + + @Test + void shouldTestGet_OutOfBounds_ThrowsException() { + MyLinkedList list = new MyLinkedList<>(); + list.add(1); + + assertThrows(IllegalArgumentException.class, () -> list.get(-1)); + assertThrows(IllegalArgumentException.class, () -> list.get(1)); // index == size + assertThrows(IllegalArgumentException.class, () -> list.get(5)); // index > size + } + + @Test + void shouldTestSet_OutOfBounds_ThrowsException() { + MyLinkedList list = new MyLinkedList<>(); + list.add(1); + + assertThrows(IllegalArgumentException.class, () -> list.set(-1, 99)); + assertThrows(IllegalArgumentException.class, () -> list.set(1, 99)); + } + + @Test + void shouldTestRemove_IndexGreaterThanSize_ThrowsException() { + MyLinkedList list = new MyLinkedList<>(); + list.add("man"); + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(5)); + } + + @Test + void shouldTestRemove_NegativeIndex_ThrowsException() { + MyLinkedList list = new MyLinkedList<>(); + list.add("1"); + list.add("2"); + + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(-1)); + } + + @Test + void shouldTestaddAll_nonEmptySource_toEmptyList_addsAllElementsInOrder() { + MyList list = new MyLinkedList<>(); + MyList source = new MyLinkedList<>(); + source.add("A"); + source.add("B"); + source.add("C"); + + list.addAll(source); + + assertEquals(3, list.size()); + assertEquals("A", list.get(0)); + assertEquals("B", list.get(1)); + assertEquals("C", list.get(2)); + } + + @Test + void shouldTestaddAll_toNonEmptyList_appendsAtEnd() { + MyList list = new MyLinkedList<>(); + list.add("X"); + list.add("Y"); + + MyList source = new MyLinkedList<>(); + source.add("A"); + source.add("B"); + + list.addAll(source); + + assertEquals(4, list.size()); + assertEquals("X", list.get(0)); + assertEquals("Y", list.get(1)); + assertEquals("A", list.get(2)); + assertEquals("B", list.get(3)); + } +} diff --git a/app/src/test/java/ru/project/collectors/MyListCollectorTest.java b/app/src/test/java/ru/project/collectors/MyListCollectorTest.java new file mode 100644 index 0000000..35abcbc --- /dev/null +++ b/app/src/test/java/ru/project/collectors/MyListCollectorTest.java @@ -0,0 +1,58 @@ +package ru.project.collectors; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import ru.project.collection.MyList; + +public class MyListCollectorTest { + private final MyListCollector collector = new MyListCollector<>(); + + @Test + void shouldTestCollectInOrderSequential() { + MyList result = IntStream.range(0, 10).boxed().collect(MyCollectors.toMyList()); + + assertEquals(10, result.size()); + for (int i = 0; i < result.size(); i++) { + assertEquals(i, (int) result.get(i)); + } + } + + @Test + void shouldTestCollectInOrderParallel() { + MyList result = + IntStream.range(0, 100).parallel().boxed().collect(MyCollectors.toMyList()); + + assertEquals(100, result.size()); + for (int i = 0; i < result.size(); i++) { + assertEquals(i, (int) result.get(i)); + } + } + + @Test + void shouldTestHandleEmptyStream() { + MyList result = IntStream.empty().boxed().collect(MyCollectors.toMyList()); + + assertTrue(result.isEmpty()); + } + + @Test + void shouldTestNotUseStandardList() { + MyList result = + IntStream.range(0, 5).mapToObj(i -> "item" + i).collect(MyCollectors.toMyList()); + + assertInstanceOf(MyList.class, result); + assertFalse(result instanceof java.util.List); + } + + @Test + void shouldTestHaveCorrectCharacteristics() { + Set characteristics = collector.characteristics(); + assertTrue( + characteristics.contains(java.util.stream.Collector.Characteristics.IDENTITY_FINISH)); + assertFalse(characteristics.contains(java.util.stream.Collector.Characteristics.UNORDERED)); + } +} diff --git a/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java b/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java new file mode 100644 index 0000000..3e5c710 --- /dev/null +++ b/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java @@ -0,0 +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; + +public class StudentOccurrenceIndexFinderTest { + private StudentOccurrenceIndexFinder finder; + private MyList students; + + @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_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)); + + Student target = new Student("B23", 3.5, 102); + int[] result = finder.findOccurrences(students, target); + + 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("A12", 4.5, 101); + int[] result = finder.findOccurrences(students, target); + + assertEquals(3, result.length); + assertArrayEquals(new int[] {0, 2, 4}, result); + } + + @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, 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) { + + 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], "Результат должен быть отсортирован"); + } + } +} 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..2cc4c35 --- /dev/null +++ b/app/src/test/java/ru/project/io/StudentCsvReaderTest.java @@ -0,0 +1,116 @@ +package ru.project.io; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +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.collection.MyList; +import ru.project.collectors.MyCollectors; +import ru.project.student.Student; + +class StudentCsvReaderTest { + @TempDir Path tempDir; + + @Test + void shouldTestreadValidCsvIntoMyList() throws IOException { + Path csv = tempDir.resolve("students.csv"); + String content = + String.join( + "\n", "groupNumber,averageGrade,recordBookNumber", "M10,4.5,12345", "B05,3.8,12346"); + Files.writeString(csv, content, StandardCharsets.UTF_8); + try (var reader = new StudentCsvReader(csv.toAbsolutePath())) { + var studentStream = + StreamSupport.stream(Spliterators.spliteratorUnknownSize(reader, 0), false); + MyList 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")); + } +} 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/search/StudentSearchTest.java b/app/src/test/java/ru/project/search/StudentSearchTest.java new file mode 100644 index 0000000..2b9b756 --- /dev/null +++ b/app/src/test/java/ru/project/search/StudentSearchTest.java @@ -0,0 +1,319 @@ +package ru.project.search; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +class StudentSearchTest { + + @Test + void testGroupNumberRangeMatchesInside() { + StudentSearchCriteria criteria = new GroupNumberRangeCriteria("A10", "C30"); + Student student = new Student("B20", 4.5, 123); + assertTrue(criteria.matches(student)); + } + + @Test + void testGroupNumberRangeMatchesLowerBoundary() { + StudentSearchCriteria criteria = new GroupNumberRangeCriteria("A10", "C30"); + Student student = new Student("A10", 4.5, 123); + assertTrue(criteria.matches(student)); + } + + @Test + void testGroupNumberRangeMatchesUpperBoundary() { + StudentSearchCriteria criteria = new GroupNumberRangeCriteria("A10", "C30"); + Student student = new Student("C30", 4.5, 123); + assertTrue(criteria.matches(student)); + } + + @Test + void testGroupNumberRangeDoesNotMatchOutside() { + StudentSearchCriteria criteria = new GroupNumberRangeCriteria("A10", "C30"); + Student student = new Student("D00", 4.5, 123); + assertFalse(criteria.matches(student)); + } + + @Test + void testGroupNumberRangeInvalidMinMaxThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new GroupNumberRangeCriteria("C30", "A10")); + } + + @Test + void testGroupNumberRangeNullValuesThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new GroupNumberRangeCriteria(null, "A10")); + assertThrows(IllegalArgumentException.class, () -> new GroupNumberRangeCriteria("A10", null)); + } + + @Test + void testAverageGradeRangeMatchesInside() { + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + Student student = new Student("A12", 4.5, 123); + assertTrue(criteria.matches(student)); + } + + @Test + void testAverageGradeRangeMatchesLowerBoundary() { + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + Student student = new Student("A12", 4.0, 123); + assertTrue(criteria.matches(student)); + } + + @Test + void testAverageGradeRangeMatchesUpperBoundary() { + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + Student student = new Student("A12", 5.0, 123); + assertTrue(criteria.matches(student)); + } + + @Test + void testAverageGradeRangeDoesNotMatchBelow() { + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + Student student = new Student("A12", 3.9, 123); + assertFalse(criteria.matches(student)); + } + + @Test + void testAverageGradeRangeDoesNotMatchAbove() { + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + Student student = new Student("A12", 2.0, 123); + assertFalse(criteria.matches(student)); + } + + @Test + void testAverageGradeRangeInvalidMinMaxThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new AverageGradeRangeCriteria(5.0, 4.0)); + } + + @Test + void testRecordBookNumberRangeMatchesInside() { + StudentSearchCriteria criteria = new RecordBookNumberRangeCriteria(100, 200); + Student student = new Student("A12", 4.5, 150); + assertTrue(criteria.matches(student)); + } + + @Test + void testRecordBookNumberRangeMatchesLowerBoundary() { + StudentSearchCriteria criteria = new RecordBookNumberRangeCriteria(100, 200); + Student student = new Student("A12", 4.5, 100); + assertTrue(criteria.matches(student)); + } + + @Test + void testRecordBookNumberRangeMatchesUpperBoundary() { + StudentSearchCriteria criteria = new RecordBookNumberRangeCriteria(100, 200); + Student student = new Student("A12", 4.5, 200); + assertTrue(criteria.matches(student)); + } + + @Test + void testRecordBookNumberRangeDoesNotMatchBelow() { + StudentSearchCriteria criteria = new RecordBookNumberRangeCriteria(100, 200); + Student student = new Student("A12", 4.5, 99); + assertFalse(criteria.matches(student)); + } + + @Test + void testRecordBookNumberRangeDoesNotMatchAbove() { + StudentSearchCriteria criteria = new RecordBookNumberRangeCriteria(100, 200); + Student student = new Student("A12", 4.5, 201); + assertFalse(criteria.matches(student)); + } + + @Test + void testRecordBookNumberRangeInvalidMinMaxThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new RecordBookNumberRangeCriteria(200, 100)); + } + + @Test + void testCompositeIntersectionEmptyReturnsTrue() { + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.INTERSECTION); + Student student = new Student("A12", 4.5, 123); + assertTrue(composite.matches(student)); + } + + @Test + void testCompositeUnionEmptyReturnsFalse() { + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.UNION); + Student student = new Student("A12", 4.5, 123); + assertFalse(composite.matches(student)); + } + + @Test + void testCompositeIntersectionAllTrue() { + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.INTERSECTION); + composite.add(new GroupNumberRangeCriteria("A10", "C30")); + composite.add(new AverageGradeRangeCriteria(4.0, 5.0)); + Student student = new Student("B20", 4.5, 123); + assertTrue(composite.matches(student)); + } + + @Test + void testCompositeIntersectionOneFalse() { + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.INTERSECTION); + composite.add(new GroupNumberRangeCriteria("A10", "C30")); + composite.add(new AverageGradeRangeCriteria(4.0, 5.0)); + Student student = new Student("B20", 3.5, 123); + assertFalse(composite.matches(student)); + } + + @Test + void testCompositeUnionOneTrue() { + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.UNION); + composite.add(new GroupNumberRangeCriteria("A10", "C30")); + composite.add(new AverageGradeRangeCriteria(4.0, 5.0)); + Student student = new Student("D00", 4.5, 123); + assertTrue(composite.matches(student)); + } + + @Test + void testCompositeUnionAllFalse() { + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.UNION); + composite.add(new GroupNumberRangeCriteria("A10", "C30")); + composite.add(new AverageGradeRangeCriteria(4.0, 5.0)); + Student student = new Student("D00", 3.5, 123); + assertFalse(composite.matches(student)); + } + + @Test + void testCompositeWithNestedComposite() { + CompositeStudentSearchCriteria outer = + new CompositeStudentSearchCriteria(SearchOperation.INTERSECTION); + outer.add(new GroupNumberRangeCriteria("A10", "C30")); + + CompositeStudentSearchCriteria inner = + new CompositeStudentSearchCriteria(SearchOperation.UNION); + inner.add(new AverageGradeRangeCriteria(4.0, 5.0)); + inner.add(new RecordBookNumberRangeCriteria(100, 200)); + + outer.add(inner); + + Student student = new Student("B20", 4.5, 150); + assertTrue(outer.matches(student)); + + student = new Student("B20", 3.5, 150); + assertTrue(outer.matches(student)); + + student = new Student("D00", 4.5, 150); + assertFalse(outer.matches(student)); + } + + @Test + void testSearchServiceWithSingleCriteria() { + MyList students = new MyLinkedList<>(); + students.add(new Student("A10", 4.5, 1)); + students.add(new Student("B20", 3.8, 2)); + students.add(new Student("C30", 4.9, 3)); + + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + StudentSearchService service = new StudentSearchService(criteria); + MyList result = service.find(students); + + assertEquals(2, result.size()); + assertEquals("A10", result.get(0).getGroupNumber()); + assertEquals("C30", result.get(1).getGroupNumber()); + } + + @Test + void testSearchServiceEmptyResult() { + MyList students = new MyLinkedList<>(); + students.add(new Student("A10", 3.5, 1)); + students.add(new Student("B20", 3.8, 2)); + + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + StudentSearchService service = new StudentSearchService(criteria); + MyList result = service.find(students); + + assertEquals(0, result.size()); + } + + @Test + void testSearchServiceEmptyCollection() { + MyList students = new MyLinkedList<>(); + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + StudentSearchService service = new StudentSearchService(criteria); + MyList result = service.find(students); + assertEquals(0, result.size()); + } + + @Test + void testSearchServicePreservesOrder() { + MyList students = new MyLinkedList<>(); + students.add(new Student("Z00", 4.5, 1)); + students.add(new Student("A10", 4.5, 2)); + students.add(new Student("M30", 4.5, 3)); + + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + StudentSearchService service = new StudentSearchService(criteria); + MyList result = service.find(students); + + assertEquals(3, result.size()); + assertEquals("Z00", result.get(0).getGroupNumber()); + assertEquals("A10", result.get(1).getGroupNumber()); + assertEquals("M30", result.get(2).getGroupNumber()); + } + + @Test + void testSearchServiceDoesNotModifyOriginal() { + MyList original = new MyLinkedList<>(); + original.add(new Student("A10", 4.5, 1)); + original.add(new Student("B20", 3.8, 2)); + + MyList copy = new MyLinkedList<>(); + for (int i = 0; i < original.size(); i++) { + copy.add(original.get(i)); + } + + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + StudentSearchService service = new StudentSearchService(criteria); + service.find(original); + + assertEquals(copy.size(), original.size()); + for (int i = 0; i < copy.size(); i++) { + assertEquals(copy.get(i).getGroupNumber(), original.get(i).getGroupNumber()); + } + } + + @Test + void testSearchServiceWithNullCriteriaThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new StudentSearchService(null)); + } + + @Test + void testSearchServiceWithCompositeCriteria() { + MyList students = new MyLinkedList<>(); + students.add(new Student("A10", 4.5, 1)); + students.add(new Student("B20", 4.5, 2)); + students.add(new Student("C30", 3.5, 3)); + + CompositeStudentSearchCriteria composite = + new CompositeStudentSearchCriteria(SearchOperation.INTERSECTION); + composite.add(new GroupNumberRangeCriteria("A10", "B20")); + composite.add(new AverageGradeRangeCriteria(4.0, 5.0)); + + StudentSearchService service = new StudentSearchService(composite); + MyList result = service.find(students); + + assertEquals(2, result.size()); + assertEquals("A10", result.get(0).getGroupNumber()); + assertEquals("B20", result.get(1).getGroupNumber()); + } + + @Test + void testSearchServiceWithNullStudentListReturnsEmpty() { + StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0); + StudentSearchService service = new StudentSearchService(criteria); + MyList result = service.find(null); + assertNotNull(result); + assertEquals(0, result.size()); + } +} diff --git a/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java new file mode 100644 index 0000000..daccad5 --- /dev/null +++ b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java @@ -0,0 +1,286 @@ +package ru.project.sorting; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Comparator; +import org.junit.jupiter.api.Test; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +class EvenOnlySortStrategyTest { + + @Test + void shouldHandleEmptyList() { + MyList list = new MyLinkedList<>(); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(0, list.size()); + } + + @Test + void shouldKeepSingleEvenElementUnchanged() { + MyList list = new MyLinkedList<>(); + list.add(4); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(4, list.get(0)); + assertEquals(1, list.size()); + } + + @Test + void shouldKeepSingleOddElementUnchanged() { + MyList list = new MyLinkedList<>(); + list.add(7); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(7, list.get(0)); + assertEquals(1, list.size()); + } + + @Test + void shouldHandleDuplicateEvenValues() { + MyList list = new MyLinkedList<>(); + + list.add(8); + list.add(4); + list.add(8); + list.add(2); + list.add(4); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(2, list.get(0)); + assertEquals(4, list.get(1)); + assertEquals(4, list.get(2)); + assertEquals(8, list.get(3)); + assertEquals(8, list.get(4)); + } + + @Test + void shouldKeepOddStudentObjectsAtSameIndexes() { + Student oddFirst = new Student("A01", 4.0, 7); + Student evenFirst = new Student("A02", 4.0, 8); + Student oddSecond = new Student("A03", 4.0, 3); + Student evenSecond = new Student("A04", 4.0, 2); + Student oddThird = new Student("A05", 4.0, 5); + Student evenThird = new Student("A06", 4.0, 4); + + MyList students = new MyLinkedList<>(); + + students.add(oddFirst); + students.add(evenFirst); + students.add(oddSecond); + students.add(evenSecond); + students.add(oddThird); + students.add(evenThird); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), Student::getRecordBookNumber); + + strategy.sort(students, new StudentRecordBookComparator()); + + assertSame(oddFirst, students.get(0)); + assertSame(oddSecond, students.get(2)); + assertSame(oddThird, students.get(4)); + + assertEquals(2, students.get(1).getRecordBookNumber()); + assertEquals(4, students.get(3).getRecordBookNumber()); + assertEquals(8, students.get(5).getRecordBookNumber()); + } + + @Test + void shouldSortWhenStrategyPassedThroughConstructor() { + MyList list = new MyLinkedList<>(); + + list.add(8); + list.add(2); + list.add(4); + + EvenOnlySortStrategy strategy = new EvenOnlySortStrategy<>(new MergeSortStrategy<>()); + + strategy.setValueExtractor(value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(2, list.get(0)); + assertEquals(4, list.get(1)); + assertEquals(8, list.get(2)); + } + + @Test + void shouldSortOnlyEvenElements() { + MyList students = new MyLinkedList<>(); + + students.add(new Student("A01", 4.0, 7)); + students.add(new Student("A02", 4.0, 8)); + students.add(new Student("A03", 4.0, 3)); + students.add(new Student("A04", 4.0, 2)); + students.add(new Student("A05", 4.0, 5)); + students.add(new Student("A06", 4.0, 4)); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), Student::getRecordBookNumber); + + strategy.sort(students, new StudentRecordBookComparator()); + + assertEquals(7, students.get(0).getRecordBookNumber()); + assertEquals(2, students.get(1).getRecordBookNumber()); + assertEquals(3, students.get(2).getRecordBookNumber()); + assertEquals(4, students.get(3).getRecordBookNumber()); + assertEquals(5, students.get(4).getRecordBookNumber()); + assertEquals(8, students.get(5).getRecordBookNumber()); + + assertEquals(6, students.size()); + } + + @Test + void shouldSortAllEvenElements() { + MyList list = new MyLinkedList<>(); + + list.add(8); + list.add(2); + list.add(6); + list.add(4); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(2, list.get(0)); + assertEquals(4, list.get(1)); + assertEquals(6, list.get(2)); + assertEquals(8, list.get(3)); + } + + @Test + void shouldKeepOddElementsUnchanged() { + MyList list = new MyLinkedList<>(); + + list.add(7); + list.add(3); + list.add(5); + list.add(1); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(7, list.get(0)); + assertEquals(3, list.get(1)); + assertEquals(5, list.get(2)); + assertEquals(1, list.get(3)); + } + + @Test + void shouldThrowWhenSortStrategyIsNotSet() { + EvenOnlySortStrategy strategy = new EvenOnlySortStrategy<>(); + + strategy.setValueExtractor(value -> value); + + MyList list = new MyLinkedList<>(); + list.add(2); + list.add(4); + + assertThrows(IllegalStateException.class, () -> strategy.sort(list, Integer::compare)); + } + + @Test + void shouldRejectNullSortStrategy() { + EvenOnlySortStrategy strategy = new EvenOnlySortStrategy<>(); + + assertThrows(IllegalArgumentException.class, () -> strategy.setSortStrategy(null)); + } + + @Test + void shouldDelegateSortingToProvidedStrategy() { + MyList list = new MyLinkedList<>(); + + list.add(7); + list.add(8); + list.add(3); + list.add(2); + + RecordingSortStrategy recordingStrategy = new RecordingSortStrategy(); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(recordingStrategy, value -> value); + + strategy.sort(list, Integer::compare); + + assertTrue(recordingStrategy.wasCalled); + assertEquals(2, recordingStrategy.receivedSize); + assertEquals(8, recordingStrategy.firstReceivedElement); + assertEquals(2, recordingStrategy.secondReceivedElement); + } + + @Test + void shouldUseNewStrategyAfterReplacement() { + RecordingSortStrategy firstStrategy = new RecordingSortStrategy(); + + RecordingSortStrategy secondStrategy = new RecordingSortStrategy(); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(firstStrategy, value -> value); + + MyList firstList = new MyLinkedList<>(); + firstList.add(4); + firstList.add(2); + + strategy.sort(firstList, Integer::compare); + + assertTrue(firstStrategy.wasCalled); + + strategy.setSortStrategy(secondStrategy); + + MyList secondList = new MyLinkedList<>(); + secondList.add(8); + secondList.add(6); + + strategy.sort(secondList, Integer::compare); + + assertTrue(secondStrategy.wasCalled); + } + + private static class RecordingSortStrategy implements SortStrategy { + + private boolean wasCalled; + private int receivedSize; + private Integer firstReceivedElement; + private Integer secondReceivedElement; + + @Override + public void sort(MyList list, Comparator comparator) { + wasCalled = true; + receivedSize = list.size(); + + if (list.size() > 0) { + firstReceivedElement = list.get(0); + } + + if (list.size() > 1) { + secondReceivedElement = list.get(1); + } + } + } +} diff --git a/app/src/test/java/ru/project/sorting/SortStrategyTest.java b/app/src/test/java/ru/project/sorting/SortStrategyTest.java new file mode 100644 index 0000000..36db74c --- /dev/null +++ b/app/src/test/java/ru/project/sorting/SortStrategyTest.java @@ -0,0 +1,163 @@ +package ru.project.sorting; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Comparator; +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; + +class SortStrategyTest { + + private MyList students; + private final Comparator groupComparator = new StudentGroupComparator(); + private final Comparator gradeComparator = new StudentAverageGradeComparator(); + private final Comparator recordComparator = new StudentRecordBookComparator(); + + @BeforeEach + void setUp() { + students = new MyLinkedList<>(); + students.add(new Student("A12", 4.5, 12345)); + students.add(new Student("B32", 3.8, 12346)); + students.add(new Student("Z00", 4.9, 12347)); + students.add(new Student("A12", 4.2, 12348)); + students.add(new Student("B32", 5.0, 12349)); + } + + @Test + void testMergeSortByGroupNumber() { + SortStrategy strategy = new MergeSortStrategy<>(); + strategy.sort(students, groupComparator); + assertOrder(students, "A12", "A12", "B32", "B32", "Z00"); + } + + @Test + void testMergeSortByAverageGrade() { + SortStrategy strategy = new MergeSortStrategy<>(); + strategy.sort(students, gradeComparator); + assertOrder(students, 3.8, 4.2, 4.5, 4.9, 5.0); + } + + @Test + void testMergeSortByRecordBookNumber() { + SortStrategy strategy = new MergeSortStrategy<>(); + strategy.sort(students, recordComparator); + assertOrder(students, 12345, 12346, 12347, 12348, 12349); + } + + @Test + void testBubbleSortByGroupNumber() { + SortStrategy strategy = new BubbleSortStrategy<>(); + strategy.sort(students, groupComparator); + assertOrder(students, "A12", "A12", "B32", "B32", "Z00"); + } + + @Test + void testBubbleSortByAverageGrade() { + SortStrategy strategy = new BubbleSortStrategy<>(); + strategy.sort(students, gradeComparator); + assertOrder(students, 3.8, 4.2, 4.5, 4.9, 5.0); + } + + @Test + void testBubbleSortByRecordBookNumber() { + SortStrategy strategy = new BubbleSortStrategy<>(); + strategy.sort(students, recordComparator); + assertOrder(students, 12345, 12346, 12347, 12348, 12349); + } + + @Test + void testEvenOnlySortByRecordBookNumber() { + MyList list = new MyLinkedList<>(); + list.add(new Student("A01", 4.5, 7)); + list.add(new Student("A02", 4.5, 8)); + list.add(new Student("A03", 4.5, 3)); + list.add(new Student("A04", 4.5, 2)); + list.add(new Student("A05", 4.5, 5)); + list.add(new Student("A06", 4.5, 4)); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy(), Student::getRecordBookNumber); + + strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber)); + + int[] expected = {7, 2, 3, 4, 5, 8}; + for (int i = 0; i < list.size(); i++) { + assertEquals(expected[i], list.get(i).getRecordBookNumber()); + } + } + + @Test + void testEvenOnlyWithNoEvenElements() { + MyList list = new MyLinkedList<>(); + list.add(new Student("A01", 4.5, 1)); + list.add(new Student("A02", 4.5, 3)); + list.add(new Student("A03", 4.5, 5)); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy(), Student::getRecordBookNumber); + strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber)); + + int[] expected = {1, 3, 5}; + for (int i = 0; i < list.size(); i++) { + assertEquals(expected[i], list.get(i).getRecordBookNumber()); + } + } + + @Test + void testEvenOnlyWithAllEvenElements() { + MyList list = new MyLinkedList<>(); + list.add(new Student("A01", 4.5, 4)); + list.add(new Student("A02", 4.5, 2)); + list.add(new Student("A03", 4.5, 6)); + + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy(), Student::getRecordBookNumber); + strategy.sort(list, Comparator.comparingInt(Student::getRecordBookNumber)); + + int[] expected = {2, 4, 6}; + for (int i = 0; i < list.size(); i++) { + assertEquals(expected[i], list.get(i).getRecordBookNumber()); + } + } + + @Test + void testSortEmptyList() { + MyList empty = new MyLinkedList<>(); + SortStrategy strategy = new MergeSortStrategy<>(); + strategy.sort(empty, groupComparator); + assertEquals(0, empty.size()); + } + + @Test + void testSortSingleElement() { + MyList single = new MyLinkedList<>(); + single.add(new Student("A12", 4.5, 12345)); + SortStrategy strategy = new MergeSortStrategy<>(); + strategy.sort(single, groupComparator); + assertEquals(1, single.size()); + } + + private void assertOrder(MyList list, String... groups) { + assertEquals(groups.length, list.size()); + for (int i = 0; i < groups.length; i++) { + assertEquals(groups[i], list.get(i).getGroupNumber()); + } + } + + private void assertOrder(MyList list, double... grades) { + assertEquals(grades.length, list.size()); + for (int i = 0; i < grades.length; i++) { + assertEquals(grades[i], list.get(i).getAverageGrade(), 0.001); + } + } + + private void assertOrder(MyList list, int... records) { + assertEquals(records.length, list.size()); + for (int i = 0; i < records.length; i++) { + assertEquals(records[i], list.get(i).getRecordBookNumber()); + } + } +} diff --git a/app/src/test/java/ru/project/student/StudentTest.java b/app/src/test/java/ru/project/student/StudentTest.java new file mode 100644 index 0000000..a51e251 --- /dev/null +++ b/app/src/test/java/ru/project/student/StudentTest.java @@ -0,0 +1,141 @@ +package ru.project.student; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class StudentTest { + + @Test + void testValidStudentCreation() { + Student student = new Student("A12", 4.5, 12345); + assertEquals("A12", student.getGroupNumber()); + assertEquals(4.5, student.getAverageGrade()); + assertEquals(12345, student.getRecordBookNumber()); + } + + @Test + void testValidGroupNumbers() { + assertDoesNotThrow(() -> new Student("A12", 4.5, 1)); + assertDoesNotThrow(() -> new Student("B32", 4.5, 1)); + assertDoesNotThrow(() -> new Student("Z00", 4.5, 1)); + } + + @Test + void testInvalidGroupNumbers() { + assertThrows(IllegalArgumentException.class, () -> new Student("a12", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("A1", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("A123", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("AA1", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("а1А", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("аа!!а123", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("23фи!12", 4.5, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("№№###*()", 4.5, 1)); + } + + @Test + void testValidAverageGrades() { + assertDoesNotThrow(() -> new Student("A12", 0.0, 1)); + assertDoesNotThrow(() -> new Student("A12", 5.0, 1)); + } + + @Test + void testInvalidAverageGrades() { + assertThrows(IllegalArgumentException.class, () -> new Student("A12", -1.0, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("A12", 6.0, 1)); + } + + @Test + void testValidRecordBookNumbers() { + assertDoesNotThrow(() -> new Student("A12", 4.5, 1)); + assertDoesNotThrow(() -> new Student("A12", 4.5, 12345)); + } + + @Test + void testInvalidRecordBookNumbers() { + assertThrows(IllegalArgumentException.class, () -> new Student("A12", 4.5, 0)); + assertThrows(IllegalArgumentException.class, () -> new Student("A12", 4.5, -1)); + } + + @Test + void testWithGroupNumber() { + Student original = new Student("A12", 4.5, 12345); + Student updated = original.withGroupNumber("B32"); + + assertNotSame(original, updated); + assertEquals("B32", updated.getGroupNumber()); + assertEquals(4.5, updated.getAverageGrade()); + assertEquals(12345, updated.getRecordBookNumber()); + + assertEquals("A12", original.getGroupNumber()); + } + + @Test + void testWithAverageGrade() { + Student original = new Student("A12", 4.5, 12345); + Student updated = original.withAverageGrade(4.8); + + assertNotSame(original, updated); + assertEquals(4.8, updated.getAverageGrade()); + assertEquals("A12", updated.getGroupNumber()); + assertEquals(12345, updated.getRecordBookNumber()); + + assertEquals(4.5, original.getAverageGrade()); + } + + @Test + void testWithRecordBookNumber() { + Student original = new Student("A12", 4.5, 12345); + Student updated = original.withRecordBookNumber(99999); + + assertNotSame(original, updated); + assertEquals(99999, updated.getRecordBookNumber()); + assertEquals("A12", updated.getGroupNumber()); + assertEquals(4.5, updated.getAverageGrade()); + + assertEquals(12345, original.getRecordBookNumber()); + } + + @Test + void testWithInvalidGroupNumber() { + Student original = new Student("A12", 4.5, 12345); + assertThrows(IllegalArgumentException.class, () -> original.withGroupNumber("a12")); + assertEquals("A12", original.getGroupNumber()); // не изменился + } + + @Test + void testWithInvalidAverageGrade() { + Student original = new Student("A12", 4.5, 12345); + assertThrows(IllegalArgumentException.class, () -> original.withAverageGrade(6.0)); + assertEquals(4.5, original.getAverageGrade()); + } + + @Test + void testWithInvalidRecordBookNumber() { + Student original = new Student("A12", 4.5, 12345); + assertThrows(IllegalArgumentException.class, () -> original.withRecordBookNumber(0)); + assertEquals(12345, original.getRecordBookNumber()); + } + + @Test + void testEqualsAndHashCode() { + Student s1 = new Student("A12", 4.5, 12345); + Student s2 = new Student("A12", 4.5, 12345); + Student s3 = new Student("B32", 4.5, 12345); + Student s4 = new Student("A12", 4.8, 12345); + Student s5 = new Student("A12", 4.5, 99999); + + assertEquals(s1, s2); + assertEquals(s1.hashCode(), s2.hashCode()); + + assertNotEquals(s1, s3); + assertNotEquals(s1, s4); + assertNotEquals(s1, s5); + assertNotEquals(s1, null); + assertNotEquals(s1, "some string"); + } +} diff --git a/docs/MENU.ru.txt b/docs/MENU.ru.txt new file mode 100644 index 0000000..0be1048 --- /dev/null +++ b/docs/MENU.ru.txt @@ -0,0 +1,130 @@ +Главное меню +{ + Показать текущую коллекцию + + Меню создания новой коллекции + { + Вернуться + + Задать размер + + Настроить способ заполнения + { + Пользовательские данные + { + Задать поле 'Название A' + Задать поле 'Название B' + Задать поле 'Название C' + Задать индекс изменяемого объекта + Изменить данные объекта + Предпросмотр + } + + Случайные данные + { + Настройка диапазона генерации + { + Поле 'Название A' + { + Задать минимальное значение + Задать максимальное значение + } + + Поле 'Название B' + { + Задать минимальное значение + Задать максимальное значение + } + + Поле 'Название C' + { + Задать минимальное значение + Задать максимальное значение + } + } + } + + Данные из файла + { + Задать путь до файла + { + Абсолютный + Относительный + } + } + } + + Выбрать способ заполнения + { + Пользовательские данные * + Случайные данные + Данные из файла + } + + Создать новую коллекцию + } + + Меню сохранения текущей коллекции в файл + { + Вывести текущий абсолютный путь до файла + + Задать путь до файла + { + Абсолютный + Относительный + } + + Дописать коллекцию в текущий файл + } + + Меню сортировки текущей коллекции + { + Вернуться + + Настроить способ сортировки + { + Обычная сортировка + { + Сортировать по полю 'Название A' + Сортировать по полю 'Название B' + Сортировать по полю 'Название C' + } + } + + Настроить способ сортировки + { + Сортировка по чётности + { + Выбрать числовое поле 'Название A' + Выбрать числовое поле 'Название B' + } + } + + Выбрать способ сортировки + { + Обычная сортировка * + Сортировка по чётности + } + + Сортировать текущую коллекцию + } + + Меню поиска элемента в текущей коллекции + { + Вернуться + Задать значение поля 'Название A' + Задать значение поля 'Название B' + Задать значение поля 'Название C' + Найти первое вхождение в текущей коллекции + } + + Меню подсчёта количества вхождений элемента + { + Задать значение поля 'Название A' + Задать значение поля 'Название B' + Задать значение поля 'Название C' + Выполнить многопоточный подсчёт + } + + Завершить выполнение программы +} 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`. Файл создавать запрещено. --- diff --git a/docs/tasks/RandomStudentGenerator.md b/docs/tasks/RandomStudentGenerator.md new file mode 100644 index 0000000..3e902f9 --- /dev/null +++ b/docs/tasks/RandomStudentGenerator.md @@ -0,0 +1,305 @@ +# Техническое задание: генератор случайных студентов + +## Задача + +Реализовать класс для формирования `MyList` со случайными значениями. + +По умолчанию ограничения диапазона для генерации не задаются. В таком случае значения должны генерироваться во всём допустимом диапазоне, установленном правилами класса `Student`. + +Генерация объектов должна выполняться через существующий `StudentBuilder`. + +## Название класса + +```text id="m9s0q1" +RandomStudentGenerator +``` + +## Контракт класса + +Класс должен хранить текущие ограничения генерации для каждого поля `Student`. + +Для каждого поля необходимо предоставить по два метода: левое и правое ограничение. + +### `groupNumber` + +Тип поля: `String`. + +```java id="t2z8k4" +RandomStudentGenerator setMinGroupNumber(String minGroupNumber); +``` + +```java id="q7x5n1" +RandomStudentGenerator setMaxGroupNumber(String maxGroupNumber); +``` + +Ограничения применяются при генерации `groupNumber`. + +Сравнение границ выполнять в соответствии с правилами, используемыми для `groupNumber` в `Student`. + +### `averageGrade` + +Тип поля: `double`. + +```java id="v4n2j8" +RandomStudentGenerator setMinAverageGrade(double minAverageGrade); +``` + +```java id="r5c1p9" +RandomStudentGenerator setMaxAverageGrade(double maxAverageGrade); +``` + +Диапазон должен быть включительным: + +```text id="u3k7w2" +minAverageGrade <= averageGrade <= maxAverageGrade +``` + +### `recordBookNumber` + +Тип поля: `int`. + +```java id="d8m4x6" +RandomStudentGenerator setMinRecordBookNumber(int minRecordBookNumber); +``` + +```java id="e1q9s5" +RandomStudentGenerator setMaxRecordBookNumber(int maxRecordBookNumber); +``` + +Диапазон должен быть включительным: + +```text id="y6p2v8" +minRecordBookNumber <= recordBookNumber <= maxRecordBookNumber +``` + +## Метод генерации + +Основной метод: + +```java id="k3w7m1" +MyList generate(int size); +``` + +Контракт: + +- создаёт ровно `size` объектов `Student`; +- все объекты должны быть валидными; +- каждый объект создаётся через `StudentBuilder`; +- результат возвращается как `MyList`; +- стандартные `List`, `ArrayList`, `LinkedList` и другие контейнеры использовать нельзя. + +## Поведение диапазонов по умолчанию + +Если для поля ограничение не задано, используется весь допустимый диапазон этого поля. + +Необходимо учитывать существующие ограничения `Student`, в частности: + +```text id="z4r8t2" +averageGrade: +getMinAverageGrade() +getMaxAverageGrade() + +recordBookNumber: +getMinRecordBookNumber() +``` + +Для `groupNumber` использовать существующий шаблон: + +```text id="n5c7y3" +getGroupNumberPattern() +``` + +Генерируемое значение `groupNumber` должно соответствовать требованиям `Student`. + +## Проверка ограничений + +При установке границ необходимо проверять их корректность. + +Недопустимые случаи: + +```text id="h2k6p4" +min > max +``` + +Для них выбрасывать: + +```java id="c7v3n9" +IllegalArgumentException +``` + +Ограничение также не должно выходить за допустимые значения `Student`. + +## Рекомендуемая реализация + +Класс должен хранить шесть ограничений: + +```text id="b5q9x2" +minGroupNumber +maxGroupNumber + +minAverageGrade +maxAverageGrade + +minRecordBookNumber +maxRecordBookNumber +``` + +Для каждого поля значения генерируются независимо. + +Рекомендуется использовать стандартный генератор случайных чисел Java, например: + +```java id="a8f4k1" +Random +``` + +или: + +```java id="p3m7d5" +ThreadLocalRandom +``` + +При создании `Student`: + +```text id="w6j2r9" +сгенерировать поля + ↓ +StudentBuilder + ↓ +build() + ↓ +Student + ↓ +MyList +``` + +Дополнительную валидацию, дублирующую правила `Student`, в генераторе не реализовывать. + +## Примеры использования + +Без настройки диапазонов: + +```java id="s2k8m4" +RandomStudentGenerator generator = + new RandomStudentGenerator(); + +MyList students = + generator.generate(100); +``` + +С настройкой диапазонов: + +```java id="f7q3n1" +RandomStudentGenerator generator = + new RandomStudentGenerator() + .setMinGroupNumber("A00") + .setMaxGroupNumber("Z99") + .setMinAverageGrade(3.0) + .setMaxAverageGrade(5.0) + .setMinRecordBookNumber(10000) + .setMaxRecordBookNumber(99999); + +MyList students = + generator.generate(100); +``` + +## Тесты + +### Генерация без ограничений + +```java id="r1w5p8" +MyList result = generator.generate(100); +``` + +Проверить: + +- размер равен `100`; +- все элементы не равны `null`; +- каждый `Student` валиден. + +### Ограничение `averageGrade` + +Задать: + +```text id="x4n7k2" +3.0 — 4.0 +``` + +Проверить, что все значения находятся в указанном диапазоне. + +### Ограничение `recordBookNumber` + +Задать: + +```text id="q9m3v6" +10000 — 20000 +``` + +Проверить диапазон и включение границ. + +### Ограничение `groupNumber` + +Задать левую и правую границы. + +Проверить, что все сгенерированные значения находятся между ними согласно правилам сравнения `groupNumber`. + +### Граничные значения + +Установить: + +```text id="p8c4s1" +min == max +``` + +Проверить, что все сгенерированные значения равны указанной границе. + +### Некорректный диапазон + +Проверить выброс `IllegalArgumentException`, если: + +```text id="z6t2h5" +min > max +``` + +### Некорректное ограничение + +Передать диапазон, выходящий за допустимые значения `Student`. + +Проверить выброс исключения. + +### Генерация пустой коллекции + +Для: + +```java id="j5r8w3" +generate(0) +``` + +ожидается пустой `MyList`. + +### Некорректный размер + +Для отрицательного размера: + +```java id="n2k7p4" +generate(-1) +``` + +ожидается `IllegalArgumentException`. + +## Результат + +Необходимо реализовать: + +```text id="v8m3q6" +RandomStudentGenerator.java +``` + +Класс должен использовать существующие: + +```text id="f1j9x4" +Student.java +StudentBuilder.java +MyList.java +``` + +Изменять интерфейсы `Student`, `StudentBuilder` и `MyList` для выполнения данного задания не требуется. diff --git a/GIT_BRANCH_DEPENDENCIES.ru.md b/tutorials/git/GIT_BRANCH_DEPENDENCIES.ru.md similarity index 100% rename from GIT_BRANCH_DEPENDENCIES.ru.md rename to tutorials/git/GIT_BRANCH_DEPENDENCIES.ru.md diff --git a/GIT_CONFIG.ru.md b/tutorials/git/GIT_CONFIG.ru.md similarity index 100% rename from GIT_CONFIG.ru.md rename to tutorials/git/GIT_CONFIG.ru.md diff --git a/tutorials/git/GIT_FLOW.ru.md b/tutorials/git/GIT_FLOW.ru.md new file mode 100644 index 0000000..02f6063 --- /dev/null +++ b/tutorials/git/GIT_FLOW.ru.md @@ -0,0 +1,1201 @@ +# Git Flow: правила разработки в команде + +## Назначение + +Этот документ описывает принятый в команде порядок работы с Git Flow: + +- какие ветки используются; +- от каких веток создавать новые; +- куда вливать изменения; +- как работать с `feature`, `release`, `hotfix`; +- как работать с зависимыми feature; +- как синхронизировать локальные ветки; +- как безопасно публиковать изменения; +- что делать через Pull Request. + +Цель — сделать историю изменений предсказуемой и избежать ситуаций, когда изменения случайно попадают не в ту ветку. + +--- + +## 1. Основная модель веток + +В классическом Git Flow используются две основные долгоживущие ветки: + +- `main` — код, находящийся в production; +- `develop` — основная ветка текущей разработки. + +Временные ветки: + +- `feature/*` — разработка новой функциональности; +- `release/*` — подготовка релиза; +- `hotfix/*` — срочное исправление production; +- `bugfix/*` — исправление ошибок в процессе разработки; +- `support/*` — поддержка старых версий, если это необходимо проекту. + +Базовая схема: + +```text + feature/* + / \ + / \ + / \ +main ─────────────────●──────────────●───────────── + ↑ ↑ + │ │ + release hotfix + │ │ + └──────┐ ┌─────┘ + ↓ ↓ +develop ────────────────●────●─●────────────────── + ↑ + │ + feature +``` + +Главное правило: + +```text +feature/* → develop +release/* → main +release/* → develop +hotfix/* → main +hotfix/* → develop +``` + +--- + +# 2. Что является источником для каждой ветки + +## Feature + +Feature создаётся от: + +```text +develop → feature/* +``` + +Пример: + +```bash +git flow feature start user-profile +``` + +Получается: + +```text +develop + \ + feature/user-profile +``` + +Новые feature по умолчанию **не создаются от `main`**. + +--- + +## Release + +Release создаётся от: + +```text +develop → release/* +``` + +Пример: + +```bash +git flow release start 2.4.0 +``` + +Получается: + +```text +develop + \ + release/2.4.0 +``` + +Release используется для подготовки уже готового набора изменений к production. + +В release обычно не добавляют новые большие функциональные возможности. + +--- + +## Hotfix + +Hotfix создаётся от: + +```text +main → hotfix/* +``` + +Пример: + +```bash +git flow hotfix start 2.4.1 +``` + +Используйте `hotfix`, когда проблема уже находится в production и исправление нельзя ждать до следующего обычного релиза. + +--- + +## Bugfix + +`bugfix` используется для исправления ошибок в процессе разработки. + +В зависимости от конфигурации `git-flow` такая ветка обычно является веткой разработки и должна в конечном итоге попасть в `develop`. + +Пример: + +```bash +git flow bugfix start fix-registration +``` + +--- + +# 3. Основной рабочий цикл Feature + +Обычная разработка выглядит так: + +```text +develop + │ + ├── feature/login + │ + ├── feature/payment + │ + └── feature/profile + │ + └────────→ develop +``` + +## Шаг 1. Обновить develop + +Перед началом работы необходимо синхронизировать локальный `develop`: + +```bash +git checkout develop +git pull origin develop +``` + +## Шаг 2. Создать feature + +```bash +git flow feature start user-profile +``` + +## Шаг 3. Работать и делать коммиты + +```bash +git add . +git commit -m "feat: add user profile" +``` + +Коммиты должны быть небольшими и логически связанными. + +## Шаг 4. Опубликовать feature + +Если работа ведётся через remote: + +```bash +git flow feature publish user-profile +``` + +или: + +```bash +git push -u origin feature/user-profile +``` + +## Шаг 5. Создать Pull Request + +Из: + +```text +feature/user-profile +``` + +в: + +```text +develop +``` + +После ревью и успешного прохождения CI изменения можно вливать в `develop`. + +--- + +# 4. Как вливать Feature в develop + +Есть два допустимых варианта. + +## Вариант A — через Pull Request + +Для командной разработки это предпочтительный способ: + +```text +feature/user-profile + │ + │ Pull Request + ↓ + develop +``` + +Плюсы: + +- code review; +- автоматические проверки; +- прозрачная история; +- возможность обсуждать изменения до merge. + +--- + +## Вариант B — через git-flow локально + +Можно использовать: + +```bash +git flow feature finish user-profile +``` + +Эта команда завершает feature и вливает её в `develop`. + +После этого изменения нужно отправить в remote: + +```bash +git push origin develop +``` + +Если проект использует обязательный Pull Request, локальный `feature finish` обычно не используется для финального merge. + +--- + +# 5. Важно: не все Feature должны зависеть друг от друга + +Предпочтительная структура: + +```text +develop + ├── feature/A + ├── feature/B + └── feature/C +``` + +Нежелательная структура: + +```text +develop + └── feature/A + └── feature/B + └── feature/C +``` + +Каждая feature по возможности должна начинаться от `develop`. + +--- + +# 6. Что делать, если Feature зависит от другой Feature + +Иногда `feature/B` действительно требует незавершённых изменений из `feature/A`. + +Например: + +```text +develop + \ + feature/A + \ + feature/B +``` + +В этом случае временно допустимо создать `feature/B` от `feature/A`. + +Пример: + +```bash +git checkout feature/A +git checkout -b feature/B +``` + +Но нужно понимать, что теперь B зависит от A. + +После завершения A желательно вернуть B на обычную базу: + +```text +develop + ├── feature/A + └── feature/B +``` + +Например, после merge A в `develop`: + +```bash +git checkout feature/B +git rebase develop +``` + +или выполнить merge: + +```bash +git checkout feature/B +git merge develop +``` + +Выбор между `rebase` и `merge` зависит от правил проекта. + +Главное правило: + +> Зависимая feature — временное исключение, а не стандартная структура проекта. + +Подробнее о зависимостях см. `GIT_BRANCH_DEPENDENCIES.ru.md`. + +--- + +# 7. Как обновлять Feature от develop + +Пока feature разрабатывается, в `develop` могут появляться новые изменения. + +Периодически feature необходимо синхронизировать. + +### Вариант с rebase + +```bash +git checkout feature/user-profile +git fetch origin +git rebase origin/develop +``` + +### Вариант с merge + +```bash +git checkout feature/user-profile +git fetch origin +git merge origin/develop +``` + +Необходимо использовать тот вариант, который принят в правилах конкретного репозитория. + +Если используется `rebase` для уже опубликованной ветки, после него может понадобиться: + +```bash +git push --force-with-lease +``` + +Использовать: + +```bash +git push --force +``` + +не рекомендуется. + +Подробнее о безопасном использовании `--force-with-lease` см. в: + +`GIT_FORCE_WITH_LEASE_RECOVERY.ru.md` + +--- + +# 8. Release Flow + +Когда набор изменений готов к релизу: + +```text +develop + │ + └──→ release/2.4.0 +``` + +Создание: + +```bash +git flow release start 2.4.0 +``` + +Далее выполняется: + +- финальное тестирование; +- исправление найденных ошибок; +- обновление версии; +- подготовка release notes; +- подготовка production-конфигурации. + +Когда release готов: + +```text +release/2.4.0 → main +release/2.4.0 → develop +``` + +То есть результат релиза должен оказаться одновременно: + +```text +main +``` + +и + +```text +develop +``` + +Пример: + +```bash +git flow release finish 2.4.0 +``` + +После чего изменения необходимо отправить в remote: + +```bash +git push origin main +git push origin develop +git push origin --tags +``` + +Конкретная последовательность зависит от используемого процесса CI/CD. + +--- + +# 9. Почему release вливается и в main, и в develop + +Предположим: + +```text +develop + \ + release/2.4.0 +``` + +Во время подготовки релиза нашли bug и исправили его в release. + +Если отправить изменения только в `main`: + +```text +release → main +``` + +то исправление может не попасть обратно в будущую разработку. + +Поэтому используется: + +```text +release → main +release → develop +``` + +Таким образом, `develop` не теряет исправления, сделанные во время подготовки релиза. + +--- + +# 10. Hotfix Flow + +Если production сломан и исправление требуется срочно: + +```text +main + \ + hotfix/2.4.1 +``` + +Создание: + +```bash +git flow hotfix start 2.4.1 +``` + +После исправления hotfix должен попасть: + +```text +hotfix/2.4.1 → main +hotfix/2.4.1 → develop +``` + +Пример: + +```bash +git flow hotfix finish 2.4.1 +``` + +После этого изменения публикуются: + +```bash +git push origin main +git push origin develop +git push origin --tags +``` + +--- + +# 11. Bugfix Flow + +Если ошибка обнаружена во время разработки и не требует отдельного production hotfix: + +```text +develop + \ + bugfix/registration +``` + +Создание: + +```bash +git flow bugfix start registration +``` + +После завершения: + +```text +bugfix/registration → develop +``` + +Использование `bugfix` или обычного `feature` для исправления ошибки определяется соглашениями проекта. + +Главное — не создавать production hotfix от `develop`. + +--- + +# 12. Support Flow + +`support/*` нужен только тогда, когда проект действительно поддерживает старые версии. + +Например: + +```text +main + ├── 3.0 + └── support/2.x +``` + +Такие ветки позволяют поддерживать старую версию отдельно от основной разработки. + +Использование `support/*` должно быть явно согласовано командой. + +Если проект не требует поддержки нескольких версий одновременно, `support` лучше не использовать без необходимости. + +--- + +# 13. Публикация веток + +Для feature: + +```bash +git flow feature publish +``` + +Для остальных типов веток используются соответствующие `publish`/`push` команды согласно конфигурации `git-flow`. + +Перед публикацией необходимо убедиться, что ветка имеет правильное имя и базовую ветку. + +Проверить ветки можно: + +```bash +git branch +git branch -r +``` + +Полезно также посмотреть историю: + +```bash +git log --all --graph --decorate --oneline +``` + +Это один из самых удобных способов визуально проверить, куда действительно идут изменения. + +--- + +# 14. От какого типа ветки что создаём + +| Ветка | Создаётся от | Основное назначение | Куда вливается | +|---|---|---|---| +| `feature/*` | `develop` | Новая функциональность | `develop` | +| `bugfix/*` | `develop` | Исправление ошибки в разработке | `develop` | +| `release/*` | `develop` | Подготовка релиза | `main` и `develop` | +| `hotfix/*` | `main` | Срочное production-исправление | `main` и `develop` | +| `support/*` | зависит от процесса | Поддержка старой версии | зависит от процесса | + +--- + +# 15. Что не следует делать + +## Не создавать Feature от main + +Плохо: + +```text +main + \ + feature/new-payment +``` + +Правильно: + +```text +develop + \ + feature/new-payment +``` + +--- + +## Не вливать Feature напрямую в main + +Обычно: + +```text +feature → develop → release → main +``` + +а не: + +```text +feature → main +``` + +--- + +## Не создавать Hotfix от develop + +Плохо: + +```text +develop → hotfix +``` + +Правильно: + +```text +main → hotfix +``` + +--- + +## Не держать Feature зависимой от другой Feature без необходимости + +Плохо: + +```text +feature/A + \ + feature/B + \ + feature/C +``` + +Лучше: + +```text +develop + ├── feature/A + ├── feature/B + └── feature/C +``` + +--- + +## Не выполнять force push без необходимости + +Опасно: + +```bash +git push --force +``` + +Предпочтительно: + +```bash +git push --force-with-lease +``` + +при условии, что force push разрешён правилами репозитория. + +--- + +# 16. Рекомендуемый командный процесс + +Для обычной задачи: + +```text +1. develop + ↓ +2. feature/* + ↓ +3. commits + ↓ +4. push + ↓ +5. Pull Request + ↓ +6. review + CI + ↓ +7. develop + ↓ +8. release/* + ↓ +9. main +``` + +В виде схемы: + +```text + Pull Request +feature/login ─────────────────────────→ develop + │ + │ release + ↓ + release/2.4.0 + │ + ┌──────────┴──────────┐ + ↓ ↓ + main develop +``` + +--- + +# 17. Правила Pull Request + +Для командной работы рекомендуется: + +1. Один PR — одна логическая задача. +2. Целевая ветка PR должна быть заранее определена. +3. PR должен проходить CI до merge. +4. Автор PR не должен самостоятельно обходить обязательные проверки. +5. Конфликты необходимо разрешать в своей рабочей ветке, а не в `develop`/`main`. +6. Не следует смешивать в одном PR feature, рефакторинг и несвязанные исправления. +7. После изменения истории feature-ветки необходимо убедиться, что PR всё ещё содержит только нужные изменения. + +Рекомендуемая схема: + +```text +feature/* → Pull Request → develop +release/* → Pull Request → main +hotfix/* → Pull Request → main +``` + +Если политика проекта допускает отдельный merge release/hotfix в `develop`, это также должно быть явно зафиксировано в правилах репозитория. + +--- + +# 18. Конвенция именования веток + +Название ветки должно быть коротким и отражать задачу. + +Примеры: + +```text +feature/user-profile +feature/payment-api +feature/checkout-flow + +bugfix/registration-error +bugfix/incorrect-total + +release/2.4.0 + +hotfix/2.4.1 + +support/2.x +``` + +Не рекомендуется: + +```text +feature/test +feature/new +feature/fix +feature/ivan +feature/123 +``` + +Если в проекте используется трекер задач, предпочтительно включать идентификатор задачи: + +```text +feature/PROJ-123-user-profile +bugfix/PROJ-456-invalid-total +``` + +Точная схема именования должна быть одинаковой для всей команды. + +--- + +# 19. Коммиты + +Коммиты должны быть: + +- небольшими; +- законченными; +- логически связанными; +- понятными без чтения всего diff. + +Плохо: + +```bash +git commit -m "fix" +git commit -m "changes" +git commit -m "test" +``` + +Лучше: + +```bash +git commit -m "feat: add user profile endpoint" +git commit -m "fix: validate empty email" +git commit -m "test: cover invalid payment status" +``` + +Если проект использует Conventional Commits, придерживаемся его правил: + +```text +feat: +fix: +refactor: +test: +docs: +chore: +build: +ci: +``` + +--- + +# 20. Синхронизация перед началом работы + +Перед созданием новой feature: + +```bash +git fetch origin +git checkout develop +git pull --ff-only origin develop +``` + +После этого: + +```bash +git flow feature start my-feature +``` + +Для `hotfix`: + +```bash +git fetch origin +git checkout main +git pull --ff-only origin main +git flow hotfix start 2.4.1 +``` + +Использование `--ff-only` помогает не создавать случайные локальные merge-коммиты во время обычного обновления базовой ветки. + +--- + +# 21. Что делать при конфликтах + +Если конфликт возник при `rebase`: + +```bash +git status +``` + +Исправить конфликтующие файлы, затем: + +```bash +git add +git rebase --continue +``` + +Чтобы полностью отменить rebase: + +```bash +git rebase --abort +``` + +Если конфликт возник при merge: + +```bash +git status +``` + +После исправления: + +```bash +git add +git commit +``` + +Если merge необходимо отменить: + +```bash +git merge --abort +``` + +Не следует разрешать конфликты вслепую. После разрешения обязательно проверить diff и тесты. + +--- + +# 22. Удаление завершённых веток + +После merge завершённая feature обычно больше не нужна. + +Локальную ветку можно удалить: + +```bash +git branch -d feature/my-feature +``` + +Удалённую ветку: + +```bash +git push origin --delete feature/my-feature +``` + +Если репозиторий автоматически удаляет ветку после merge, вручную удалять её не нужно. + +Не удаляйте ветку до тех пор, пока не убедились, что нужные изменения действительно попали в целевую ветку. + +--- + +# 23. Как проверить поток коммитов + +Для просмотра всех локальных и remote веток: + +```bash +git log --all --graph --decorate --oneline --date-order +``` + +Полезно также: + +```bash +git branch -a +``` + +и: + +```bash +git show-branch --all +``` + +Пример графа: + +```text +* abc1234 (main) Merge release/2.4.0 +|\ +| * def5678 (release/2.4.0) Prepare release 2.4.0 +| * 123abcd Fix release configuration +|/ +* 456efgh (develop) Merge feature/payment +|\ +| * 789ijkl (feature/payment) Add payment service +| * 012mnop Add payment validation +|/ +* 345qrst Previous release +``` + +Такой граф позволяет быстро проверить: + +- от какой ветки была создана ветка; +- какие изменения в неё попали; +- где находится merge; +- не ушла ли feature в неправильную ветку. + +--- + +# 24. Частые ошибки + +## Feature создана от старого develop + +Симптом: + +```text +feature + \ + старый develop +``` + +Решение — сначала синхронизировать feature с актуальным `develop`. + +--- + +## Feature случайно создана от main + +Не продолжайте разработку, не проверив ситуацию. + +Сначала нужно убедиться, какие изменения уже появились в feature, и только после этого выбрать способ переноса на актуальный `develop`. + +В зависимости от ситуации это может быть: + +```bash +git rebase develop +``` + +или пересоздание ветки от правильной базы. + +--- + +## В hotfix попали незавершённые изменения из develop + +Это обычно означает, что hotfix создавался не от актуального `main`. + +Hotfix должен базироваться на том состоянии, которое реально находится в production. + +--- + +## Исправление сделано только в release + +Если release содержит важный bugfix, он должен попасть и в `main`, и обратно в `develop`. + +Нельзя оставлять важные исправления только внутри временной release-ветки. + +--- + +## Два разработчика одновременно переписали историю одной feature + +Не следует выполнять `rebase`/force push общей ветки без согласования. + +Для опубликованных веток безопаснее: + +```bash +git push --force-with-lease +``` + +Но даже `--force-with-lease` не отменяет необходимость договориться с остальными участниками. + +Подробнее: `GIT_FORCE_WITH_LEASE_RECOVERY.ru.md`. + +--- + +# 25. Полная схема Git Flow + +```text + feature/A + / + / + ● + / + / +develop ────────────────●────────●───────────────●──────── + \ / + \ / + \ / + release/2.4.0 + │ + ┌────────┴────────┐ + ↓ ↓ + main develop + │ + │ + └── hotfix/2.4.1 + │ + ┌────────┴────────┐ + ↓ ↓ + main develop +``` + +Для обычной разработки поток выглядит так: + +```text +develop + │ + ├── feature/A ──────────────┐ + │ ↓ + ├── feature/B ─────────→ develop + │ + └── feature/C ──────────────┘ + │ + ↓ + release/2.4.0 + │ + ┌─────┴─────┐ + ↓ ↓ + main develop +``` + +--- + +# 26. Краткая памятка + +Запоминаем: + +```text +feature ← develop +feature → develop + +bugfix ← develop +bugfix → develop + +release ← develop +release → main +release → develop + +hotfix ← main +hotfix → main +hotfix → develop +``` + +Основные команды: + +```bash +# Feature +git flow feature start +git flow feature publish +git flow feature finish + +# Bugfix +git flow bugfix start +git flow bugfix finish + +# Release +git flow release start +git flow release finish + +# Hotfix +git flow hotfix start +git flow hotfix finish + +# Support +git flow support start +``` + +Для просмотра графа: + +```bash +git log --all --graph --decorate --oneline --date-order +``` + +--- + +# 27. Наше командное правило + +Перед каждой операцией с ветками нужно ответить на три вопроса: + +**1. От какой ветки я создаю текущую ветку?** + +**2. В какую ветку должны попасть мои изменения?** + +**3. Не содержит ли моя ветка изменений, которые не должны попасть в целевую ветку?** + +Если на эти три вопроса есть однозначный ответ, операция с Git почти всегда становится предсказуемой. + +--- + +# 28. Итог + +Нормальный Git Flow в команде выглядит так: + +```text + feature + ↗ +develop ───────────────●─────────────── + │ + │ release + ↓ + main + │ + │ hotfix + ↓ + develop +``` + +Ключевая идея проста: + +> `develop` — источник обычной разработки, `main` — источник production-правок. Feature идут в `develop`, release — в `main` и обратно в `develop`, hotfix — из `main` в `main` и обратно в `develop`. + +Git Flow — это не набор обязательных магических команд. Это договорённость команды о том, **где рождаются изменения, через какие этапы проходят и в какую ветку попадают**. + +Перед merge всегда проверяем целевую ветку, актуальность базы, CI и содержимое diff. diff --git a/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md b/tutorials/git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md similarity index 100% rename from GIT_FORCE_WITH_LEASE_RECOVERY.ru.md rename to tutorials/git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md