From bed49120e7ff6833300d23e3ef78f1b27a36b3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 21 Aug 2026 18:28:43 +0400 Subject: [PATCH 001/110] docs: add user menu documentation --- docs/MENU.ru.txt | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/MENU.ru.txt diff --git a/docs/MENU.ru.txt b/docs/MENU.ru.txt new file mode 100644 index 0000000..2c49d7b --- /dev/null +++ b/docs/MENU.ru.txt @@ -0,0 +1,29 @@ +ГЛАВНОЕ МЕНЮ + +1. Создать и вывести коллекцию + 1.1. Выбрать размер коллекции + 1.2. Выбрать способ заполнения + 1.2.1. Вручную + 1.2.1.1. Задать новый элемент + 1.2.1.2. Завершить создание коллекции + 1.2.2. Случайными значениями + 1.2.3. Из файла + 1.2.3.1. Указать абсолютный путь до файла + +2. Настроить и выполнить сортировку + 2.1. Выбрать тип сортировки + 2.1.1. Обычная сортировка + 2.1.1.1. Выбрать поле 1 + 2.1.1.2. Выбрать поле 2 + 2.1.1.3. Выбрать поле 3 + 2.1.2. Сортировка по чётности + 2.1.2.1. Выбрать числовое поле + 2.2. Выполнить сортировку и вывести результат + 2.3. Дописать в файл последний результат сортировки + +3. Подсчитать количество вхождений элемента + 3.1. Ввести элемент N + 3.2. Выполнить многопоточный подсчёт + 3.3. Вывести результат + +4. Выход From f07d181cf7d7a3a454bebcf04e9ccefa86ccd72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 21 Aug 2026 19:02:34 +0400 Subject: [PATCH 002/110] docs: add task specifications for project components --- docs/tasks/CustomList.md | 44 +++++++++++++++++++++++++++++++++++ docs/tasks/DataSource.md | 44 +++++++++++++++++++++++++++++++++++ docs/tasks/Sorting.md | 50 ++++++++++++++++++++++++++++++++++++++++ docs/tasks/Student.md | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 docs/tasks/CustomList.md create mode 100644 docs/tasks/DataSource.md create mode 100644 docs/tasks/Sorting.md create mode 100644 docs/tasks/Student.md diff --git a/docs/tasks/CustomList.md b/docs/tasks/CustomList.md new file mode 100644 index 0000000..e95066e --- /dev/null +++ b/docs/tasks/CustomList.md @@ -0,0 +1,44 @@ +# Техническое задание: кастомный контейнер List + +## Задача + +Реализовать собственный контейнер для хранения объектов, не используя готовые реализации `List`, `ArrayList` и `LinkedList`. + +## Требования + +1. Создать интерфейс `MyList`. + +2. В интерфейсе определить основные операции: + + - `add(...)`; + - `get(...)`; + - `set(...)`; + - `remove(...)`; + - `size()`; + - `isEmpty()`. + +3. Создать класс `MyLinkedList`, реализующий `MyList`. + +4. Реализовать контейнер на основе собственных узлов `Node`. + +5. Контейнер должен поддерживать: + + - добавление элемента; + - получение элемента по индексу; + - замену элемента по индексу; + - удаление элемента по индексу; + - получение количества элементов; + - проверку на пустоту. + +6. Обработать некорректные индексы с помощью исключений. + +7. Контейнер не должен зависеть от класса `Student`, сортировки или пользовательского интерфейса. + +## Результат + +Должны быть реализованы: + +```text +MyList.java +MyLinkedList.java +``` diff --git a/docs/tasks/DataSource.md b/docs/tasks/DataSource.md new file mode 100644 index 0000000..3c42760 --- /dev/null +++ b/docs/tasks/DataSource.md @@ -0,0 +1,44 @@ +# Техническое задание: заполнение коллекции + +## Задача + +Реализовать разные способы заполнения `MyList` данными. + +## Требования + +1. Создать интерфейс `DataSource` с методом: + +```text +fill(int size) +``` + +2. Реализовать три способа заполнения: + + - `ManualDataSource` — ручной ввод; + - `RandomDataSource` — случайная генерация; + - `FileDataSource` — чтение из файла. + +3. Результатом работы каждого источника должна быть коллекция `MyList`. + +4. Для создания объектов `Student` использовать `StudentBuilder`. + +5. Все данные должны проходить валидацию средствами `Student`. + +6. При ручном вводе некорректные данные не должны добавляться в коллекцию. + +7. При чтении из файла некорректные строки должны обрабатываться без создания невалидных объектов `Student`. + +8. Размер коллекции должен задаваться пользователем и соблюдаться при заполнении. + +9. Не использовать стандартные коллекции `List`, `ArrayList`, `LinkedList` для хранения результата. + +## Результат + +```text +DataSource.java +ManualDataSource.java +RandomDataSource.java +FileDataSource.java +``` + +Классы не должны содержать логику сортировки или пользовательского меню. diff --git a/docs/tasks/Sorting.md b/docs/tasks/Sorting.md new file mode 100644 index 0000000..65bbffa --- /dev/null +++ b/docs/tasks/Sorting.md @@ -0,0 +1,50 @@ +# Техническое задание: сортировка + +## Задача + +Реализовать паттерн Strategy и собственный алгоритм сортировки объектов `Student`. + +## Требования + +1. Создать интерфейс `SortStrategy` с методом: + +```text +sort(MyList list, Comparator comparator) +``` + +1. Создать класс `BubbleSortStrategy`, реализующий `SortStrategy`. + +2. Алгоритм сортировки реализовать самостоятельно. Готовые методы сортировки Java не использовать. + +3. Сортировка должна работать с `MyList`, а не со стандартными коллекциями Java. + +4. Сортировка должна использовать переданный `Comparator`. + +5. Реализовать возможность сортировки `Student` по каждому из трёх полей: + + - `groupNumber`; + - `averageGrade`; + - `recordBookNumber`. + +6. Для каждого поля создать отдельный компаратор: + + - `StudentGroupComparator`; + - `StudentAverageGradeComparator`; + - `StudentRecordBookComparator`. + +7. Алгоритм сортировки не должен зависеть от класса `Student`. + +## Результат + +Должны быть реализованы: + +```text +SortStrategy.java +BubbleSortStrategy.java + +StudentGroupComparator.java +StudentAverageGradeComparator.java +StudentRecordBookComparator.java +``` + +Классы должны работать с `MyList` и не использовать готовые реализации сортировки. diff --git a/docs/tasks/Student.md b/docs/tasks/Student.md new file mode 100644 index 0000000..2e17172 --- /dev/null +++ b/docs/tasks/Student.md @@ -0,0 +1,46 @@ +# Техническое задание: класс `Student` + +## Задача + +Реализовать класс `Student` как единственную точку проверки корректности данных. Объект класса `Student` не может быть создан с некорректными данными. + +## Требования + +1. Класс `Student` содержит три поля: + + - `groupNumber`; + - `averageGrade`; + - `recordBookNumber`. + +2. Все поля должны быть `private` и неизменяемыми после создания объекта. + +3. Проверка корректности данных должна находиться в классе `Student`. + +4. Реализовать класс `StudentBuilder` для создания объектов `Student`. + +5. В `StudentBuilder` реализовать методы: + + - `setGroupNumber(...)`; + - `setAverageGrade(...)`; + - `setRecordBookNumber(...)`; + - `build()`. + +6. Метод `build()` должен создавать только корректный объект `Student`. При некорректных данных должно выбрасываться исключение. + +7. Реализовать для `Student`: + + - методы получения значений полей; + - `toString()`; + - `equals()`; + - `hashCode()`. + +## Результат + +Должны быть реализованы два независимых класса: + +```text +Student.java +StudentBuilder.java +``` + +Классы не должны зависеть от сортировки, коллекции, меню и ввода данных. From 1d3adceed083975bcda21110e92857d1a1fcc565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 21 Aug 2026 20:08:28 +0400 Subject: [PATCH 003/110] test: add JUnit test requirements --- docs/tasks/Testing.md | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/tasks/Testing.md diff --git a/docs/tasks/Testing.md b/docs/tasks/Testing.md new file mode 100644 index 0000000..26f898c --- /dev/null +++ b/docs/tasks/Testing.md @@ -0,0 +1,67 @@ +# Общее техническое задание: тестирование + +## Задача + +Для всех реализованных классов написать автоматические тесты с использованием **JUnit**. + +## Требования + +1. Для каждого основного класса проекта должны быть написаны тесты. + +2. Проверить: + + - корректное выполнение основных методов; + - корректную обработку некорректных данных; + - граничные случаи; + - ожидаемые исключения. + +3. Для `Student` проверить: + + - создание корректного объекта; + - невозможность создания объекта с некорректными данными; + - работу `Builder`; + - `equals()` и `hashCode()`. + +4. Для `MyLinkedList` проверить: + + - `add()`; + - `get()`; + - `set()`; + - `remove()`; + - `size()`; + - `isEmpty()`; + - некорректные индексы. + +5. Для `MergeSortStrategy` проверить: + + - сортировку по каждому из трёх полей; + - уже отсортированную коллекцию; + - обратный порядок; + - коллекцию из одного элемента; + - пустую коллекцию. + +6. Для каждого компаратора проверить правильное сравнение объектов `Student`. + +7. Тесты должны быть независимыми друг от друга и воспроизводимыми. + +8. Необходимо использовать JUnit и стандартные средства проверки результатов. + +## Результат + +Тесты размещать отдельно от основного кода, например: + +```text +src/ +├── main/ +│ └── java/ +└── test/ + └── java/ + ├── StudentTest.java + ├── MyLinkedListTest.java + ├── MergeSortStrategyTest.java + ├── StudentGroupComparatorTest.java + ├── StudentAverageGradeComparatorTest.java + └── StudentRecordBookComparatorTest.java +``` + +Все тесты должны успешно проходить перед слиянием ветки в `main`. From c8d2683c5d5995d99e1ec47a6fbcfe7381b26de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 21 Aug 2026 20:09:43 +0400 Subject: [PATCH 004/110] docs: update sorting task to merge sort --- docs/tasks/Sorting.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/tasks/Sorting.md b/docs/tasks/Sorting.md index 65bbffa..3fc39b0 100644 --- a/docs/tasks/Sorting.md +++ b/docs/tasks/Sorting.md @@ -1,8 +1,8 @@ -# Техническое задание: сортировка +# Техническое задание: сортировка слиянием ## Задача -Реализовать паттерн Strategy и собственный алгоритм сортировки объектов `Student`. +Реализовать паттерн Strategy и собственный алгоритм сортировки слиянием объектов `Student`. ## Требования @@ -12,39 +12,37 @@ sort(MyList list, Comparator comparator) ``` -1. Создать класс `BubbleSortStrategy`, реализующий `SortStrategy`. +1. Создать класс `MergeSortStrategy`, реализующий `SortStrategy`. -2. Алгоритм сортировки реализовать самостоятельно. Готовые методы сортировки Java не использовать. +2. Реализовать алгоритм сортировки слиянием самостоятельно. -3. Сортировка должна работать с `MyList`, а не со стандартными коллекциями Java. +3. Готовые методы сортировки Java не использовать. -4. Сортировка должна использовать переданный `Comparator`. +4. Сортировка должна работать с `MyList`. -5. Реализовать возможность сортировки `Student` по каждому из трёх полей: +5. Сортировка должна использовать переданный `Comparator`. + +6. Реализовать сортировку `Student` по каждому из трёх полей: - `groupNumber`; - `averageGrade`; - `recordBookNumber`. -6. Для каждого поля создать отдельный компаратор: +7. Создать отдельные компараторы: - `StudentGroupComparator`; - `StudentAverageGradeComparator`; - `StudentRecordBookComparator`. -7. Алгоритм сортировки не должен зависеть от класса `Student`. +8. `MergeSortStrategy` не должен зависеть от класса `Student`. ## Результат -Должны быть реализованы: - ```text SortStrategy.java -BubbleSortStrategy.java +MergeSortStrategy.java StudentGroupComparator.java StudentAverageGradeComparator.java StudentRecordBookComparator.java ``` - -Классы должны работать с `MyList` и не использовать готовые реализации сортировки. From ba117ae21e57eee5fe86921a5e18f73f39bb4ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 22 Aug 2026 12:00:50 +0400 Subject: [PATCH 005/110] feat(collection): add MyList interface --- .../main/java/ru/project/collection/MyList.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/main/java/ru/project/collection/MyList.java 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..e97132e --- /dev/null +++ b/app/src/main/java/ru/project/collection/MyList.java @@ -0,0 +1,16 @@ +package ru.project.collection; + +public interface MyList { + + void add(T element); + + T get(int index); + + T set(int index, T element); + + T remove(int index); + + int size(); + + boolean isEmpty(); +} From 37f02bf3a062bac57d155670d677ba2572509b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 22 Aug 2026 21:18:44 +0400 Subject: [PATCH 006/110] docs: add guide for dependencies between git branches --- GIT_BRANCH_DEPENDENCIES.ru.md | 122 ++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 GIT_BRANCH_DEPENDENCIES.ru.md diff --git a/GIT_BRANCH_DEPENDENCIES.ru.md b/GIT_BRANCH_DEPENDENCIES.ru.md new file mode 100644 index 0000000..f427e43 --- /dev/null +++ b/GIT_BRANCH_DEPENDENCIES.ru.md @@ -0,0 +1,122 @@ +# Работа с зависимостями между Git-ветками + +## Сценарий + +Предположим, мы находимся в ветке: + +```bash +feature/student +``` + +В этой ветке реализуется функциональность, связанная со `Student`. + +При этом у нас появляется новая, независимая по смыслу фича: + +```bash +feature/sorting +``` + +Для реализации сортировки нам необходим класс или интерфейс `Student`, который уже был создан в `feature/student`. + +## Как создать новую ветку + +Если нам необходимо продолжить разработку `feature/sorting`, используя текущее состояние `feature/student`, создаём новую ветку **от `feature/student`**. + +Сначала переключаемся на `feature/student`: + +```bash +git checkout feature/student +``` + +Затем создаём новую ветку: + +```bash +git branch feature/sorting +``` + +После этого история будет выглядеть примерно так: + +```text + feature/sorting + / +---------------● + \ + feature/student +``` + +Обе ветки на момент создания указывают на один и тот же коммит. + +Теперь в `feature/sorting` доступен весь код, который был добавлен в `feature/student`, в том числе `Student`. + +Чтобы перейти в новую ветку: + +```bash +git checkout feature/sorting +``` + +## Почему важно создавать ветку именно таким образом + +В данном случае `feature/sorting` зависит от кода, находящегося в `feature/student`. + +При этом сами фичи логически разные: + +- `feature/student` — разработка `Student`; +- `feature/sorting` — реализация сортировки; +- `Student` нужен в `feature/sorting` как зависимость для реализации сортировки. + +Поэтому не стоит продолжать разработку сортировки непосредственно в `feature/student`. Лучше создать отдельную ветку от того состояния, где необходимый код уже существует. + +--- + +## Если ветки уже разошлись + +Иногда невозможно создать `feature/sorting` непосредственно от нужного места в истории `feature/student`. + +Например, `feature/sorting` уже была создана раньше: + +```text +A---B---C---D feature/student + \ + E---F---G feature/sorting +``` + +В таком случае можно получить изменения из `feature/student` посредством слияния (merge). + +Находясь в `feature/sorting`, выполняем: + +```bash +git checkout feature/sorting +git merge feature/student +``` + +История станет примерно такой: + +```text +A---B---C---D feature/student + \ \ + E---F---G---M feature/sorting +``` + +Коммит `M` — результат слияния двух веток. + +После этого в `feature/sorting` будут доступны изменения из `feature/student`, в том числе необходимый `Student`. + +## Главное правило + +**Если новая фича зависит от кода из другой ветки, но при этом является самостоятельной по смыслу — создаём для неё отдельную ветку, используя нужную ветку как основу.** + +Если новую ветку можно создать непосредственно от нужного состояния исходной ветки: + +```bash +git checkout feature/student +git branch feature/sorting +``` + +Если ветки уже разошлись и создать её от нужного состояния невозможно: + +```bash +git checkout feature/sorting +git merge feature/student +``` + +Таким образом, мы сохраняем логическое разделение фич и одновременно можем использовать необходимый код из другой ветки. From 52251c658cdf65173ed648240db54e864014e200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sun, 23 Aug 2026 19:34:34 +0300 Subject: [PATCH 007/110] feat(sorting): add SortStrategy interface --- app/src/test/java/sorting/SortStrategy.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/src/test/java/sorting/SortStrategy.java diff --git a/app/src/test/java/sorting/SortStrategy.java b/app/src/test/java/sorting/SortStrategy.java new file mode 100644 index 0000000..f252317 --- /dev/null +++ b/app/src/test/java/sorting/SortStrategy.java @@ -0,0 +1,7 @@ +package sorting; + +import java.util.Comparator; + +public interface SortStrategy { + void sort(MyList list, Comparator comparator); +} \ No newline at end of file From 813b790214bcaa9ce9d47bafe9003ada16abc310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sun, 23 Aug 2026 19:36:33 +0300 Subject: [PATCH 008/110] feat(sorting): implement merge sort strategy --- .../test/java/sorting/MergeSortStrategy.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 app/src/test/java/sorting/MergeSortStrategy.java diff --git a/app/src/test/java/sorting/MergeSortStrategy.java b/app/src/test/java/sorting/MergeSortStrategy.java new file mode 100644 index 0000000..86655f7 --- /dev/null +++ b/app/src/test/java/sorting/MergeSortStrategy.java @@ -0,0 +1,65 @@ +package sorting; + +import java.util.Comparator; + +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++; + } + } +} \ No newline at end of file From edc1e6f84014ea8a961e197db3b6be78c75da3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sun, 23 Aug 2026 19:37:29 +0300 Subject: [PATCH 009/110] feat(sorting): add comparator for student group --- app/src/test/java/sorting/StudentGroupComparator.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/src/test/java/sorting/StudentGroupComparator.java diff --git a/app/src/test/java/sorting/StudentGroupComparator.java b/app/src/test/java/sorting/StudentGroupComparator.java new file mode 100644 index 0000000..3d40be4 --- /dev/null +++ b/app/src/test/java/sorting/StudentGroupComparator.java @@ -0,0 +1,11 @@ +package sorting; + +import java.util.Comparator; + +public class StudentGroupComparator implements Comparator { + + @Override + public int compare(Student s1, Student s2) { + return Integer.compare(s1.getGroupNumber(), s2.getGroupNumber()); + } +} \ No newline at end of file From f77babf81f6d1720850ce1b5014b726b79976b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sun, 23 Aug 2026 19:39:12 +0300 Subject: [PATCH 010/110] feat(sorting): add comparator for average grade --- .../java/sorting/StudentAvarageGradeComparator.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/src/test/java/sorting/StudentAvarageGradeComparator.java diff --git a/app/src/test/java/sorting/StudentAvarageGradeComparator.java b/app/src/test/java/sorting/StudentAvarageGradeComparator.java new file mode 100644 index 0000000..46204c2 --- /dev/null +++ b/app/src/test/java/sorting/StudentAvarageGradeComparator.java @@ -0,0 +1,11 @@ +package sorting; + +import java.util.Comparator; + +public class StudentAverageGradeComparator implements Comparator { + + @Override + public int compare(Student s1, Student s2) { + return Double.compare(s1.getAverageGrade(), s2.getAverageGrade()); + } +} \ No newline at end of file From f05c79c3cbcfbd0844b917c80c0f81930a3bdda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sun, 23 Aug 2026 19:40:57 +0300 Subject: [PATCH 011/110] feat(sorting): add comparator for record book number --- .../java/sorting/StudentRecordBookComparator.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/src/test/java/sorting/StudentRecordBookComparator.java diff --git a/app/src/test/java/sorting/StudentRecordBookComparator.java b/app/src/test/java/sorting/StudentRecordBookComparator.java new file mode 100644 index 0000000..22b73e4 --- /dev/null +++ b/app/src/test/java/sorting/StudentRecordBookComparator.java @@ -0,0 +1,11 @@ +package sorting; + +import java.util.Comparator; + +public class StudentRecordBookComparator implements Comparator { + + @Override + public int compare(Student s1, Student s2) { + return Integer.compare(s1.getRecordBookNumber(), s2.getRecordBookNumber()); + } +} \ No newline at end of file From f7443a9d6c2e994409c2d4f30c7b6bd7d64954d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Mon, 24 Aug 2026 18:30:17 +0300 Subject: [PATCH 012/110] fix: correct groupNumber type in StudentGroupComparator --- app/src/test/java/sorting/StudentAvarageGradeComparator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/sorting/StudentAvarageGradeComparator.java b/app/src/test/java/sorting/StudentAvarageGradeComparator.java index 46204c2..1946fd4 100644 --- a/app/src/test/java/sorting/StudentAvarageGradeComparator.java +++ b/app/src/test/java/sorting/StudentAvarageGradeComparator.java @@ -2,10 +2,10 @@ package sorting; import java.util.Comparator; -public class StudentAverageGradeComparator implements Comparator { +public class StudentAvarageGradeComparator implements Comparator { @Override public int compare(Student s1, Student s2) { - return Double.compare(s1.getAverageGrade(), s2.getAverageGrade()); + return Double.compare(s1.getAvarageGrade(), s2.getAvarageGrade()); } } \ No newline at end of file From dfd6ab9ce658eb76e5989ebda956ae1dcf1cb9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Mon, 24 Aug 2026 18:54:28 +0300 Subject: [PATCH 013/110] refactor: change groupNumber type from int to String in StudentGroupComparator --- app/src/test/java/sorting/StudentGroupComparator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/sorting/StudentGroupComparator.java b/app/src/test/java/sorting/StudentGroupComparator.java index 3d40be4..b1efd93 100644 --- a/app/src/test/java/sorting/StudentGroupComparator.java +++ b/app/src/test/java/sorting/StudentGroupComparator.java @@ -5,7 +5,7 @@ import java.util.Comparator; public class StudentGroupComparator implements Comparator { @Override - public int compare(Student s1, Student s2) { + public String compare(Student s1, Student s2) { return Integer.compare(s1.getGroupNumber(), s2.getGroupNumber()); } -} \ No newline at end of file +} From 13c4ff995ae1eaa08481d6d8307d4216d2f7f79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 16:20:23 +0400 Subject: [PATCH 014/110] docs(menu): redesign application menu structure --- docs/MENU.ru.txt | 92 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/docs/MENU.ru.txt b/docs/MENU.ru.txt index 2c49d7b..ad703cd 100644 --- a/docs/MENU.ru.txt +++ b/docs/MENU.ru.txt @@ -1,29 +1,73 @@ ГЛАВНОЕ МЕНЮ -1. Создать и вывести коллекцию - 1.1. Выбрать размер коллекции - 1.2. Выбрать способ заполнения - 1.2.1. Вручную - 1.2.1.1. Задать новый элемент - 1.2.1.2. Завершить создание коллекции - 1.2.2. Случайными значениями - 1.2.3. Из файла - 1.2.3.1. Указать абсолютный путь до файла +Показать текущую коллекцию -2. Настроить и выполнить сортировку - 2.1. Выбрать тип сортировки - 2.1.1. Обычная сортировка - 2.1.1.1. Выбрать поле 1 - 2.1.1.2. Выбрать поле 2 - 2.1.1.3. Выбрать поле 3 - 2.1.2. Сортировка по чётности - 2.1.2.1. Выбрать числовое поле - 2.2. Выполнить сортировку и вывести результат - 2.3. Дописать в файл последний результат сортировки +Меню создания новой коллекции + Вернуться + Задать размер + Настроить способ заполнения + Пользовательские данные + Задать поле 'Название A' + Задать поле 'Название B' + Задать поле 'Название C' + Задать индекс изменяемого объекта + Изменить данные объекта + Предпросмотр + Случайные данные + Настройка диапазона генерации + Поле 'Название A' + Задать минимальное значение + Задать максимальное значение + Поле 'Название B' + Задать минимальное значение + Задать максимальное значение + Поле 'Название C' + Задать минимальное значение + Задать максимальное значение + Данные из файла + Задать путь до файла + Абсолютный + Относительный + Выбрать способ заполнения + Пользовательские данные * + Случайные данные + Данные из файла + Создать новую коллекцию -3. Подсчитать количество вхождений элемента - 3.1. Ввести элемент N - 3.2. Выполнить многопоточный подсчёт - 3.3. Вывести результат +Меню сохранения текущей коллекции в файл + Вывести текущий абсолютный путь до файла + Задать путь до файла + Абсолютный + Относительный + Дописать коллекцию в текущий файл -4. Выход +Меню сортировки текущей коллекции + Вернуться + Настроить способ сортировки + Обычная сортировка + Сортировать по полю 'Название A' + Сортировать по полю 'Название B' + Сортировать по полю 'Название C' +Настроить способ сортировки + Сортировка по чётности + Выбрать числовое поле 'Название A' + Выбрать числовое поле 'Название B' +Выбрать способ сортировки + Обычная сортировка * + Сортировка по чётности +Сортировать текущую коллекцию + +Меню поиска элемента в текущей коллекции + Вернуться + Задать значение поля 'Название A' + Задать значение поля 'Название B' + Задать значение поля 'Название C' + Найти первое вхождение в текущей коллекции + +Меню подсчёта количества вхождений элемента + Задать значение поля 'Название A' + Задать значение поля 'Название B' + Задать значение поля 'Название C' + Выполнить многопоточный подсчёт + +Завершить выполнение программы From a139fa10c6765498d0dd147a47e8b4e16055af2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 17:14:32 +0400 Subject: [PATCH 015/110] feat(ui): add main menu and navigation skeleton --- app/pom.xml | 20 +++++++++ app/src/main/java/ru/project/App.java | 33 ++++++++++++--- .../java/ru/project/ui/CollectionView.java | 8 ++++ .../ru/project/ui/CountOccurrencesMenu.java | 8 ++++ .../ru/project/ui/CreateCollectionMenu.java | 8 ++++ .../main/java/ru/project/ui/EmptyMenu.java | 23 +++++++++++ app/src/main/java/ru/project/ui/MainMenu.java | 34 +++++++++++++++ .../main/java/ru/project/ui/Navigator.java | 41 +++++++++++++++++++ .../ru/project/ui/SaveCollectionMenu.java | 8 ++++ .../java/ru/project/ui/SearchElementMenu.java | 8 ++++ .../ru/project/ui/SortCollectionMenu.java | 8 ++++ 11 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/ru/project/ui/CollectionView.java create mode 100644 app/src/main/java/ru/project/ui/CountOccurrencesMenu.java create mode 100644 app/src/main/java/ru/project/ui/CreateCollectionMenu.java create mode 100644 app/src/main/java/ru/project/ui/EmptyMenu.java create mode 100644 app/src/main/java/ru/project/ui/MainMenu.java create mode 100644 app/src/main/java/ru/project/ui/Navigator.java create mode 100644 app/src/main/java/ru/project/ui/SaveCollectionMenu.java create mode 100644 app/src/main/java/ru/project/ui/SearchElementMenu.java create mode 100644 app/src/main/java/ru/project/ui/SortCollectionMenu.java diff --git a/app/pom.xml b/app/pom.xml index 509e563..5afca24 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -41,6 +41,13 @@ junit-jupiter-params test + + + com.googlecode.lanterna + lanterna + 3.1.5 + + @@ -91,5 +98,18 @@ + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + 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..bf57afc 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -1,10 +1,31 @@ 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.screen.TerminalScreen; +import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import ru.project.ui.MainMenu; +import ru.project.ui.Navigator; + +public final class App { + + private App() {} + + public static void main(String[] args) throws Exception { + DefaultTerminalFactory factory = new DefaultTerminalFactory(); + + Screen screen = new TerminalScreen(factory.createTerminal()); + + screen.startScreen(); + + try { + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen); + + Navigator navigator = new Navigator(gui); + + gui.addWindowAndWait(new MainMenu(navigator)); + } finally { + screen.stopScreen(); } + } } diff --git a/app/src/main/java/ru/project/ui/CollectionView.java b/app/src/main/java/ru/project/ui/CollectionView.java new file mode 100644 index 0000000..82b9540 --- /dev/null +++ b/app/src/main/java/ru/project/ui/CollectionView.java @@ -0,0 +1,8 @@ +package ru.project.ui; + +public class CollectionView extends EmptyMenu { + + public CollectionView() { + super("Текущая коллекция"); + } +} diff --git a/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java b/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java new file mode 100644 index 0000000..db8e0c8 --- /dev/null +++ b/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java @@ -0,0 +1,8 @@ +package ru.project.ui; + +public class CountOccurrencesMenu extends EmptyMenu { + + public CountOccurrencesMenu() { + super("Подсчёт количества вхождений"); + } +} diff --git a/app/src/main/java/ru/project/ui/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/CreateCollectionMenu.java new file mode 100644 index 0000000..6ea56fc --- /dev/null +++ b/app/src/main/java/ru/project/ui/CreateCollectionMenu.java @@ -0,0 +1,8 @@ +package ru.project.ui; + +public class CreateCollectionMenu extends EmptyMenu { + + public CreateCollectionMenu() { + super("Создание новой коллекции"); + } +} diff --git a/app/src/main/java/ru/project/ui/EmptyMenu.java b/app/src/main/java/ru/project/ui/EmptyMenu.java new file mode 100644 index 0000000..0385780 --- /dev/null +++ b/app/src/main/java/ru/project/ui/EmptyMenu.java @@ -0,0 +1,23 @@ +package ru.project.ui; + +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; + +public abstract class EmptyMenu extends BasicWindow { + + protected EmptyMenu(String title) { + super(title); + + Panel panel = new Panel(new LinearLayout(Direction.VERTICAL)); + + panel.addComponent(new Label("Раздел находится в разработке")); + + panel.addComponent(new Button("Назад", this::close)); + + setComponent(panel); + } +} 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..361ce13 --- /dev/null +++ b/app/src/main/java/ru/project/ui/MainMenu.java @@ -0,0 +1,34 @@ +package ru.project.ui; + +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; + +public class MainMenu extends BasicWindow { + + public MainMenu(Navigator navigator) { + super("ГЛАВНОЕ МЕНЮ"); + + Panel panel = new Panel(new LinearLayout(Direction.VERTICAL)); + + panel.addComponent(new Button("Показать текущую коллекцию", navigator::showCollectionView)); + + panel.addComponent(new Button("Создать новую коллекцию", navigator::showCreateCollection)); + + panel.addComponent( + new Button("Сохранить текущую коллекцию в файл", navigator::showSaveCollection)); + + panel.addComponent(new Button("Сортировать текущую коллекцию", navigator::showSortCollection)); + + panel.addComponent(new Button("Найти элемент", navigator::showSearchElement)); + + panel.addComponent( + new Button("Подсчитать количество вхождений", navigator::showCountOccurrences)); + + panel.addComponent(new Button("Завершить выполнение программы", this::close)); + + setComponent(panel); + } +} diff --git a/app/src/main/java/ru/project/ui/Navigator.java b/app/src/main/java/ru/project/ui/Navigator.java new file mode 100644 index 0000000..13489c1 --- /dev/null +++ b/app/src/main/java/ru/project/ui/Navigator.java @@ -0,0 +1,41 @@ +package ru.project.ui; + +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; + +public class Navigator { + + private final WindowBasedTextGUI gui; + + public Navigator(WindowBasedTextGUI gui) { + this.gui = gui; + } + + public void show(BasicWindow window) { + gui.addWindowAndWait(window); + } + + public void showCollectionView() { + show(new CollectionView()); + } + + public void showCreateCollection() { + show(new CreateCollectionMenu()); + } + + public void showSaveCollection() { + show(new SaveCollectionMenu()); + } + + public void showSortCollection() { + show(new SortCollectionMenu()); + } + + public void showSearchElement() { + show(new SearchElementMenu()); + } + + public void showCountOccurrences() { + show(new CountOccurrencesMenu()); + } +} diff --git a/app/src/main/java/ru/project/ui/SaveCollectionMenu.java b/app/src/main/java/ru/project/ui/SaveCollectionMenu.java new file mode 100644 index 0000000..cc0f36a --- /dev/null +++ b/app/src/main/java/ru/project/ui/SaveCollectionMenu.java @@ -0,0 +1,8 @@ +package ru.project.ui; + +public class SaveCollectionMenu extends EmptyMenu { + + public SaveCollectionMenu() { + super("Сохранение текущей коллекции"); + } +} diff --git a/app/src/main/java/ru/project/ui/SearchElementMenu.java b/app/src/main/java/ru/project/ui/SearchElementMenu.java new file mode 100644 index 0000000..7e92c4f --- /dev/null +++ b/app/src/main/java/ru/project/ui/SearchElementMenu.java @@ -0,0 +1,8 @@ +package ru.project.ui; + +public class SearchElementMenu extends EmptyMenu { + + public SearchElementMenu() { + super("Поиск элемента"); + } +} diff --git a/app/src/main/java/ru/project/ui/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/SortCollectionMenu.java new file mode 100644 index 0000000..68e6a7a --- /dev/null +++ b/app/src/main/java/ru/project/ui/SortCollectionMenu.java @@ -0,0 +1,8 @@ +package ru.project.ui; + +public class SortCollectionMenu extends EmptyMenu { + + public SortCollectionMenu() { + super("Сортировка текущей коллекции"); + } +} From 014442453d7c169d7de7e3caa5de8fd93cc7f71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 18:54:48 +0400 Subject: [PATCH 016/110] docs: add even-only merge sort task --- docs/tasks/EvenOnlyMergeSort.md | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/tasks/EvenOnlyMergeSort.md diff --git a/docs/tasks/EvenOnlyMergeSort.md b/docs/tasks/EvenOnlyMergeSort.md new file mode 100644 index 0000000..acfd270 --- /dev/null +++ b/docs/tasks/EvenOnlyMergeSort.md @@ -0,0 +1,156 @@ +# Техническое задание: сортировка по чётным значениям + +## Задача + +Дополнить существующий алгоритм сортировки слиянием специальной сортировкой, при которой элементы с чётным значением выбранного числового поля сортируются, а элементы с нечётным значением сохраняют свои исходные позиции. + +## Контракты + +1. Стратегия должна реализовывать существующий интерфейс `SortStrategy`. + +2. Стратегия должна работать с `MyList`. + +3. Алгоритм должен быть основан на сортировке слиянием. + +4. Готовые методы сортировки Java использовать запрещено. + +5. Стратегия не должна зависеть непосредственно от класса `Student`. + +6. Числовое поле должно передаваться в стратегию извне в виде функции получения значения: + +```java +ToIntFunction +``` + +1. Функция получения числового значения должна: + + - возвращать значение выбранного поля; + - использоваться для проверки чётности; + - не содержать дополнительной логики сортировки. + +2. Стратегия не должна самостоятельно выбирать поле через: + + - `if`; + - `switch`; + - проверку типа `Student`; + - прямое обращение к конкретным полям `Student`. + +3. Для `Student` доступны функции получения числовых полей: + +```java +Student::getGroupNumber +Student::getRecordBookNumber +``` + + 1. Поле `averageGrade` не используется для определения чётности, если оно имеет нецелочисленный тип. + +## Правила сортировки + +1. Элементы с чётным значением выбранного поля участвуют в сортировке. + +2. Элементы с нечётным значением выбранного поля не перемещаются. + +3. Чётные элементы должны занимать только те позиции, которые до сортировки принадлежали чётным элементам. + +4. Чётные элементы сортируются в натуральном порядке выбранного числового поля. + +5. Нечётные элементы должны остаться на своих исходных позициях. + +6. Количество элементов коллекции не должно изменяться. + +### Пример + +Исходная коллекция: + +```text +[3, 8, 5, 2, 4, 7] +``` + +Результат: + +```text +[3, 2, 5, 4, 8, 7] +``` + +Нечётные значения `3`, `5`, `7` остались на исходных позициях. + +Чётные значения `8`, `2`, `4` были отсортированы по возрастанию и заняли позиции, которые изначально принадлежали чётным элементам. + +## Примеры тестов + +### Тест сортировки по номеру группы + +Использовать `Student::getGroupNumber`: + +```java +MyList students = ...; + +SortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(Student::getGroupNumber); + +strategy.sort(students, new StudentGroupComparator()); +``` + +Проверить, что: + +- студенты с нечётным номером группы остались на исходных позициях; +- студенты с чётным номером группы отсортированы по возрастанию; +- количество элементов не изменилось. + +### Тест сортировки по номеру зачётной книжки + +Использовать `Student::getRecordBookNumber`. + +Проверить те же условия для номера зачётной книжки. + +### Тест без чётных элементов + +Исходные значения: + +```text +[1, 3, 5, 7] +``` + +Результат должен быть идентичен исходной коллекции. + +### Тест без нечётных элементов + +Исходные значения: + +```text +[8, 2, 6, 4] +``` + +Результат: + +```text +[2, 4, 6, 8] +``` + +### Тест с одним элементом + +Исходные значения: + +```text +[4] +``` + +Результат: + +```text +[4] +``` + +### Тест с пустой коллекцией + +Сортировка пустой коллекции не должна приводить к ошибке. + +## Результат + +Реализовать: + +```text +EvenOnlyMergeSortStrategy.java +``` + +Класс должен принимать `ToIntFunction` через конструктор и работать с существующими `MyList`, `SortStrategy` и компараторами. From 304f66986848f7be44aa78c3eb99b9fad2dbcc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 19:18:20 +0400 Subject: [PATCH 017/110] docs: improve data source task specification --- docs/tasks/DataSource.md | 393 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 374 insertions(+), 19 deletions(-) diff --git a/docs/tasks/DataSource.md b/docs/tasks/DataSource.md index 3c42760..79a6c14 100644 --- a/docs/tasks/DataSource.md +++ b/docs/tasks/DataSource.md @@ -1,38 +1,393 @@ -# Техническое задание: заполнение коллекции +# Техническое задание: источники данных ## Задача -Реализовать разные способы заполнения `MyList` данными. +Реализовать три независимых источника данных для формирования `MyList`: -## Требования +- `ManualDataSource`; +- `RandomDataSource`; +- `FileDataSource`. -1. Создать интерфейс `DataSource` с методом: +Все источники должны формировать только валидные объекты `Student`. -```text -fill(int size) +Класс `Student` содержит: + +```java +private final String groupNumber; +private final double averageGrade; +private final String recordBookNumber; ``` -2. Реализовать три способа заполнения: +Создание объектов `Student` выполнять через `StudentBuilder`. - - `ManualDataSource` — ручной ввод; - - `RandomDataSource` — случайная генерация; - - `FileDataSource` — чтение из файла. +--- -3. Результатом работы каждого источника должна быть коллекция `MyList`. +# Общий контракт -4. Для создания объектов `Student` использовать `StudentBuilder`. +Создать интерфейс `DataSource`: -5. Все данные должны проходить валидацию средствами `Student`. +```java +public interface DataSource { -6. При ручном вводе некорректные данные не должны добавляться в коллекцию. + MyList create(); +} +``` -7. При чтении из файла некорректные строки должны обрабатываться без создания невалидных объектов `Student`. +Источник данных не должен самостоятельно получать данные от пользователя. -8. Размер коллекции должен задаваться пользователем и соблюдаться при заполнении. +Все необходимые входные значения должны передаваться извне. -9. Не использовать стандартные коллекции `List`, `ArrayList`, `LinkedList` для хранения результата. +Результатом работы каждого источника является `MyList`. -## Результат +Стандартные `List`, `ArrayList` и `LinkedList` для хранения результата не использовать. + +--- + +# `ManualDataSource` + +## Назначение + +Постепенно формировать коллекцию `Student` на основе значений, передаваемых извне для каждого отдельного объекта. + +## Входные данные одного объекта + +Для создания одного `Student` передаются: + +```text +groupNumber +averageGrade +recordBookNumber +``` + +Также передаётся `index` — позиция в коллекции, на которой должен находиться создаваемый объект. + +Размер результирующей коллекции задаётся заранее. + +## Контракт + +`ManualDataSource` должен: + +1. Принимать требуемый размер коллекции. +2. Принимать значения одного `Student`: + + - `groupNumber`; + - `averageGrade`; + - `recordBookNumber`. + +3. Принимать индекс, на котором должен находиться этот объект. +4. Создавать новый `Student` через `StudentBuilder`. +5. Добавлять созданный объект в нужную позицию коллекции. +6. Позволять повторно передавать данные для существующего индекса, заменяя объект на новый. +7. Проверять допустимость индекса. +8. Не изменять уже созданный объект `Student`. +9. Позволять получить текущее состояние коллекции для просмотра. +10. После заполнения вернуть сформированную `MyList`. + +Рекомендуемый метод для передачи одного объекта: + +```java +setStudent( + int index, + String groupNumber, + double averageGrade, + String recordBookNumber +) +``` + +Таким образом, пользовательский код может последовательно передать данные для всех элементов: + +```java +dataSource.setStudent(0, "101", 4.5, "10001"); +dataSource.setStudent(1, "102", 3.8, "10002"); +dataSource.setStudent(2, "101", 4.9, "10003"); +``` + +После заполнения: + +```java +MyList students = dataSource.create(); +``` + +### Изменение объекта + +Для изменения объекта повторно вызывается `setStudent(...)` с нужным индексом: + +```java +dataSource.setStudent(1, "105", 4.2, "10025"); +``` + +Объект на позиции `1` заменяется новым валидным `Student`. + +### Размер коллекции + +Если задан: + +```text +size = 5 +``` + +то для успешного создания коллекции должны быть заданы данные для всех пяти позиций: + +```text +0 +1 +2 +3 +4 +``` + +Порядок заполнения не обязан совпадать с порядком индексов: + +```java +dataSource.setStudent(3, "103", 4.1, "10004"); +dataSource.setStudent(0, "101", 4.5, "10001"); +dataSource.setStudent(4, "102", 3.9, "10005"); +dataSource.setStudent(1, "101", 4.8, "10002"); +dataSource.setStudent(2, "104", 3.7, "10003"); +``` + +После заполнения коллекция должна содержать ровно пять объектов в соответствии с указанными индексами. + +Если хотя бы одна позиция не заполнена, `create()` должен сообщить об ошибке и не возвращать неполную коллекцию. + +--- + +# `RandomDataSource` + +## Назначение + +Создавать заданное количество `Student` со случайными значениями в диапазонах, заданных извне. + +## Входные данные + +Передаются: + +```text +size +``` + +и диапазоны для каждого поля: + +```text +groupNumber: + minimum + maximum + +averageGrade: + minimum + maximum + +recordBookNumber: + minimum + maximum +``` + +## Контракт + +`RandomDataSource` должен: + +1. Принять размер создаваемой коллекции. +2. Принять минимальное и максимальное значение каждого поля. +3. Проверить корректность каждого диапазона. +4. Сгенерировать значения в заданных пределах. +5. Создать каждый объект через `StudentBuilder`. +6. Поместить объекты в `MyList`. +7. Вернуть коллекцию заданного размера. + +Минимальное и максимальное значения включаются в диапазон генерации. + +`RandomDataSource` не должен самостоятельно получать или отображать параметры генерации. + +--- + +# `FileDataSource` + +## Назначение + +Создавать `MyList` на основании содержимого файла. + +## Входные данные + +Передаётся **абсолютный путь к файлу**. + +Относительный путь не является входным параметром `FileDataSource`. + +Формат одной записи: + +```text +groupNumber;averageGrade;recordBookNumber +``` + +Пример: + +```text +101;4.75;123456 +102;3.80;123457 +103;4.20;123458 +``` + +## Контракт + +`FileDataSource` должен: + +1. Получить абсолютный путь к файлу. +2. Открыть файл. +3. Последовательно обработать его записи. +4. Разделить каждую запись на три значения. +5. Создать `Student` через `StudentBuilder`. +6. Добавить только валидные объекты в `MyList`. +7. Пропустить некорректную запись и продолжить обработку остальных. +8. При невозможности открыть файл сообщить об ошибке. +9. Вернуть сформированную `MyList`. + +--- + +# Общие требования + +1. Все источники должны создавать `Student` только через `StudentBuilder`. +2. Источник данных не должен самостоятельно вводить данные. +3. Источник данных не должен зависеть от интерфейса пользователя. +4. Источник данных не должен содержать логику сортировки. +5. Результат хранить только в `MyList`. +6. Невалидный `Student` не должен попасть в коллекцию. +7. Проверка корректности самого объекта выполняется средствами `Student`. +8. Для ручного заполнения изменение существующего `Student` выполнять заменой объекта, а не изменением его полей. + +--- + +# Примеры тестов + +## `ManualDataSource` + +### Заполнение всей коллекции + +Создать источник размера `3`: + +```java +ManualDataSource dataSource = new ManualDataSource(3); + +dataSource.setStudent(0, "101", 4.5, "10001"); +dataSource.setStudent(1, "102", 3.8, "10002"); +dataSource.setStudent(2, "103", 4.2, "10003"); + +MyList result = dataSource.create(); +``` + +Проверить: + +```text +result.size() == 3 +``` + +и соответствие каждого объекта своему индексу. + +### Заполнение в произвольном порядке + +```java +dataSource.setStudent(2, "103", 4.2, "10003"); +dataSource.setStudent(0, "101", 4.5, "10001"); +dataSource.setStudent(1, "102", 3.8, "10002"); +``` + +Проверить правильное расположение объектов. + +### Изменение объекта + +После заполнения: + +```java +dataSource.setStudent(1, "105", 4.9, "10025"); +``` + +Проверить, что изменился только объект с индексом `1`. + +### Незаданный индекс + +Для коллекции размера `3` вызвать: + +```java +dataSource.setStudent(3, ...); +``` + +Проверить выброс исключения. + +### Неполное заполнение + +Заполнить только две позиции из трёх и вызвать `create()`. + +Проверить, что неполная коллекция не возвращается. + +--- + +## `RandomDataSource` + +### Корректная генерация + +Задать размер `100` и допустимые диапазоны для всех трёх полей. + +Проверить: + +```text +result.size() == 100 +``` + +и нахождение каждого значения внутри заданного диапазона. + +### Граничный диапазон + +Задать одинаковые минимальное и максимальное значения. + +Проверить, что все сгенерированные значения соответствуют этому значению. + +### Некорректный диапазон + +Задать: + +```text +minimum > maximum +``` + +Проверить выброс исключения. + +--- + +## `FileDataSource` + +### Корректный файл + +Для файла: + +```text +101;4.75;123456 +102;3.80;123457 +103;4.20;123458 +``` + +проверить получение трёх корректных объектов. + +### Файл с некорректной записью + +```text +101;4.75;123456 +invalid;abc;123457 +103;4.20;123458 +``` + +Проверить, что в коллекцию попадают только две корректные записи. + +### Пустой файл + +Проверить получение пустой коллекции. + +### Несуществующий файл + +Передать путь к несуществующему файлу и проверить корректную обработку ошибки. + +--- + +# Результат + +Реализовать: ```text DataSource.java @@ -41,4 +396,4 @@ RandomDataSource.java FileDataSource.java ``` -Классы не должны содержать логику сортировки или пользовательского меню. +Допускается создание вспомогательных классов для чтения файла и генерации случайных значений. From 6b10c04df25c8a11b93f67f95e301081f22d19d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 22:08:40 +0400 Subject: [PATCH 018/110] docs: add stream collection task --- docs/tasks/StreamCollection.md | 327 +++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 docs/tasks/StreamCollection.md diff --git a/docs/tasks/StreamCollection.md b/docs/tasks/StreamCollection.md new file mode 100644 index 0000000..e447a58 --- /dev/null +++ b/docs/tasks/StreamCollection.md @@ -0,0 +1,327 @@ +# Техническое задание: заполнение `MyList` посредством Stream API + +## Задача + +Реализовать механизм заполнения собственной коллекции `MyList` посредством Java Stream API без использования стандартных коллекций Java в качестве промежуточного или итогового хранилища. + +## Требования + +1. Итоговым результатом работы потока должна быть: + +```java +MyList +``` + +1. Для сбора элементов потока непосредственно в `MyList` реализовать собственный `Collector`. + +2. Стандартные коллекции Java (`List`, `ArrayList`, `LinkedList` и другие реализации `List`) запрещено использовать: + + - как итоговую коллекцию; + - как промежуточное хранилище элементов перед их переносом в `MyList`. + +3. `Student` должен создаваться только через уже существующий независимый класс `StudentBuilder`, если объект создаётся первоначально. + +4. `MyListCollector` не должен создавать или изменять объекты `Student`. Он работает с уже созданными объектами. + +5. При использовании `map` для получения нового `Student` использовать методы `with...()`, а не `StudentBuilder`. + +6. Логика заполнения `MyList` должна быть отделена от логики класса `Student`. + +7. Для добавления объектов использовать существующий метод: + +```java +add(Student student) +``` + +1. Необходимо реализовать поддержку объединения частичных результатов, необходимую для работы `Collector`. + +--- + +## `MyListCollector` + +Создать универсальный класс: + +```java +public class MyListCollector + implements Collector, MyList> { +} +``` + +Реализовать методы: + +```text +supplier() +accumulator() +combiner() +finisher() +characteristics() +``` + +Назначение: + +- `supplier()` — создать пустой `MyList`; +- `accumulator()` — добавить очередной элемент в `MyList`; +- `combiner()` — объединить две частично заполненные коллекции; +- `finisher()` — вернуть готовый `MyList`; +- `characteristics()` — определить характеристики сборщика. + +Коллектор должен работать с `Student` без специальной логики, связанной с его полями. + +--- + +## `MyCollectors` + +Создать вспомогательный класс: + +```java +public final class MyCollectors { + + public static Collector, MyList> toMyList() { + return new MyListCollector<>(); + } +} +``` + +Класс должен предоставлять удобный способ получения собственного сборщика. + +--- + +## Контракт использования + +Должна поддерживаться конструкция: + +```java +MyList students = Stream.of( + student1, + student2, + student3 +).collect(MyCollectors.toMyList()); +``` + +где `student1`, `student2`, `student3` — валидные объекты `Student`, предварительно созданные через `StudentBuilder`. + +Результатом должен быть именно `MyList`. + +--- + +## Пример `map` + +`map` должен использоваться для преобразования одного `Student` в новый `Student`. + +Исходный объект не изменяется. Для создания нового объекта использовать `with...()`. + +Одно из преобразуемых полей должно быть `String` и формироваться по заданному бизнес-правилу: + +```text +Заглавная латинская буква + двузначное число +``` + +Например: + +```text +A07 +B12 +Z35 +``` + +Пример: + +```java +MyList students = Stream.of( + student1, + student2, + student3 +) +.map(student -> student.withGroupNumber( + generateGroupNumber(student) +)) +.collect(MyCollectors.toMyList()); +``` + +Пример функции формирования значения: + +```java +private static String generateGroupNumber(Student student) { + int number = Math.abs(student.getRecordBookNumber().hashCode() % 100); + char letter = (char) ('A' + + Math.abs(student.getRecordBookNumber().hashCode()) % 26); + + return String.format("%c%02d", letter, number); +} +``` + +Результат `map` должен оставаться `Stream`, после чего он непосредственно собирается в `MyList`. + +Формат создаваемого `groupNumber` должен соответствовать: + +```text +^[A-Z][0-9]{2}$ +``` + +--- + +## Пример `filter` совместно с `map` + +Проверить совместную работу промежуточных операций: + +```java +MyList students = Stream.of( + student1, + student2, + student3 +) +.filter(student -> student.getAverageGrade() >= 4.0) +.map(student -> student.withGroupNumber( + generateGroupNumber(student) +)) +.collect(MyCollectors.toMyList()); +``` + +Проверить, что: + +- в результат попали только студенты со средним баллом не ниже `4.0`; +- каждый результат является новым `Student`; +- `groupNumber` соответствует формату `[A-Z][0-9]{2}`; +- исходные объекты не изменились. + +--- + +## Параллельные потоки + +`combiner()` должен корректно объединять две частично сформированные `MyList`. + +Реалистичный пример: + +```java +MyList students = IntStream.range(0, 10_000) + .parallel() + .mapToObj(i -> student1.withGroupNumber( + String.format("%c%02d", + 'A' + (i % 26), + i % 100) + )) + .collect(MyCollectors.toMyList()); +``` + +Проверить: + +- результат содержит `10_000` объектов; +- ни один объект не потерян; +- количество объектов не превышает ожидаемого; +- каждый `groupNumber` соответствует формату `[A-Z][0-9]{2}`; +- объекты `Student` не изменяются после создания; +- `combiner()` корректно объединяет частичные результаты. + +--- + +## Примеры тестов + +### Заполнение несколькими `Student` + +Создать три валидных объекта `Student` через `StudentBuilder` и собрать их: + +```java +MyList result = Stream.of( + student1, + student2, + student3 +).collect(MyCollectors.toMyList()); +``` + +Проверить: + +```text +result != null +result.size() == 3 +``` + +и соответствие элементов исходному потоку. + +### Проверка порядка + +Для: + +```text +student1 +student2 +student3 +``` + +проверить: + +```text +result.get(0) == student1 +result.get(1) == student2 +result.get(2) == student3 +``` + +### Пустой поток + +```java +MyList result = Stream.empty() + .collect(MyCollectors.toMyList()); +``` + +Проверить: + +```text +result != null +result.isEmpty() +``` + +### Проверка `map` + +Преобразовать несколько `Student` через `withGroupNumber()`. + +Проверить: + +- исходные объекты не изменились; +- созданные объекты отличаются от исходных; +- `groupNumber` соответствует формату `[A-Z][0-9]{2}`. + +### Проверка `filter` и `map` + +Проверить, что после `filter` и `map` в `MyList` попадают только необходимые студенты с корректно сформированным `groupNumber`. + +### Параллельный поток + +Создать поток из `10_000` объектов через `parallel()`. + +Проверить размер итоговой коллекции, отсутствие потери элементов и корректность `groupNumber`. + +--- + +## Ограничения + +Запрещено: + +- использовать `Collectors.toList()`; +- использовать `ArrayList`, `LinkedList` или другие стандартные коллекции для накопления результата; +- сначала собирать `Student` в стандартную коллекцию, а затем переносить их в `MyList`; +- создавать `Student` внутри `MyListCollector`; +- изменять существующий `Student`; +- использовать `StudentBuilder` внутри `map` вместо `with...()`. + +## Результат + +Реализовать: + +```text +MyListCollector.java +MyCollectors.java +``` + +Использовать существующие: + +```text +Student.java +StudentBuilder.java +MyList.java +MyLinkedList.java +``` + +### Рекомендация по архитектуре + +`MyListCollector` должен зависеть только от интерфейса `MyList`, а не от конкретной реализации `MyLinkedList`. + +`MyCollectors` должен содержать только методы создания собственных сборщиков и не должен содержать бизнес-логику. From f504914bd0087b86539720501940ab2ea14b2925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 23:00:52 +0400 Subject: [PATCH 019/110] docs: refine EvenOnlyMergeSortStrategy specification --- docs/tasks/EvenOnlyMergeSort.md | 207 ++++++++++++++++++++------------ 1 file changed, 130 insertions(+), 77 deletions(-) diff --git a/docs/tasks/EvenOnlyMergeSort.md b/docs/tasks/EvenOnlyMergeSort.md index acfd270..ad5c4d3 100644 --- a/docs/tasks/EvenOnlyMergeSort.md +++ b/docs/tasks/EvenOnlyMergeSort.md @@ -2,148 +2,192 @@ ## Задача -Дополнить существующий алгоритм сортировки слиянием специальной сортировкой, при которой элементы с чётным значением выбранного числового поля сортируются, а элементы с нечётным значением сохраняют свои исходные позиции. +Дополнить существующую сортировку слиянием специальной стратегией `EvenOnlyMergeSortStrategy`. + +Стратегия должна сортировать только объекты, у которых выбранное числовое поле имеет чётное значение. Объекты с нечётным значением должны сохранять свои исходные позиции. ## Контракты -1. Стратегия должна реализовывать существующий интерфейс `SortStrategy`. +1. Стратегия должна реализовывать существующий интерфейс: -2. Стратегия должна работать с `MyList`. +```java +SortStrategy +``` -3. Алгоритм должен быть основан на сортировке слиянием. +1. Метод сортировки должен работать с: -4. Готовые методы сортировки Java использовать запрещено. +```java +MyList +``` -5. Стратегия не должна зависеть непосредственно от класса `Student`. +1. Для сортировки чётных элементов использовать алгоритм **Merge Sort**. -6. Числовое поле должно передаваться в стратегию извне в виде функции получения значения: +2. Готовые методы сортировки Java использовать запрещено. + +3. Стратегия не должна зависеть непосредственно от класса `Student`. + +4. Числовое поле передавать в стратегию извне через: ```java ToIntFunction ``` -1. Функция получения числового значения должна: +1. Сохранить переданный `ToIntFunction` во внутреннем поле стратегии. - - возвращать значение выбранного поля; - - использоваться для проверки чётности; - - не содержать дополнительной логики сортировки. - -2. Стратегия не должна самостоятельно выбирать поле через: - - - `if`; - - `switch`; - - проверку типа `Student`; - - прямое обращение к конкретным полям `Student`. - -3. Для `Student` доступны функции получения числовых полей: +2. Для определения чётности элемента использовать: + +```java +valueExtractor.applyAsInt(element) +``` + +1. `ToIntFunction` используется только для получения числового значения выбранного поля. Стратегия не должна знать, какое именно поле объекта используется. + +2. Для `Student` в качестве числового поля используется: ```java -Student::getGroupNumber Student::getRecordBookNumber ``` - 1. Поле `averageGrade` не используется для определения чётности, если оно имеет нецелочисленный тип. + 1. `getRecordBookNumber()` должен возвращать `int`. -## Правила сортировки +## Правила алгоритма -1. Элементы с чётным значением выбранного поля участвуют в сортировке. +Стратегия должна: -2. Элементы с нечётным значением выбранного поля не перемещаются. - -3. Чётные элементы должны занимать только те позиции, которые до сортировки принадлежали чётным элементам. - -4. Чётные элементы сортируются в натуральном порядке выбранного числового поля. - -5. Нечётные элементы должны остаться на своих исходных позициях. - -6. Количество элементов коллекции не должно изменяться. +1. Пройти по исходной `MyList`. +2. Определить элементы с чётным значением, используя `ToIntFunction`. +3. Запомнить позиции этих элементов. +4. Отдельно отсортировать только чётные элементы с помощью **Merge Sort** и переданного `Comparator`. +5. Вернуть отсортированные чётные элементы на позиции, которые занимали чётные элементы до сортировки. +6. Не изменять позиции элементов с нечётным значением. +7. Не изменять размер коллекции. ### Пример -Исходная коллекция: +Исходные значения выбранного числового поля: ```text -[3, 8, 5, 2, 4, 7] +[7, 8, 3, 2, 5, 4] +``` + +Чётные элементы: + +```text +8, 2, 4 +``` + +После Merge Sort: + +```text +2, 4, 8 ``` Результат: ```text -[3, 2, 5, 4, 8, 7] +[7, 2, 3, 4, 5, 8] ``` -Нечётные значения `3`, `5`, `7` остались на исходных позициях. +Нечётные элементы `7`, `3`, `5` остались на исходных позициях. -Чётные значения `8`, `2`, `4` были отсортированы по возрастанию и заняли позиции, которые изначально принадлежали чётным элементам. +## Пример использования + +```java +SortStrategy strategy = + new EvenOnlyMergeSortStrategy<>( + Student::getRecordBookNumber + ); + +strategy.sort( + students, + new StudentRecordBookComparator() +); +``` + +Стратегия получает функцию выбора числового поля извне и не содержит логики, специфичной для `Student`. ## Примеры тестов -### Тест сортировки по номеру группы +### Обычный случай -Использовать `Student::getGroupNumber`: +Создать студентов с `recordBookNumber`: -```java -MyList students = ...; - -SortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(Student::getGroupNumber); - -strategy.sort(students, new StudentGroupComparator()); +```text +7, 8, 3, 2, 5, 4 ``` -Проверить, что: +После сортировки ожидается: -- студенты с нечётным номером группы остались на исходных позициях; -- студенты с чётным номером группы отсортированы по возрастанию; +```text +7, 2, 3, 4, 5, 8 +``` + +Проверить: + +- нечётные элементы сохранили свои позиции; +- чётные элементы отсортированы по возрастанию; - количество элементов не изменилось. -### Тест сортировки по номеру зачётной книжки - -Использовать `Student::getRecordBookNumber`. - -Проверить те же условия для номера зачётной книжки. - -### Тест без чётных элементов - -Исходные значения: +### Все значения нечётные ```text -[1, 3, 5, 7] +1, 3, 5, 7 ``` -Результат должен быть идентичен исходной коллекции. - -### Тест без нечётных элементов - -Исходные значения: +Ожидаемый результат: ```text -[8, 2, 6, 4] +1, 3, 5, 7 ``` -Результат: +### Все значения чётные ```text -[2, 4, 6, 8] +8, 2, 6, 4 ``` -### Тест с одним элементом - -Исходные значения: +Ожидаемый результат: ```text -[4] +2, 4, 6, 8 ``` -Результат: +### Один элемент ```text -[4] +4 ``` -### Тест с пустой коллекцией +Ожидаемый результат: -Сортировка пустой коллекции не должна приводить к ошибке. +```text +4 +``` + +### Пустая коллекция + +Пустая коллекция должна остаться пустой и не приводить к ошибке. + +### Проверка `ToIntFunction` + +Передать: + +```java +Student::getRecordBookNumber +``` + +и проверить, что именно `recordBookNumber` используется для определения чётности. + +## Ограничения + +Запрещено: + +- использовать `Arrays.sort()`, `Collections.sort()`, `List.sort()` и другие готовые алгоритмы сортировки; +- перемещать элементы с нечётным значением; +- использовать отдельный алгоритм сортировки вместо Merge Sort; +- напрямую обращаться к полям `Student` внутри `EvenOnlyMergeSortStrategy`; +- выбирать поле через `if` или `switch`; +- использовать стандартные коллекции Java в качестве основной коллекции. ## Результат @@ -153,4 +197,13 @@ strategy.sort(students, new StudentGroupComparator()); EvenOnlyMergeSortStrategy.java ``` -Класс должен принимать `ToIntFunction` через конструктор и работать с существующими `MyList`, `SortStrategy` и компараторами. +Класс должен использовать: + +```text +SortStrategy.java +MyList.java +ToIntFunction +Comparator +``` + +и не должен иметь зависимости от конкретного класса `Student`. From 4d20a59072715cd62d4406b75cdec0f3a05ca2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 23:14:35 +0400 Subject: [PATCH 020/110] refactor: move sorting classes from test to main source set --- .../java => main/java/ru/project}/sorting/MergeSortStrategy.java | 0 .../{test/java => main/java/ru/project}/sorting/SortStrategy.java | 0 .../java/ru/project}/sorting/StudentAvarageGradeComparator.java | 0 .../java/ru/project}/sorting/StudentGroupComparator.java | 0 .../java/ru/project}/sorting/StudentRecordBookComparator.java | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename app/src/{test/java => main/java/ru/project}/sorting/MergeSortStrategy.java (100%) rename app/src/{test/java => main/java/ru/project}/sorting/SortStrategy.java (100%) rename app/src/{test/java => main/java/ru/project}/sorting/StudentAvarageGradeComparator.java (100%) rename app/src/{test/java => main/java/ru/project}/sorting/StudentGroupComparator.java (100%) rename app/src/{test/java => main/java/ru/project}/sorting/StudentRecordBookComparator.java (100%) diff --git a/app/src/test/java/sorting/MergeSortStrategy.java b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java similarity index 100% rename from app/src/test/java/sorting/MergeSortStrategy.java rename to app/src/main/java/ru/project/sorting/MergeSortStrategy.java diff --git a/app/src/test/java/sorting/SortStrategy.java b/app/src/main/java/ru/project/sorting/SortStrategy.java similarity index 100% rename from app/src/test/java/sorting/SortStrategy.java rename to app/src/main/java/ru/project/sorting/SortStrategy.java diff --git a/app/src/test/java/sorting/StudentAvarageGradeComparator.java b/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java similarity index 100% rename from app/src/test/java/sorting/StudentAvarageGradeComparator.java rename to app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java diff --git a/app/src/test/java/sorting/StudentGroupComparator.java b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java similarity index 100% rename from app/src/test/java/sorting/StudentGroupComparator.java rename to app/src/main/java/ru/project/sorting/StudentGroupComparator.java diff --git a/app/src/test/java/sorting/StudentRecordBookComparator.java b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java similarity index 100% rename from app/src/test/java/sorting/StudentRecordBookComparator.java rename to app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java From 57868a33a29ab4232f22f00669185484ba24ef85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 23:41:58 +0400 Subject: [PATCH 021/110] docs: add student update task --- docs/tasks/StudentUpdate.md | 326 ++++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 docs/tasks/StudentUpdate.md diff --git a/docs/tasks/StudentUpdate.md b/docs/tasks/StudentUpdate.md new file mode 100644 index 0000000..e3db385 --- /dev/null +++ b/docs/tasks/StudentUpdate.md @@ -0,0 +1,326 @@ +# Техническое задание: обновление `Student` + +## Задача + +Обновить класс `Student` и связанный с ним код в соответствии с новой моделью данных и требованиями к неизменяемости объекта. + +Текущая реализация содержит: + +```java +private final String groupNumber; +private final double averageGrade; +private final String recordBookNumber; +``` + +Необходимо изменить тип `recordBookNumber` и добавить методы создания изменённых копий `Student`. + +## Изменения в `Student` + +### 1. Изменить тип `recordBookNumber` + +Поле: + +```java +private final String recordBookNumber; +``` + +заменить на: + +```java +private final int recordBookNumber; +``` + +Метод: + +```java +getRecordBookNumber() +``` + +должен возвращать `int`. + +Все проверки, `equals()`, `hashCode()` и `toString()` должны быть адаптированы под новый тип. + +### 2. Добавить бизнес-правило для `groupNumber` + +`groupNumber` должен соответствовать строго следующему формату: + +```text +<Заглавная буква латинского алфавита><2 цифры> +``` + +Регулярное выражение: + +```text +^[A-Z][0-9]{2}$ +``` + +Допустимые значения: + +```text +A12 +B32 +Z00 +``` + +Недопустимые значения: + +```text +а12 +a12 +A1 +A123 +AA1 +а1А +аа!!а123 +23фи!12 +№№###*() +``` + +Проверка должна выполняться при создании `Student`. + +`Student` не может существовать с некорректным `groupNumber`. + +### 3. Проверить `averageGrade` + +Сохранить правило: + +```text +0.0 <= averageGrade <= 5.0 +``` + +Значения `0.0` и `5.0` допустимы. + +### 4. Проверить `recordBookNumber` + +После преобразования в `int` установить бизнес-правило: + +```text +recordBookNumber > 0 +``` + +Значение `0` и отрицательные значения считаются недопустимыми. + +--- + +# Методы `with...()` + +Добавить методы, возвращающие **новый объект `Student`**, не изменяя текущий объект. + +Необходимо реализовать: + +```java +withGroupNumber(String groupNumber) +withAverageGrade(double averageGrade) +withRecordBookNumber(int recordBookNumber) +``` + +Каждый метод должен: + +1. Сохранять исходный объект без изменений. +2. Изменять только указанное поле в новом объекте. +3. Сохранять остальные поля исходного объекта. +4. Повторно применять все правила валидации. +5. Возвращать новый валидный объект `Student`. + +Пример: + +```java +Student updated = student.withAverageGrade(4.8); +``` + +После выполнения: + +```text +student.getAverageGrade() == прежнее значение +updated.getAverageGrade() == 4.8 +``` + +Если новое значение невалидно, метод должен выбросить исключение, а исходный объект не должен измениться. + +--- + +# `StudentBuilder` + +Обновить существующий `StudentBuilder` в соответствии с новой моделью `Student`. + +Методы должны соответствовать типам: + +```java +setGroupNumber(String groupNumber) +setAverageGrade(double averageGrade) +setRecordBookNumber(int recordBookNumber) +``` + +`build()` должен создавать только валидный `Student`. + +Все новые правила валидации должны применяться при создании объекта через `StudentBuilder`. + +--- + +# Обновление зависимого кода + +Найти и обновить весь код, использующий `Student` и изменяемые методы или типы. + +В частности, обновить: + +- `Comparator`; +- сортировки; +- источники данных; +- тестовые классы; +- создание `Student`; +- `with...()`-операции; +- чтение и запись данных, если они используют `recordBookNumber`. + +Например, существующий: + +```java +implements Comparator +``` + +должен использовать актуальный метод: + +```java +getAverageGrade() +``` + +вместо ошибочного `getAvarageGrade()`. + +Компаратор `recordBookNumber` должен сравнивать значения как числа. + +Например: + +```java +Integer.compare( + s1.getRecordBookNumber(), + s2.getRecordBookNumber() +) +``` + +Все места, где `recordBookNumber` рассматривался как `String`, необходимо адаптировать к `int`. + +--- + +# Тесты + +Обновить существующий `StudentTest` с учётом новой модели. + +## Создание корректного `Student` + +Проверить создание объекта со значениями: + +```text +groupNumber = A12 +averageGrade = 4.5 +recordBookNumber = 12345 +``` + +Проверить корректность всех геттеров. + +## Проверка `groupNumber` + +Проверить успешное создание для: + +```text +A12 +B32 +Z00 +``` + +Проверить исключение для: + +```text +a12 +A1 +A123 +AA1 +а1А +аа!!а123 +23фи!12 +№№###*() +``` + +## Проверка `averageGrade` + +Проверить: + +```text +0.0 — допустимо +5.0 — допустимо +-1.0 — недопустимо +6.0 — недопустимо +``` + +## Проверка `recordBookNumber` + +Проверить: + +```text +1 — допустимо +12345 — допустимо +0 — недопустимо +-1 — недопустимо +``` + +## Тесты `with...()` + +### `withGroupNumber()` + +Проверить, что: + +- возвращается новый объект; +- `groupNumber` изменён; +- `averageGrade` не изменился; +- `recordBookNumber` не изменился; +- исходный `Student` остался прежним. + +### `withAverageGrade()` + +Проверить те же свойства для `averageGrade`. + +### `withRecordBookNumber()` + +Проверить те же свойства для `recordBookNumber`. + +### `with...()` с невалидным значением + +Для каждого метода передать некорректное значение. + +Проверить: + +- выбрасывается `IllegalArgumentException`; +- исходный объект не изменяется; +- не создаётся невалидный `Student`. + +## `equals()` и `hashCode()` + +Обновить существующие тесты с учётом `int recordBookNumber`. + +Проверить: + +- одинаковые студенты равны; +- одинаковые студенты имеют одинаковый `hashCode`; +- изменение любого поля создаёт отличающийся объект. + +## Результат + +Необходимо обновить: + +```text +Student.java +StudentBuilder.java +StudentTest.java +``` + +а также **все классы проекта, зависящие от изменённого интерфейса `Student`**, включая компараторы и другие реализации, использующие `recordBookNumber`, `groupNumber`, `averageGrade` или их геттеры. + +### Основной принцип + +`Student` должен оставаться единственной точкой истины для валидности данных: + +```text +StudentBuilder ──┐ + ├──> Student +with...() ───────┘ +``` + +Ни `Builder`, ни `with...()`, ни внешние классы не должны позволять создать невалидный объект. From 1fe77ef1b3a2600cc0e452fb28addf4b019d3179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 23:56:20 +0400 Subject: [PATCH 022/110] docs: add data source update --- docs/tasks/DataSourceUpdate.md | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/tasks/DataSourceUpdate.md diff --git a/docs/tasks/DataSourceUpdate.md b/docs/tasks/DataSourceUpdate.md new file mode 100644 index 0000000..c175d9c --- /dev/null +++ b/docs/tasks/DataSourceUpdate.md @@ -0,0 +1,81 @@ +# Дополнение к техническому заданию: изменение типа `recordBookNumber` + +Данное дополнение имеет **приоритет над основным ТЗ `DataSource.md`**. + +## Изменение модели `Student` + +Поле: + +```java +private final String recordBookNumber; +``` + +заменено на: + +```java +private final int recordBookNumber; +``` + +Метод: + +```java +getRecordBookNumber() +``` + +возвращает `int`. + +## Требования к `DataSource` + +Во всех реализациях источников данных необходимо учитывать новый тип `recordBookNumber`. + +### `ManualDataSource` + +`recordBookNumber` должен передаваться как `int`: + +```java +setStudent( + int index, + String groupNumber, + double averageGrade, + int recordBookNumber +); +``` + +Строковое значение номера зачётной книжки больше не используется. + +### `RandomDataSource` + +Для `recordBookNumber` необходимо задавать и генерировать **числовой диапазон**: + +```text +минимальное значение — int +максимальное значение — int +``` + +Генерируемое значение должно быть целым числом и соответствовать ограничениям `Student`. + +### `FileDataSource` + +В файле `recordBookNumber` должен записываться как целое число. + +Формат записи: + +```text +groupNumber;averageGrade;recordBookNumber +``` + +Пример: + +```text +A12;4.75;12345 +B32;3.80;12346 +C17;4.20;12347 +``` + +При чтении значение необходимо преобразовать в `int` до создания `Student`. + +## Приоритет + +При противоречии между данным дополнением и основным `DataSource.md` использовать требования этого дополнения. + +Все остальные требования основного технического задания остаются без изменений. From c2efae1248b5153af38bcb7f08e2a97e017cd1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 24 Aug 2026 00:27:47 +0300 Subject: [PATCH 023/110] feat: add element to linked list --- app/src/main/java/ru/project/collection/MyLinkedList.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/src/main/java/ru/project/collection/MyLinkedList.java 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..e69de29 From ffb229b1a72960987913d2d20c4c1ef664a1ac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 24 Aug 2026 00:40:02 +0300 Subject: [PATCH 024/110] feat: add implement get by index for linked --- .../ru/project/collection/MyLinkedList.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index e69de29..42ea519 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -0,0 +1,74 @@ +public class MyLinkedList implements MyList +{ + private Node head; + private int size; + + 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++; + } + public T get(int index) + { + int currentIndex = 0; + Node temp = head; + while(temp != null) + { + if (currentIndex == index) + { + return (T) temp.getValue(); + + } + else { + temp = temp.getNext(); + currentIndex++; + } + + } + throw new IllegalArgumentException(); + } + 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); + } + public 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; + } + } + + +} \ No newline at end of file From ef1c54f10ed4a9ab2d3ea997c11702577194ac06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 24 Aug 2026 00:43:12 +0300 Subject: [PATCH 025/110] feat: add an index deletion method --- .../ru/project/collection/MyLinkedList.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 42ea519..1b1dc47 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -34,6 +34,31 @@ public class MyLinkedList implements MyList } throw new IllegalArgumentException(); } + public void remove(int index) + { + 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++; + } + } + } public String toString() { Object[] res = new Object[size]; int index = 0; From a7861dd58a891881d00455c7495164be0131ae67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 24 Aug 2026 00:48:14 +0300 Subject: [PATCH 026/110] feat: The set method for setting the value of a node by index. --- .../main/java/ru/project/collection/MyLinkedList.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 1b1dc47..6ba300f 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -59,6 +59,17 @@ public class MyLinkedList implements MyList } } } + 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(); + } + + public String toString() { Object[] res = new Object[size]; int index = 0; From 9e8fc5effb6453fa440db1a3d5d6f2eb1e74f747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 24 Aug 2026 00:49:43 +0300 Subject: [PATCH 027/110] feat: add size() method to MyLinkedList --- app/src/main/java/ru/project/collection/MyLinkedList.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 6ba300f..4d54ab8 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -69,7 +69,14 @@ public class MyLinkedList implements MyList temp = temp.getNext(); } + T oldValue = (T) temp.getValue(); + temp.setValue(value); + return oldValue; + } + public int size() { + return size; + } public String toString() { Object[] res = new Object[size]; int index = 0; From e7db5fee4db500653c32571283ce046e4e3cf5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 24 Aug 2026 00:52:09 +0300 Subject: [PATCH 028/110] feat: add an isEmpty() method that allows you to check whether the colle --- app/src/main/java/ru/project/collection/MyLinkedList.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 4d54ab8..7164857 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -77,6 +77,10 @@ public class MyLinkedList implements MyList public int size() { return size; } + public boolean isEmpty() + { + return size == 0; + } public String toString() { Object[] res = new Object[size]; int index = 0; From f855a760922fda49f019e5f00b18ee54c1c70306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Tue, 25 Aug 2026 13:25:03 +0300 Subject: [PATCH 029/110] fix: correct minor syntax issues --- .../ru/project/collection/MyLinkedList.java | 28 ++++++++++--------- .../java/ru/project/collection/MyList.java | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 7164857..23b2b75 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -1,13 +1,13 @@ +import java.util.Arrays; public class MyLinkedList implements MyList { - private Node head; + private Node head; private int size; - public void add(T value) { if (head == null) { this.head = new Node<>(value); } else { - Node temp = head; + Node temp = head; while (temp.getNext() != null) { temp = temp.getNext(); } @@ -18,12 +18,12 @@ public class MyLinkedList implements MyList public T get(int index) { int currentIndex = 0; - Node temp = head; + Node temp = head; while(temp != null) { if (currentIndex == index) { - return (T) temp.getValue(); + return temp.getValue(); } else { @@ -36,6 +36,9 @@ public class MyLinkedList implements MyList } public void remove(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); + } if (index == 0) { head = head.getNext(); @@ -44,7 +47,7 @@ public class MyLinkedList implements MyList } int currentIndex = 0; - Node temp = head; + Node temp = head; while (temp != null) { if (currentIndex == index-1) @@ -64,16 +67,15 @@ public class MyLinkedList implements MyList throw new IllegalArgumentException(); } - Node temp = head; + Node temp = head; for (int i = 0; i < index; i++) { temp = temp.getNext(); } - T oldValue = (T) temp.getValue(); + T oldValue = temp.getValue(); temp.setValue(value); return oldValue; } - public int size() { return size; } @@ -81,10 +83,11 @@ public class MyLinkedList implements MyList { return size == 0; } + @Override public String toString() { Object[] res = new Object[size]; int index = 0; - Node temp = head; + Node temp = head; while (temp != null) { res[index] = temp.getValue(); index++; @@ -92,7 +95,8 @@ public class MyLinkedList implements MyList } return Arrays.toString(res); } - public static class Node { + + private static class Node { private T value; private Node next; @@ -116,6 +120,4 @@ public class MyLinkedList implements MyList return next; } } - - } \ No newline at end of file diff --git a/app/src/main/java/ru/project/collection/MyList.java b/app/src/main/java/ru/project/collection/MyList.java index e97132e..bd47567 100644 --- a/app/src/main/java/ru/project/collection/MyList.java +++ b/app/src/main/java/ru/project/collection/MyList.java @@ -8,7 +8,7 @@ public interface MyList { T set(int index, T element); - T remove(int index); + void remove(int index); int size(); From 2775c5fc430a0438588038ee30a2358282b5811c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 22 Aug 2026 21:18:44 +0400 Subject: [PATCH 030/110] docs: add guide for dependencies between git branches --- GIT_BRANCH_DEPENDENCIES.ru.md | 122 ++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 GIT_BRANCH_DEPENDENCIES.ru.md diff --git a/GIT_BRANCH_DEPENDENCIES.ru.md b/GIT_BRANCH_DEPENDENCIES.ru.md new file mode 100644 index 0000000..f427e43 --- /dev/null +++ b/GIT_BRANCH_DEPENDENCIES.ru.md @@ -0,0 +1,122 @@ +# Работа с зависимостями между Git-ветками + +## Сценарий + +Предположим, мы находимся в ветке: + +```bash +feature/student +``` + +В этой ветке реализуется функциональность, связанная со `Student`. + +При этом у нас появляется новая, независимая по смыслу фича: + +```bash +feature/sorting +``` + +Для реализации сортировки нам необходим класс или интерфейс `Student`, который уже был создан в `feature/student`. + +## Как создать новую ветку + +Если нам необходимо продолжить разработку `feature/sorting`, используя текущее состояние `feature/student`, создаём новую ветку **от `feature/student`**. + +Сначала переключаемся на `feature/student`: + +```bash +git checkout feature/student +``` + +Затем создаём новую ветку: + +```bash +git branch feature/sorting +``` + +После этого история будет выглядеть примерно так: + +```text + feature/sorting + / +---------------● + \ + feature/student +``` + +Обе ветки на момент создания указывают на один и тот же коммит. + +Теперь в `feature/sorting` доступен весь код, который был добавлен в `feature/student`, в том числе `Student`. + +Чтобы перейти в новую ветку: + +```bash +git checkout feature/sorting +``` + +## Почему важно создавать ветку именно таким образом + +В данном случае `feature/sorting` зависит от кода, находящегося в `feature/student`. + +При этом сами фичи логически разные: + +- `feature/student` — разработка `Student`; +- `feature/sorting` — реализация сортировки; +- `Student` нужен в `feature/sorting` как зависимость для реализации сортировки. + +Поэтому не стоит продолжать разработку сортировки непосредственно в `feature/student`. Лучше создать отдельную ветку от того состояния, где необходимый код уже существует. + +--- + +## Если ветки уже разошлись + +Иногда невозможно создать `feature/sorting` непосредственно от нужного места в истории `feature/student`. + +Например, `feature/sorting` уже была создана раньше: + +```text +A---B---C---D feature/student + \ + E---F---G feature/sorting +``` + +В таком случае можно получить изменения из `feature/student` посредством слияния (merge). + +Находясь в `feature/sorting`, выполняем: + +```bash +git checkout feature/sorting +git merge feature/student +``` + +История станет примерно такой: + +```text +A---B---C---D feature/student + \ \ + E---F---G---M feature/sorting +``` + +Коммит `M` — результат слияния двух веток. + +После этого в `feature/sorting` будут доступны изменения из `feature/student`, в том числе необходимый `Student`. + +## Главное правило + +**Если новая фича зависит от кода из другой ветки, но при этом является самостоятельной по смыслу — создаём для неё отдельную ветку, используя нужную ветку как основу.** + +Если новую ветку можно создать непосредственно от нужного состояния исходной ветки: + +```bash +git checkout feature/student +git branch feature/sorting +``` + +Если ветки уже разошлись и создать её от нужного состояния невозможно: + +```bash +git checkout feature/sorting +git merge feature/student +``` + +Таким образом, мы сохраняем логическое разделение фич и одновременно можем использовать необходимый код из другой ветки. From 99255f392f0d9e7d7020eca6e5bbd2bc88e973d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Sun, 23 Aug 2026 15:17:57 +0300 Subject: [PATCH 031/110] feat(student): add Student class --- .../main/java/ru/project/student/Student.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/main/java/ru/project/student/Student.java 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..121aea5 --- /dev/null +++ b/app/src/main/java/ru/project/student/Student.java @@ -0,0 +1,38 @@ +package ru.project.student; + +public class Student { + private final String groupNumber; + private final double averageGrade; + private final String recordBookNumber; + + Student(String groupNumber, double averageGrade, String recordBookNumber) { + if (groupNumber == null || groupNumber.isBlank()) { + throw new IllegalArgumentException("Group number cannot be empty"); + } + if (averageGrade < 0.0 || averageGrade > 5.0) { + throw new IllegalArgumentException("Average grade must be between 0.0 and 5.0"); + } + if (recordBookNumber == null || recordBookNumber.isBlank()) { + throw new IllegalArgumentException("Record book number cannot be empty"); + } + this.groupNumber = groupNumber; + this.averageGrade = averageGrade; + this.recordBookNumber = recordBookNumber; + } + + public String getGroupNumber() { + return groupNumber; + } + + public double getAverageGrade() { + return averageGrade; + } + + public String getRecordBookNumber() { + return recordBookNumber; + } +@Override + public String toString(){ + return "Student ="+groupNumber+ +} +} From cdd33b6cf80e8917e46828dcc5bac172b60d7f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Sun, 23 Aug 2026 19:41:21 +0300 Subject: [PATCH 032/110] feat(student): update Student class --- .../main/java/ru/project/student/Student.java | 39 ++++++++++++++++--- .../ru/project/student/StudentBuilder.java | 4 ++ .../java/ru/project/student/StudentText.java | 4 ++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/ru/project/student/StudentBuilder.java create mode 100644 app/src/test/java/ru/project/student/StudentText.java diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index 121aea5..2eaa6ed 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -1,5 +1,7 @@ package ru.project.student; +import java.util.Objects; + public class Student { private final String groupNumber; private final double averageGrade; @@ -31,8 +33,35 @@ public class Student { public String getRecordBookNumber() { return recordBookNumber; } -@Override - public String toString(){ - return "Student ="+groupNumber+ -} -} + + @Override + public String toString() { + return "Student{" + + "groupNumber='" + groupNumber + '\'' + + ", averageGrade=" + averageGrade + + ", recordBookNumber='" + recordBookNumber + '\'' + + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + + } + Student student = (Student) obj; + + return Double.compare(averageGrade, student.averageGrade) == 0 + && Objects.equals(groupNumber, student.groupNumber) + && Objects.equals(recordBookNumber, student.recordBookNumber); + } + + @Override + public int hashCode() { + return Objects.hash(groupNumber, averageGrade, recordBookNumber); + } +} \ No newline at end of file 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..a370f1f --- /dev/null +++ b/app/src/main/java/ru/project/student/StudentBuilder.java @@ -0,0 +1,4 @@ +package ru.project.student; + +public class StudentBuilder { +} diff --git a/app/src/test/java/ru/project/student/StudentText.java b/app/src/test/java/ru/project/student/StudentText.java new file mode 100644 index 0000000..cad3bef --- /dev/null +++ b/app/src/test/java/ru/project/student/StudentText.java @@ -0,0 +1,4 @@ +package ru.project.student; + +public class StudentText { +} From 4e53eb03f9c02cfff695c99fdc9e7ba163ab79d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Sun, 23 Aug 2026 19:46:00 +0300 Subject: [PATCH 033/110] feat(student): add StudentBuilder --- .../ru/project/student/StudentBuilder.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ru/project/student/StudentBuilder.java b/app/src/main/java/ru/project/student/StudentBuilder.java index a370f1f..3e1ec41 100644 --- a/app/src/main/java/ru/project/student/StudentBuilder.java +++ b/app/src/main/java/ru/project/student/StudentBuilder.java @@ -1,4 +1,26 @@ package ru.project.student; public class StudentBuilder { -} + private String groupNumber; + private double averageGrade; + private String recordBookNumber; + + public StudentBuilder setGroupNumber(String groupNumber) { + this.groupNumber = groupNumber; + return this; + } + + public StudentBuilder setAverageGrade(double averageGrade) { + this.averageGrade = averageGrade; + return this; + } + + public StudentBuilder setRecordBookNumber(String recordBookNumber) { + this.recordBookNumber = recordBookNumber; + return this; + } + + public Student build() { + return new Student(groupNumber, averageGrade, recordBookNumber); + } +} \ No newline at end of file From 5e71d2788537137313c162b9cd300b664e39d6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Mon, 24 Aug 2026 18:36:58 +0300 Subject: [PATCH 034/110] fix(student): add hashCode check to equals --- app/src/main/java/ru/project/student/Student.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index 2eaa6ed..3e4332f 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -51,8 +51,12 @@ public class Student { 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 From caf2c95dbb295b656f9dae9ae014bb37ee0f319a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Mon, 24 Aug 2026 21:14:09 +0300 Subject: [PATCH 035/110] test(student): add JUnit tests --- .../java/ru/project/student/StudentTest.java | 168 ++++++++++++++++++ .../java/ru/project/student/StudentText.java | 4 - 2 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 app/src/test/java/ru/project/student/StudentTest.java delete mode 100644 app/src/test/java/ru/project/student/StudentText.java 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..e9a7434 --- /dev/null +++ b/app/src/test/java/ru/project/student/StudentTest.java @@ -0,0 +1,168 @@ +package ru.project.student; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + + +class StudentTest { + @Test + void shouldCreateValidStudent() { + Student student = new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + + assertEquals("101", student.getGroupNumber()); + assertEquals(4.5, student.getAverageGrade()); + assertEquals("12345", student.getRecordBookNumber()); + } + + @Test + void shouldThrowExceptionWhenGroupNumberIsEmpty() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber("") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + }); + } + + @Test + void shouldThrowExceptionWhenAverageGradeIsGreaterThanFive() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(6.0) + .setRecordBookNumber("12345") + .build(); + }); + } + + @Test + void shouldThrowExceptionWhenAverageGradeIsLessThanZero() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(-1.0) + .setRecordBookNumber("12345") + .build(); + }); + } + + @Test + void shouldCreateStudentWhenAverageGradeIsZero() { + Student student = new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(0.0) + .setRecordBookNumber("12345") + .build(); + + assertEquals(0.0, student.getAverageGrade()); + } + + @Test + void shouldCreateStudentWhenAverageGradeIsFive() { + Student student = new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(5.0) + .setRecordBookNumber("12345") + .build(); + + assertEquals(5.0, student.getAverageGrade()); + } + + @Test + void shouldThrowExceptionWhenRecordBookNumberIsEmpty() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber("") + .build(); + }); + } + + @Test + void shouldThrowExceptionWhenGroupNumberIsNull() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber(null) + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + }); + } + + @Test + void shouldThrowExceptionWhenGroupNumberIsBlank() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber(" ") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + }); + } + + @Test + void shouldThrowExceptionWhenRecordBookNumberIsNull() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber(null) + .build(); + }); + } + + @Test + void shouldThrowExceptionWhenRecordBookNumberIsBlank() { + assertThrows(IllegalArgumentException.class, () -> { + new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber(" ") + .build(); + }); + } + + @Test + void shouldReturnTrueWhenStudentsAreEqual() { + Student student1 = new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + + Student student2 = new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + + assertEquals(student1, student2); + assertEquals(student1.hashCode(), student2.hashCode()); + } + + @Test + void shouldReturnFalseWhenStudentsAreDifferent() { + Student student1 = new StudentBuilder() + .setGroupNumber("101") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + + Student student2 = new StudentBuilder() + .setGroupNumber("102") + .setAverageGrade(4.5) + .setRecordBookNumber("12345") + .build(); + + assertNotEquals(student1, student2); + } +} diff --git a/app/src/test/java/ru/project/student/StudentText.java b/app/src/test/java/ru/project/student/StudentText.java deleted file mode 100644 index cad3bef..0000000 --- a/app/src/test/java/ru/project/student/StudentText.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.project.student; - -public class StudentText { -} From 7d916344d57f3e36a8a28d1e124af1fba210a362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 11:22:34 +0400 Subject: [PATCH 036/110] docs(git): add force-with-lease recovery guide --- GIT_FORCE_WITH_LEASE_RECOVERY.ru.md | 175 ++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 GIT_FORCE_WITH_LEASE_RECOVERY.ru.md diff --git a/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md b/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md new file mode 100644 index 0000000..8631d4c --- /dev/null +++ b/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md @@ -0,0 +1,175 @@ +# Памятка: как синхронизировать локальную ветку после `git push --force-with-lease` + +Эта инструкция подходит для ситуации, когда удалённая ветка была переписана через `git push --force-with-lease`, и теперь локальная ветка расходится с `origin`. + +## Если локальных изменений нет + +Если у тебя **нет никаких локальных изменений, которые нужно сохранить**, это хорошо: можно безопасно заменить указатель локальной ветки на удалённую. + +В таком случае ничего нужного не пропадёт. + +### 1. Сделай `git fetch` + +```bash +git fetch +``` + +### 2. Посмотри, где расходятся ветки + +```bash +git log --oneline --decorate --color --graph --all +``` + +С помощью графа можно увидеть, где локальная и удалённая ветки расходятся в истории. + +Например: + +```text +feature/student +origin/feature/student +``` + +`feature/student` — локальная ветка, `origin/feature/student` — состояние этой ветки в удалённом репозитории. + +### 3. Перемести указатель локальной ветки на удалённую + +Сначала перейди на нужную локальную ветку: + +```bash +git checkout feature/student +``` + +Затем: + +```bash +git reset --hard origin/feature/student +``` + +После этого локальная `feature/student` будет указывать на тот же коммит, что и `origin/feature/student`. + +### Весь сценарий целиком + +```bash +git fetch +git log --oneline --decorate --color --graph --all +git checkout feature/student +git reset --hard origin/feature/student +``` + +--- + +# Если локальные изменения нужно сохранить + +В этом случае **не нужно сразу делать `git reset --hard`**. + +`reset --hard` изменит состояние рабочей директории и может удалить незакоммиченные изменения. + +Сначала нужно сохранить свою работу. + +## Вариант 1. Изменения уже закоммичены + +Если твои изменения находятся в локальных коммитах, самый простой и надёжный вариант — сначала создать дополнительную ветку, которая будет указывать на текущее состояние: + +```bash +git checkout feature/student +git branch backup/feature-student +``` + +Теперь у тебя есть резервная ветка: + +```text +backup/feature-student + ↓ + твои старые коммиты + +feature/student + ↓ + твои старые коммиты +``` + +После этого можно безопаснее двигать `feature/student`: + +```bash +git fetch +git reset --hard origin/feature/student +``` + +Резервная ветка `backup/feature-student` при этом никуда не исчезнет. + +Если позже выяснится, что какие-то коммиты из старой истории нужны, их можно перенести в новую историю с помощью `git cherry-pick`. + +Например: + +```bash +git cherry-pick +``` + +## Вариант 2. Есть незакоммиченные изменения + +Если ты что-то изменил, но ещё не сделал `git commit`, сначала сохрани изменения. + +Один из удобных вариантов — временно убрать их в `stash`: + +```bash +git stash push -u -m "backup before force-push recovery" +``` + +После этого рабочая директория станет чистой, и можно выполнять: + +```bash +git fetch +git checkout feature/student +git reset --hard origin/feature/student +``` + +Затем можно вернуть сохранённые изменения: + +```bash +git stash pop +``` + +Git попытается применить их поверх новой истории. + +Если возникнут конфликты, их нужно будет разрешить вручную. + +--- + +# Самое важное правило + +Перед: + +```bash +git reset --hard origin/feature/student +``` + +нужно ответить на вопрос: + +> **Есть ли у меня локальные изменения, которые я не хочу потерять?** + +### Если нет + +Можно: + +```bash +git reset --hard origin/feature/student +``` + +### Если да, но изменения закоммичены + +Сначала создай резервную ветку: + +```bash +git branch backup/feature-student +``` + +### Если да, но изменения ещё не закоммичены + +Сначала используй `stash`: + +```bash +git stash push -u -m "backup before force-push recovery" +``` + +И только после этого выполняй `reset --hard`. + +**Принцип простой: сначала сохранить то, что может понадобиться, потом двигать указатель ветки.** From 2a3700b6c4abf0ce9d02d1d5f7b1084a2fddb572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 12:36:38 +0400 Subject: [PATCH 037/110] refactor: separate collection state from UI navigation --- app/src/main/java/ru/project/App.java | 11 ++++- app/src/main/java/ru/project/AppState.java | 18 +++++++ .../java/ru/project/CollectionController.java | 49 +++++++++++++++++++ .../main/java/ru/project/DataCollection.java | 47 ++++++++++++++++++ .../java/ru/project/ui/CollectionView.java | 4 +- .../ru/project/ui/CountOccurrencesMenu.java | 4 +- .../ru/project/ui/CreateCollectionMenu.java | 4 +- .../main/java/ru/project/ui/Navigator.java | 17 ++++--- .../ru/project/ui/SaveCollectionMenu.java | 4 +- .../java/ru/project/ui/SearchElementMenu.java | 4 +- .../ru/project/ui/SortCollectionMenu.java | 4 +- 11 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/ru/project/AppState.java create mode 100644 app/src/main/java/ru/project/CollectionController.java create mode 100644 app/src/main/java/ru/project/DataCollection.java diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index bf57afc..646773c 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -21,9 +21,18 @@ public final class App { try { MultiWindowTextGUI gui = new MultiWindowTextGUI(screen); - Navigator navigator = new Navigator(gui); + // Application state + AppState state = new AppState(); + // Controllers + CollectionController collectionController = new CollectionController(state); + + // Navigation + Navigator navigator = new Navigator(gui, collectionController); + + // Application entry point gui.addWindowAndWait(new MainMenu(navigator)); + } finally { screen.stopScreen(); } diff --git a/app/src/main/java/ru/project/AppState.java b/app/src/main/java/ru/project/AppState.java new file mode 100644 index 0000000..144f857 --- /dev/null +++ b/app/src/main/java/ru/project/AppState.java @@ -0,0 +1,18 @@ +package ru.project; + +public class AppState { + + private DataCollection collection; + + public DataCollection getCollection() { + return collection; + } + + public void setCollection(DataCollection collection) { + this.collection = collection; + } + + public boolean hasCollection() { + return collection != null; + } +} diff --git a/app/src/main/java/ru/project/CollectionController.java b/app/src/main/java/ru/project/CollectionController.java new file mode 100644 index 0000000..8cf3deb --- /dev/null +++ b/app/src/main/java/ru/project/CollectionController.java @@ -0,0 +1,49 @@ +package ru.project; + +public class CollectionController { + + private final AppState state; + + public CollectionController(AppState state) { + this.state = state; + } + + public void createCollection() { + DataCollection collection = new DataCollection(); + + collection.add("Charlie"); + collection.add("Alice"); + collection.add("Bob"); + collection.add("Alice"); + + state.setCollection(collection); + } + + public DataCollection getCollection() { + return state.getCollection(); + } + + public boolean hasCollection() { + return state.hasCollection(); + } + + public void sortCollection() { + if (!state.hasCollection()) { + return; + } + + state.getCollection().sort(); + } + + public boolean contains(String element) { + return state.hasCollection() && state.getCollection().contains(element); + } + + public int countOccurrences(String element) { + if (!state.hasCollection()) { + return 0; + } + + return state.getCollection().count(element); + } +} diff --git a/app/src/main/java/ru/project/DataCollection.java b/app/src/main/java/ru/project/DataCollection.java new file mode 100644 index 0000000..bd5553a --- /dev/null +++ b/app/src/main/java/ru/project/DataCollection.java @@ -0,0 +1,47 @@ +package ru.project; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DataCollection { + + private final List elements = new ArrayList<>(); + + public void add(String element) { + elements.add(element); + } + + public void sort() { + Collections.sort(elements); + } + + public boolean contains(String element) { + return elements.contains(element); + } + + public int count(String element) { + int count = 0; + + for (String current : elements) { + if (current.equals(element)) { + count++; + } + } + + return count; + } + + public List getElements() { + return Collections.unmodifiableList(elements); + } + + @Override + public String toString() { + if (elements.isEmpty()) { + return "Коллекция пуста"; + } + + return String.join(", ", elements); + } +} diff --git a/app/src/main/java/ru/project/ui/CollectionView.java b/app/src/main/java/ru/project/ui/CollectionView.java index 82b9540..ddad8cc 100644 --- a/app/src/main/java/ru/project/ui/CollectionView.java +++ b/app/src/main/java/ru/project/ui/CollectionView.java @@ -1,8 +1,10 @@ package ru.project.ui; +import ru.project.CollectionController; + public class CollectionView extends EmptyMenu { - public CollectionView() { + public CollectionView(CollectionController controller) { super("Текущая коллекция"); } } diff --git a/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java b/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java index db8e0c8..5ef70e5 100644 --- a/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java +++ b/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java @@ -1,8 +1,10 @@ package ru.project.ui; +import ru.project.CollectionController; + public class CountOccurrencesMenu extends EmptyMenu { - public CountOccurrencesMenu() { + public CountOccurrencesMenu(CollectionController controller) { super("Подсчёт количества вхождений"); } } diff --git a/app/src/main/java/ru/project/ui/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/CreateCollectionMenu.java index 6ea56fc..69cfd2d 100644 --- a/app/src/main/java/ru/project/ui/CreateCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/CreateCollectionMenu.java @@ -1,8 +1,10 @@ package ru.project.ui; +import ru.project.CollectionController; + public class CreateCollectionMenu extends EmptyMenu { - public CreateCollectionMenu() { + public CreateCollectionMenu(CollectionController controller) { super("Создание новой коллекции"); } } diff --git a/app/src/main/java/ru/project/ui/Navigator.java b/app/src/main/java/ru/project/ui/Navigator.java index 13489c1..873a1a4 100644 --- a/app/src/main/java/ru/project/ui/Navigator.java +++ b/app/src/main/java/ru/project/ui/Navigator.java @@ -2,13 +2,16 @@ package ru.project.ui; import com.googlecode.lanterna.gui2.BasicWindow; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.CollectionController; public class Navigator { private final WindowBasedTextGUI gui; + private final CollectionController collectionController; - public Navigator(WindowBasedTextGUI gui) { + public Navigator(WindowBasedTextGUI gui, CollectionController collectionController) { this.gui = gui; + this.collectionController = collectionController; } public void show(BasicWindow window) { @@ -16,26 +19,26 @@ public class Navigator { } public void showCollectionView() { - show(new CollectionView()); + show(new CollectionView(collectionController)); } public void showCreateCollection() { - show(new CreateCollectionMenu()); + show(new CreateCollectionMenu(collectionController)); } public void showSaveCollection() { - show(new SaveCollectionMenu()); + show(new SaveCollectionMenu(collectionController)); } public void showSortCollection() { - show(new SortCollectionMenu()); + show(new SortCollectionMenu(collectionController)); } public void showSearchElement() { - show(new SearchElementMenu()); + show(new SearchElementMenu(collectionController)); } public void showCountOccurrences() { - show(new CountOccurrencesMenu()); + show(new CountOccurrencesMenu(collectionController)); } } diff --git a/app/src/main/java/ru/project/ui/SaveCollectionMenu.java b/app/src/main/java/ru/project/ui/SaveCollectionMenu.java index cc0f36a..9f7d2e4 100644 --- a/app/src/main/java/ru/project/ui/SaveCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/SaveCollectionMenu.java @@ -1,8 +1,10 @@ package ru.project.ui; +import ru.project.CollectionController; + public class SaveCollectionMenu extends EmptyMenu { - public SaveCollectionMenu() { + public SaveCollectionMenu(CollectionController controller) { super("Сохранение текущей коллекции"); } } diff --git a/app/src/main/java/ru/project/ui/SearchElementMenu.java b/app/src/main/java/ru/project/ui/SearchElementMenu.java index 7e92c4f..b185219 100644 --- a/app/src/main/java/ru/project/ui/SearchElementMenu.java +++ b/app/src/main/java/ru/project/ui/SearchElementMenu.java @@ -1,8 +1,10 @@ package ru.project.ui; +import ru.project.CollectionController; + public class SearchElementMenu extends EmptyMenu { - public SearchElementMenu() { + public SearchElementMenu(CollectionController controller) { super("Поиск элемента"); } } diff --git a/app/src/main/java/ru/project/ui/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/SortCollectionMenu.java index 68e6a7a..99bc1a8 100644 --- a/app/src/main/java/ru/project/ui/SortCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/SortCollectionMenu.java @@ -1,8 +1,10 @@ package ru.project.ui; +import ru.project.CollectionController; + public class SortCollectionMenu extends EmptyMenu { - public SortCollectionMenu() { + public SortCollectionMenu(CollectionController controller) { super("Сортировка текущей коллекции"); } } From 5ae40ff089c53df213022ccdbd33bee39e01a3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 16:09:38 +0400 Subject: [PATCH 038/110] docs: clarify menu hierarchy with explicit nesting --- docs/MENU.ru.txt | 185 +++++++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 64 deletions(-) diff --git a/docs/MENU.ru.txt b/docs/MENU.ru.txt index ad703cd..0be1048 100644 --- a/docs/MENU.ru.txt +++ b/docs/MENU.ru.txt @@ -1,73 +1,130 @@ -ГЛАВНОЕ МЕНЮ +Главное меню +{ + Показать текущую коллекцию -Показать текущую коллекцию + Меню создания новой коллекции + { + Вернуться -Меню создания новой коллекции - Вернуться - Задать размер - Настроить способ заполнения - Пользовательские данные - Задать поле 'Название A' - Задать поле 'Название B' - Задать поле 'Название C' - Задать индекс изменяемого объекта - Изменить данные объекта - Предпросмотр - Случайные данные - Настройка диапазона генерации - Поле 'Название A' - Задать минимальное значение - Задать максимальное значение - Поле 'Название B' - Задать минимальное значение - Задать максимальное значение - Поле 'Название C' - Задать минимальное значение - Задать максимальное значение - Данные из файла - Задать путь до файла + Задать размер + + Настроить способ заполнения + { + Пользовательские данные + { + Задать поле 'Название A' + Задать поле 'Название B' + Задать поле 'Название C' + Задать индекс изменяемого объекта + Изменить данные объекта + Предпросмотр + } + + Случайные данные + { + Настройка диапазона генерации + { + Поле 'Название A' + { + Задать минимальное значение + Задать максимальное значение + } + + Поле 'Название B' + { + Задать минимальное значение + Задать максимальное значение + } + + Поле 'Название C' + { + Задать минимальное значение + Задать максимальное значение + } + } + } + + Данные из файла + { + Задать путь до файла + { + Абсолютный + Относительный + } + } + } + + Выбрать способ заполнения + { + Пользовательские данные * + Случайные данные + Данные из файла + } + + Создать новую коллекцию + } + + Меню сохранения текущей коллекции в файл + { + Вывести текущий абсолютный путь до файла + + Задать путь до файла + { Абсолютный Относительный - Выбрать способ заполнения - Пользовательские данные * - Случайные данные - Данные из файла - Создать новую коллекцию + } -Меню сохранения текущей коллекции в файл - Вывести текущий абсолютный путь до файла - Задать путь до файла - Абсолютный - Относительный - Дописать коллекцию в текущий файл + Дописать коллекцию в текущий файл + } -Меню сортировки текущей коллекции - Вернуться - Настроить способ сортировки - Обычная сортировка - Сортировать по полю 'Название A' - Сортировать по полю 'Название B' - Сортировать по полю 'Название C' -Настроить способ сортировки - Сортировка по чётности - Выбрать числовое поле 'Название A' - Выбрать числовое поле 'Название B' -Выбрать способ сортировки - Обычная сортировка * - Сортировка по чётности -Сортировать текущую коллекцию + Меню сортировки текущей коллекции + { + Вернуться -Меню поиска элемента в текущей коллекции - Вернуться - Задать значение поля 'Название A' - Задать значение поля 'Название B' - Задать значение поля 'Название C' - Найти первое вхождение в текущей коллекции + Настроить способ сортировки + { + Обычная сортировка + { + Сортировать по полю 'Название A' + Сортировать по полю 'Название B' + Сортировать по полю 'Название C' + } + } -Меню подсчёта количества вхождений элемента - Задать значение поля 'Название A' - Задать значение поля 'Название B' - Задать значение поля 'Название C' - Выполнить многопоточный подсчёт + Настроить способ сортировки + { + Сортировка по чётности + { + Выбрать числовое поле 'Название A' + Выбрать числовое поле 'Название B' + } + } -Завершить выполнение программы + Выбрать способ сортировки + { + Обычная сортировка * + Сортировка по чётности + } + + Сортировать текущую коллекцию + } + + Меню поиска элемента в текущей коллекции + { + Вернуться + Задать значение поля 'Название A' + Задать значение поля 'Название B' + Задать значение поля 'Название C' + Найти первое вхождение в текущей коллекции + } + + Меню подсчёта количества вхождений элемента + { + Задать значение поля 'Название A' + Задать значение поля 'Название B' + Задать значение поля 'Название C' + Выполнить многопоточный подсчёт + } + + Завершить выполнение программы +} From 2f9e681b54b6f0794c333813277ab83f5d4d6efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Wed, 26 Aug 2026 15:57:17 +0300 Subject: [PATCH 039/110] fix: add missing imports for MyList, Student and Comparator in sorting package --- app/src/main/java/ru/project/sorting/MergeSortStrategy.java | 4 +++- app/src/main/java/ru/project/sorting/SortStrategy.java | 3 ++- .../ru/project/sorting/StudentAvarageGradeComparator.java | 3 ++- .../main/java/ru/project/sorting/StudentGroupComparator.java | 3 ++- .../java/ru/project/sorting/StudentRecordBookComparator.java | 3 ++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java index 86655f7..ab22562 100644 --- a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java +++ b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java @@ -1,4 +1,6 @@ -package sorting; +package ru.project.sorting; + +import ru.project.list.MyList; import java.util.Comparator; diff --git a/app/src/main/java/ru/project/sorting/SortStrategy.java b/app/src/main/java/ru/project/sorting/SortStrategy.java index f252317..7dc9983 100644 --- a/app/src/main/java/ru/project/sorting/SortStrategy.java +++ b/app/src/main/java/ru/project/sorting/SortStrategy.java @@ -1,5 +1,6 @@ -package sorting; +package ru.project.sorting; +import ru.project.list.MyList; import java.util.Comparator; public interface SortStrategy { diff --git a/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java b/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java index 1946fd4..a4ca818 100644 --- a/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java @@ -1,5 +1,6 @@ -package sorting; +package ru.project.sorting; +import ru.project.model.Student; import java.util.Comparator; public class StudentAvarageGradeComparator implements Comparator { diff --git a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java index b1efd93..a43c0e1 100644 --- a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java @@ -1,5 +1,6 @@ -package sorting; +package ru.project.sorting; +import ru.project.model.Student; import java.util.Comparator; public class StudentGroupComparator implements Comparator { diff --git a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java index 22b73e4..fd72fb4 100644 --- a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java @@ -1,5 +1,6 @@ -package sorting; +package ru.project.sorting; +import ru.project.model.Student; import java.util.Comparator; public class StudentRecordBookComparator implements Comparator { From 2616603af4256cf4047cf711d88504bec6b264df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Wed, 26 Aug 2026 16:00:37 +0300 Subject: [PATCH 040/110] fix: rename StudentAvarageGradeComparator to StudentAverageGradeComparator --- ...radeComparator.java => StudentAverageGradeComparator.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename app/src/main/java/ru/project/sorting/{StudentAvarageGradeComparator.java => StudentAverageGradeComparator.java} (59%) diff --git a/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java similarity index 59% rename from app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java rename to app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java index a4ca818..488aff9 100644 --- a/app/src/main/java/ru/project/sorting/StudentAvarageGradeComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java @@ -3,10 +3,10 @@ package ru.project.sorting; import ru.project.model.Student; import java.util.Comparator; -public class StudentAvarageGradeComparator implements Comparator { +public class StudentAverageGradeComparator implements Comparator { @Override public int compare(Student s1, Student s2) { - return Double.compare(s1.getAvarageGrade(), s2.getAvarageGrade()); + return Double.compare(s1.getAverageGrade(), s2.getAverageGrade()); } } \ No newline at end of file From 3a77ce7d889a6b205cb8bfac8ffd6d7e864399dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 18:58:39 +0400 Subject: [PATCH 041/110] style: format Java code with google-java-format --- .../ru/project/collection/MyLinkedList.java | 217 +++++++++--------- .../main/java/ru/project/student/Student.java | 119 +++++----- .../ru/project/student/StudentBuilder.java | 38 +-- 3 files changed, 188 insertions(+), 186 deletions(-) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 23b2b75..23bbad0 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -1,123 +1,120 @@ +package ru.project.collection; + import java.util.Arrays; -public class MyLinkedList implements MyList -{ - private Node head; - private int size; - 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++; - } - 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++; - } +public class MyLinkedList implements MyList { + private Node head; + private int size; - } - throw new IllegalArgumentException(); + 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)); } - public void remove(int index) - { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(); - } - if (index == 0) - { - head = head.getNext(); - size--; - return; + size++; + } - } - 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++; - } - } - } - public T set(int index, T value) { - if (index < 0 || index >= size) { - throw new IllegalArgumentException(); - } + public T get(int index) { + int currentIndex = 0; + Node temp = head; + while (temp != null) { + if (currentIndex == index) { + return temp.getValue(); - Node temp = head; - for (int i = 0; i < index; i++) { - temp = temp.getNext(); - } + } else { + temp = temp.getNext(); + currentIndex++; + } + } + throw new IllegalArgumentException(); + } - T oldValue = temp.getValue(); - temp.setValue(value); - return oldValue; + public void remove(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); } - public int size() { - return size; + if (index == 0) { + head = head.getNext(); + size--; + return; } - public boolean isEmpty() - { - return size == 0; + 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 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); + } + + public T set(int index, T value) { + if (index < 0 || index >= size) { + throw new IllegalArgumentException(); } - 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; - } + Node temp = head; + for (int i = 0; i < index; i++) { + temp = temp.getNext(); } -} \ No newline at end of file + + T oldValue = temp.getValue(); + temp.setValue(value); + return oldValue; + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return size == 0; + } + + @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); + } + + 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/student/Student.java b/app/src/main/java/ru/project/student/Student.java index 3e4332f..f433387 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -3,69 +3,74 @@ package ru.project.student; import java.util.Objects; public class Student { - private final String groupNumber; - private final double averageGrade; - private final String recordBookNumber; + private final String groupNumber; + private final double averageGrade; + private final String recordBookNumber; - Student(String groupNumber, double averageGrade, String recordBookNumber) { - if (groupNumber == null || groupNumber.isBlank()) { - throw new IllegalArgumentException("Group number cannot be empty"); - } - if (averageGrade < 0.0 || averageGrade > 5.0) { - throw new IllegalArgumentException("Average grade must be between 0.0 and 5.0"); - } - if (recordBookNumber == null || recordBookNumber.isBlank()) { - throw new IllegalArgumentException("Record book number cannot be empty"); - } - this.groupNumber = groupNumber; - this.averageGrade = averageGrade; - this.recordBookNumber = recordBookNumber; + Student(String groupNumber, double averageGrade, String recordBookNumber) { + if (groupNumber == null || groupNumber.isBlank()) { + throw new IllegalArgumentException("Group number cannot be empty"); + } + if (averageGrade < 0.0 || averageGrade > 5.0) { + throw new IllegalArgumentException("Average grade must be between 0.0 and 5.0"); + } + if (recordBookNumber == null || recordBookNumber.isBlank()) { + throw new IllegalArgumentException("Record book number cannot be empty"); + } + this.groupNumber = groupNumber; + this.averageGrade = averageGrade; + this.recordBookNumber = recordBookNumber; + } + + public String getGroupNumber() { + return groupNumber; + } + + public double getAverageGrade() { + return averageGrade; + } + + public String getRecordBookNumber() { + return recordBookNumber; + } + + @Override + public String toString() { + return "Student{" + + "groupNumber='" + + groupNumber + + '\'' + + ", averageGrade=" + + averageGrade + + ", recordBookNumber='" + + recordBookNumber + + '\'' + + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; } - public String getGroupNumber() { - return groupNumber; + if (obj == null || getClass() != obj.getClass()) { + return false; } - public double getAverageGrade() { - return averageGrade; + if (this.hashCode() != obj.hashCode()) { + return false; } - public String getRecordBookNumber() { - return recordBookNumber; - } + Student student = (Student) obj; - @Override - public String toString() { - return "Student{" + - "groupNumber='" + groupNumber + '\'' + - ", averageGrade=" + averageGrade + - ", recordBookNumber='" + recordBookNumber + '\'' + - '}'; - } + return Double.compare(averageGrade, student.averageGrade) == 0 + && Objects.equals(groupNumber, student.groupNumber) + && Objects.equals(recordBookNumber, student.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 - && Objects.equals(groupNumber, student.groupNumber) - && Objects.equals(recordBookNumber, student.recordBookNumber); - } - - @Override - public int hashCode() { - return Objects.hash(groupNumber, averageGrade, recordBookNumber); - } -} \ No newline at end of file + @Override + public int hashCode() { + return Objects.hash(groupNumber, averageGrade, recordBookNumber); + } +} diff --git a/app/src/main/java/ru/project/student/StudentBuilder.java b/app/src/main/java/ru/project/student/StudentBuilder.java index 3e1ec41..af67320 100644 --- a/app/src/main/java/ru/project/student/StudentBuilder.java +++ b/app/src/main/java/ru/project/student/StudentBuilder.java @@ -1,26 +1,26 @@ package ru.project.student; public class StudentBuilder { - private String groupNumber; - private double averageGrade; - private String recordBookNumber; + private String groupNumber; + private double averageGrade; + private String recordBookNumber; - public StudentBuilder setGroupNumber(String groupNumber) { - this.groupNumber = groupNumber; - return this; - } + public StudentBuilder setGroupNumber(String groupNumber) { + this.groupNumber = groupNumber; + return this; + } - public StudentBuilder setAverageGrade(double averageGrade) { - this.averageGrade = averageGrade; - return this; - } + public StudentBuilder setAverageGrade(double averageGrade) { + this.averageGrade = averageGrade; + return this; + } - public StudentBuilder setRecordBookNumber(String recordBookNumber) { - this.recordBookNumber = recordBookNumber; - return this; - } + public StudentBuilder setRecordBookNumber(String recordBookNumber) { + this.recordBookNumber = recordBookNumber; + return this; + } - public Student build() { - return new Student(groupNumber, averageGrade, recordBookNumber); - } -} \ No newline at end of file + public Student build() { + return new Student(groupNumber, averageGrade, recordBookNumber); + } +} From 1ee2fc3d062068171b6c7372f7375acd7462b428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 19:01:39 +0400 Subject: [PATCH 042/110] refactor: restructure application using MVP architecture --- app/src/main/java/ru/project/App.java | 41 ++++++----- app/src/main/java/ru/project/AppState.java | 18 ----- .../java/ru/project/CollectionController.java | 49 ------------- .../main/java/ru/project/DataCollection.java | 47 ------------- .../main/java/ru/project/model/AppState.java | 32 +++++++++ .../presenter/CollectionPresenter.java | 31 ++++++++ .../ru/project/presenter/MainPresenter.java | 70 +++++++++++++++++++ .../java/ru/project/ui/CollectionView.java | 10 --- .../ru/project/ui/CountOccurrencesMenu.java | 10 --- .../ru/project/ui/CreateCollectionMenu.java | 10 --- .../main/java/ru/project/ui/EmptyMenu.java | 23 ------ app/src/main/java/ru/project/ui/MainMenu.java | 34 --------- .../main/java/ru/project/ui/Navigator.java | 44 ------------ .../ru/project/ui/SaveCollectionMenu.java | 10 --- .../java/ru/project/ui/SearchElementMenu.java | 10 --- .../ru/project/ui/SortCollectionMenu.java | 10 --- .../java/ru/project/view/CollectionView.java | 47 +++++++++++++ .../main/java/ru/project/view/MainView.java | 62 ++++++++++++++++ 18 files changed, 266 insertions(+), 292 deletions(-) delete mode 100644 app/src/main/java/ru/project/AppState.java delete mode 100644 app/src/main/java/ru/project/CollectionController.java delete mode 100644 app/src/main/java/ru/project/DataCollection.java create mode 100644 app/src/main/java/ru/project/model/AppState.java create mode 100644 app/src/main/java/ru/project/presenter/CollectionPresenter.java create mode 100644 app/src/main/java/ru/project/presenter/MainPresenter.java delete mode 100644 app/src/main/java/ru/project/ui/CollectionView.java delete mode 100644 app/src/main/java/ru/project/ui/CountOccurrencesMenu.java delete mode 100644 app/src/main/java/ru/project/ui/CreateCollectionMenu.java delete mode 100644 app/src/main/java/ru/project/ui/EmptyMenu.java delete mode 100644 app/src/main/java/ru/project/ui/MainMenu.java delete mode 100644 app/src/main/java/ru/project/ui/Navigator.java delete mode 100644 app/src/main/java/ru/project/ui/SaveCollectionMenu.java delete mode 100644 app/src/main/java/ru/project/ui/SearchElementMenu.java delete mode 100644 app/src/main/java/ru/project/ui/SortCollectionMenu.java create mode 100644 app/src/main/java/ru/project/view/CollectionView.java create mode 100644 app/src/main/java/ru/project/view/MainView.java diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index 646773c..f7c48d9 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -2,39 +2,46 @@ package ru.project; import com.googlecode.lanterna.gui2.MultiWindowTextGUI; import com.googlecode.lanterna.screen.Screen; -import com.googlecode.lanterna.screen.TerminalScreen; import com.googlecode.lanterna.terminal.DefaultTerminalFactory; -import ru.project.ui.MainMenu; -import ru.project.ui.Navigator; +import ru.project.model.AppState; +import ru.project.presenter.MainPresenter; +import ru.project.view.MainView; public final class App { private App() {} - public static void main(String[] args) throws Exception { - DefaultTerminalFactory factory = new DefaultTerminalFactory(); - - Screen screen = new TerminalScreen(factory.createTerminal()); - - screen.startScreen(); + public static void main(String[] args) { + Screen screen = null; try { + DefaultTerminalFactory terminalFactory = new DefaultTerminalFactory(); + + screen = terminalFactory.createScreen(); + screen.startScreen(); + MultiWindowTextGUI gui = new MultiWindowTextGUI(screen); - // Application state AppState state = new AppState(); + MainView view = new MainView(); - // Controllers - CollectionController collectionController = new CollectionController(state); + MainPresenter presenter = new MainPresenter(state, view, gui); - // Navigation - Navigator navigator = new Navigator(gui, collectionController); + presenter.start(); - // Application entry point - gui.addWindowAndWait(new MainMenu(navigator)); + } catch (Exception e) { + System.err.println("Произошла ошибка при выполнении программы:"); + e.printStackTrace(); } finally { - screen.stopScreen(); + if (screen != null) { + try { + screen.stopScreen(); + } catch (Exception e) { + System.err.println("Не удалось корректно завершить работу терминала:"); + e.printStackTrace(); + } + } } } } diff --git a/app/src/main/java/ru/project/AppState.java b/app/src/main/java/ru/project/AppState.java deleted file mode 100644 index 144f857..0000000 --- a/app/src/main/java/ru/project/AppState.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.project; - -public class AppState { - - private DataCollection collection; - - public DataCollection getCollection() { - return collection; - } - - public void setCollection(DataCollection collection) { - this.collection = collection; - } - - public boolean hasCollection() { - return collection != null; - } -} diff --git a/app/src/main/java/ru/project/CollectionController.java b/app/src/main/java/ru/project/CollectionController.java deleted file mode 100644 index 8cf3deb..0000000 --- a/app/src/main/java/ru/project/CollectionController.java +++ /dev/null @@ -1,49 +0,0 @@ -package ru.project; - -public class CollectionController { - - private final AppState state; - - public CollectionController(AppState state) { - this.state = state; - } - - public void createCollection() { - DataCollection collection = new DataCollection(); - - collection.add("Charlie"); - collection.add("Alice"); - collection.add("Bob"); - collection.add("Alice"); - - state.setCollection(collection); - } - - public DataCollection getCollection() { - return state.getCollection(); - } - - public boolean hasCollection() { - return state.hasCollection(); - } - - public void sortCollection() { - if (!state.hasCollection()) { - return; - } - - state.getCollection().sort(); - } - - public boolean contains(String element) { - return state.hasCollection() && state.getCollection().contains(element); - } - - public int countOccurrences(String element) { - if (!state.hasCollection()) { - return 0; - } - - return state.getCollection().count(element); - } -} diff --git a/app/src/main/java/ru/project/DataCollection.java b/app/src/main/java/ru/project/DataCollection.java deleted file mode 100644 index bd5553a..0000000 --- a/app/src/main/java/ru/project/DataCollection.java +++ /dev/null @@ -1,47 +0,0 @@ -package ru.project; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class DataCollection { - - private final List elements = new ArrayList<>(); - - public void add(String element) { - elements.add(element); - } - - public void sort() { - Collections.sort(elements); - } - - public boolean contains(String element) { - return elements.contains(element); - } - - public int count(String element) { - int count = 0; - - for (String current : elements) { - if (current.equals(element)) { - count++; - } - } - - return count; - } - - public List getElements() { - return Collections.unmodifiableList(elements); - } - - @Override - public String toString() { - if (elements.isEmpty()) { - return "Коллекция пуста"; - } - - return String.join(", ", elements); - } -} 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..9b2a61e --- /dev/null +++ b/app/src/main/java/ru/project/model/AppState.java @@ -0,0 +1,32 @@ +package ru.project.model; + +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +public class AppState { + + private MyList collection; + private String currentFilePath; + + public AppState() { + this.collection = new MyLinkedList<>(); + this.currentFilePath = null; + } + + public MyList getCollection() { + return collection; + } + + public void setCollection(MyList collection) { + this.collection = collection; + } + + public String getCurrentFilePath() { + return currentFilePath; + } + + public void setCurrentFilePath(String currentFilePath) { + this.currentFilePath = currentFilePath; + } +} diff --git a/app/src/main/java/ru/project/presenter/CollectionPresenter.java b/app/src/main/java/ru/project/presenter/CollectionPresenter.java new file mode 100644 index 0000000..f9c6606 --- /dev/null +++ b/app/src/main/java/ru/project/presenter/CollectionPresenter.java @@ -0,0 +1,31 @@ +package ru.project.presenter; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.collection.MyList; +import ru.project.model.AppState; +import ru.project.student.Student; +import ru.project.view.CollectionView; + +public class CollectionPresenter { + + private final AppState state; + private final CollectionView view; + private final WindowBasedTextGUI gui; + + public CollectionPresenter(AppState state, CollectionView view, WindowBasedTextGUI gui) { + this.state = state; + this.view = view; + this.gui = gui; + } + + public void start() { + MyList collection = state.getCollection(); + + if (collection == null || collection.isEmpty()) { + view.showEmptyMessage(gui); + return; + } + + view.showCollection(gui, collection); + } +} diff --git a/app/src/main/java/ru/project/presenter/MainPresenter.java b/app/src/main/java/ru/project/presenter/MainPresenter.java new file mode 100644 index 0000000..91b195b --- /dev/null +++ b/app/src/main/java/ru/project/presenter/MainPresenter.java @@ -0,0 +1,70 @@ +package ru.project.presenter; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import com.googlecode.lanterna.gui2.dialogs.MessageDialog; +import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton; +import ru.project.model.AppState; +import ru.project.view.CollectionView; +import ru.project.view.MainView; + +public class MainPresenter { + + private final AppState state; + private final MainView view; + private final WindowBasedTextGUI gui; + private final CollectionPresenter collectionPresenter; + + public MainPresenter(AppState state, MainView view, WindowBasedTextGUI gui) { + this.state = state; + this.view = view; + this.gui = gui; + this.collectionPresenter = new CollectionPresenter(state, new CollectionView(), gui); + } + + public void start() { + bindActions(); + view.show(gui); + } + + private void bindActions() { + view.setShowCollectionAction(this::showCollection); + view.setCreateCollectionAction(this::createCollection); + view.setSaveCollectionAction(this::saveCollection); + view.setSortCollectionAction(this::sortCollection); + view.setSearchCollectionAction(this::searchCollection); + view.setCountOccurrencesAction(this::countOccurrences); + view.setExitAction(this::exit); + } + + private void showCollection() { + collectionPresenter.start(); + } + + private void createCollection() { + showMessage("Меню создания коллекции."); + } + + private void saveCollection() { + showMessage("Меню сохранения коллекции."); + } + + private void sortCollection() { + showMessage("Меню сортировки коллекции."); + } + + private void searchCollection() { + showMessage("Меню поиска элемента."); + } + + private void countOccurrences() { + showMessage("Меню подсчёта количества вхождений."); + } + + private void exit() { + view.close(); + } + + private void showMessage(String message) { + MessageDialog.showMessageDialog(gui, "Информация", message, MessageDialogButton.OK); + } +} diff --git a/app/src/main/java/ru/project/ui/CollectionView.java b/app/src/main/java/ru/project/ui/CollectionView.java deleted file mode 100644 index ddad8cc..0000000 --- a/app/src/main/java/ru/project/ui/CollectionView.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.project.ui; - -import ru.project.CollectionController; - -public class CollectionView extends EmptyMenu { - - public CollectionView(CollectionController controller) { - super("Текущая коллекция"); - } -} diff --git a/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java b/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java deleted file mode 100644 index 5ef70e5..0000000 --- a/app/src/main/java/ru/project/ui/CountOccurrencesMenu.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.project.ui; - -import ru.project.CollectionController; - -public class CountOccurrencesMenu extends EmptyMenu { - - public CountOccurrencesMenu(CollectionController controller) { - super("Подсчёт количества вхождений"); - } -} diff --git a/app/src/main/java/ru/project/ui/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/CreateCollectionMenu.java deleted file mode 100644 index 69cfd2d..0000000 --- a/app/src/main/java/ru/project/ui/CreateCollectionMenu.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.project.ui; - -import ru.project.CollectionController; - -public class CreateCollectionMenu extends EmptyMenu { - - public CreateCollectionMenu(CollectionController controller) { - super("Создание новой коллекции"); - } -} diff --git a/app/src/main/java/ru/project/ui/EmptyMenu.java b/app/src/main/java/ru/project/ui/EmptyMenu.java deleted file mode 100644 index 0385780..0000000 --- a/app/src/main/java/ru/project/ui/EmptyMenu.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.project.ui; - -import com.googlecode.lanterna.gui2.BasicWindow; -import com.googlecode.lanterna.gui2.Button; -import com.googlecode.lanterna.gui2.Direction; -import com.googlecode.lanterna.gui2.Label; -import com.googlecode.lanterna.gui2.LinearLayout; -import com.googlecode.lanterna.gui2.Panel; - -public abstract class EmptyMenu extends BasicWindow { - - protected EmptyMenu(String title) { - super(title); - - Panel panel = new Panel(new LinearLayout(Direction.VERTICAL)); - - panel.addComponent(new Label("Раздел находится в разработке")); - - panel.addComponent(new Button("Назад", this::close)); - - setComponent(panel); - } -} diff --git a/app/src/main/java/ru/project/ui/MainMenu.java b/app/src/main/java/ru/project/ui/MainMenu.java deleted file mode 100644 index 361ce13..0000000 --- a/app/src/main/java/ru/project/ui/MainMenu.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.project.ui; - -import com.googlecode.lanterna.gui2.BasicWindow; -import com.googlecode.lanterna.gui2.Button; -import com.googlecode.lanterna.gui2.Direction; -import com.googlecode.lanterna.gui2.LinearLayout; -import com.googlecode.lanterna.gui2.Panel; - -public class MainMenu extends BasicWindow { - - public MainMenu(Navigator navigator) { - super("ГЛАВНОЕ МЕНЮ"); - - Panel panel = new Panel(new LinearLayout(Direction.VERTICAL)); - - panel.addComponent(new Button("Показать текущую коллекцию", navigator::showCollectionView)); - - panel.addComponent(new Button("Создать новую коллекцию", navigator::showCreateCollection)); - - panel.addComponent( - new Button("Сохранить текущую коллекцию в файл", navigator::showSaveCollection)); - - panel.addComponent(new Button("Сортировать текущую коллекцию", navigator::showSortCollection)); - - panel.addComponent(new Button("Найти элемент", navigator::showSearchElement)); - - panel.addComponent( - new Button("Подсчитать количество вхождений", navigator::showCountOccurrences)); - - panel.addComponent(new Button("Завершить выполнение программы", this::close)); - - setComponent(panel); - } -} diff --git a/app/src/main/java/ru/project/ui/Navigator.java b/app/src/main/java/ru/project/ui/Navigator.java deleted file mode 100644 index 873a1a4..0000000 --- a/app/src/main/java/ru/project/ui/Navigator.java +++ /dev/null @@ -1,44 +0,0 @@ -package ru.project.ui; - -import com.googlecode.lanterna.gui2.BasicWindow; -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import ru.project.CollectionController; - -public class Navigator { - - private final WindowBasedTextGUI gui; - private final CollectionController collectionController; - - public Navigator(WindowBasedTextGUI gui, CollectionController collectionController) { - this.gui = gui; - this.collectionController = collectionController; - } - - public void show(BasicWindow window) { - gui.addWindowAndWait(window); - } - - public void showCollectionView() { - show(new CollectionView(collectionController)); - } - - public void showCreateCollection() { - show(new CreateCollectionMenu(collectionController)); - } - - public void showSaveCollection() { - show(new SaveCollectionMenu(collectionController)); - } - - public void showSortCollection() { - show(new SortCollectionMenu(collectionController)); - } - - public void showSearchElement() { - show(new SearchElementMenu(collectionController)); - } - - public void showCountOccurrences() { - show(new CountOccurrencesMenu(collectionController)); - } -} diff --git a/app/src/main/java/ru/project/ui/SaveCollectionMenu.java b/app/src/main/java/ru/project/ui/SaveCollectionMenu.java deleted file mode 100644 index 9f7d2e4..0000000 --- a/app/src/main/java/ru/project/ui/SaveCollectionMenu.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.project.ui; - -import ru.project.CollectionController; - -public class SaveCollectionMenu extends EmptyMenu { - - public SaveCollectionMenu(CollectionController controller) { - super("Сохранение текущей коллекции"); - } -} diff --git a/app/src/main/java/ru/project/ui/SearchElementMenu.java b/app/src/main/java/ru/project/ui/SearchElementMenu.java deleted file mode 100644 index b185219..0000000 --- a/app/src/main/java/ru/project/ui/SearchElementMenu.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.project.ui; - -import ru.project.CollectionController; - -public class SearchElementMenu extends EmptyMenu { - - public SearchElementMenu(CollectionController controller) { - super("Поиск элемента"); - } -} diff --git a/app/src/main/java/ru/project/ui/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/SortCollectionMenu.java deleted file mode 100644 index 99bc1a8..0000000 --- a/app/src/main/java/ru/project/ui/SortCollectionMenu.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.project.ui; - -import ru.project.CollectionController; - -public class SortCollectionMenu extends EmptyMenu { - - public SortCollectionMenu(CollectionController controller) { - super("Сортировка текущей коллекции"); - } -} diff --git a/app/src/main/java/ru/project/view/CollectionView.java b/app/src/main/java/ru/project/view/CollectionView.java new file mode 100644 index 0000000..27d139e --- /dev/null +++ b/app/src/main/java/ru/project/view/CollectionView.java @@ -0,0 +1,47 @@ +package ru.project.view; + +import com.googlecode.lanterna.gui2.BasicWindow; +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.student.Student; + +public class CollectionView { + + public void showCollection(WindowBasedTextGUI gui, MyList collection) { + BasicWindow window = new BasicWindow("Текущая коллекция"); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + for (int i = 0; i < collection.size(); i++) { + Student student = collection.get(i); + + panel.addComponent(new Label((i + 1) + ". " + student)); + } + + panel.addComponent(new Button("Вернуться", window::close)); + + window.setComponent(panel); + + gui.addWindowAndWait(window); + } + + public void showEmptyMessage(WindowBasedTextGUI gui) { + BasicWindow window = new BasicWindow("Текущая коллекция"); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label("Текущая коллекция пуста.")); + + panel.addComponent(new Button("Вернуться", window::close)); + + window.setComponent(panel); + + gui.addWindowAndWait(window); + } +} diff --git a/app/src/main/java/ru/project/view/MainView.java b/app/src/main/java/ru/project/view/MainView.java new file mode 100644 index 0000000..632f2db --- /dev/null +++ b/app/src/main/java/ru/project/view/MainView.java @@ -0,0 +1,62 @@ +package ru.project.view; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; + +public class MainView { + + private final BasicWindow window; + private final ActionListBox menu; + + public MainView() { + window = new BasicWindow("Главное меню"); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + menu = new ActionListBox(); + + panel.addComponent(menu); + + window.setComponent(panel); + } + + public void setShowCollectionAction(Runnable action) { + menu.addItem("Показать текущую коллекцию", action); + } + + public void setCreateCollectionAction(Runnable action) { + menu.addItem("Меню создания новой коллекции", action); + } + + public void setSaveCollectionAction(Runnable action) { + menu.addItem("Меню сохранения текущей коллекции в файл", action); + } + + public void setSortCollectionAction(Runnable action) { + menu.addItem("Меню сортировки текущей коллекции", action); + } + + public void setSearchCollectionAction(Runnable action) { + menu.addItem("Меню поиска элемента в текущей коллекции", action); + } + + public void setCountOccurrencesAction(Runnable action) { + menu.addItem("Меню подсчёта количества вхождений элемента", action); + } + + public void setExitAction(Runnable action) { + menu.addItem("Завершить выполнение программы", action); + } + + public void show(WindowBasedTextGUI gui) { + gui.addWindowAndWait(window); + } + + public void close() { + window.close(); + } +} From d08546e05c89fb3335fc5d47c56c6ddd2444a067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Wed, 26 Aug 2026 16:40:03 +0300 Subject: [PATCH 043/110] refactor: change recordBookNumber type to int and add copy methods --- .../main/java/ru/project/student/Student.java | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index 3e4332f..f8c529a 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -5,17 +5,17 @@ import java.util.Objects; public class Student { private final String groupNumber; private final double averageGrade; - private final String recordBookNumber; + private final int recordBookNumber; - Student(String groupNumber, double averageGrade, String recordBookNumber) { + public Student(String groupNumber, double averageGrade, int recordBookNumber) { if (groupNumber == null || groupNumber.isBlank()) { throw new IllegalArgumentException("Group number cannot be empty"); } if (averageGrade < 0.0 || averageGrade > 5.0) { throw new IllegalArgumentException("Average grade must be between 0.0 and 5.0"); } - if (recordBookNumber == null || recordBookNumber.isBlank()) { - throw new IllegalArgumentException("Record book number cannot be empty"); + if (recordBookNumber <= 0) { + throw new IllegalArgumentException("Record book number must be positive"); } this.groupNumber = groupNumber; this.averageGrade = averageGrade; @@ -30,42 +30,43 @@ public class Student { return averageGrade; } - public String getRecordBookNumber() { + 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); + } + @Override public String toString() { return "Student{" + "groupNumber='" + groupNumber + '\'' + ", averageGrade=" + averageGrade + - ", recordBookNumber='" + recordBookNumber + '\'' + + ", recordBookNumber=" + 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; - } - + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; Student student = (Student) obj; - return Double.compare(averageGrade, student.averageGrade) == 0 - && Objects.equals(groupNumber, student.groupNumber) - && Objects.equals(recordBookNumber, student.recordBookNumber); + && recordBookNumber == student.recordBookNumber + && Objects.equals(groupNumber, student.groupNumber); } @Override public int hashCode() { return Objects.hash(groupNumber, averageGrade, recordBookNumber); } -} \ No newline at end of file +} From 456ea9005ab86cccae7a7e29e90b8fabca3590c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Wed, 26 Aug 2026 17:49:39 +0300 Subject: [PATCH 044/110] feat: add validation for averageGrade and recordBookNumber --- app/src/main/java/ru/project/student/Student.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index f8c529a..288fddd 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -7,9 +7,11 @@ public class Student { private final double averageGrade; private final int recordBookNumber; + // Конструктор с проверками public Student(String groupNumber, double averageGrade, int recordBookNumber) { - if (groupNumber == null || groupNumber.isBlank()) { - throw new IllegalArgumentException("Group number cannot be empty"); + // Проверка groupNumber на формат + if (groupNumber == null || !groupNumber.matches("^[A-Z][0-9]{2}$")) { + throw new IllegalArgumentException("Group number must match format: one uppercase letter + two digits (e.g. A12)"); } if (averageGrade < 0.0 || averageGrade > 5.0) { throw new IllegalArgumentException("Average grade must be between 0.0 and 5.0"); @@ -22,6 +24,7 @@ public class Student { this.recordBookNumber = recordBookNumber; } + // Геттеры — исправлен тип groupNumber (теперь String) public String getGroupNumber() { return groupNumber; } @@ -34,6 +37,7 @@ public class Student { return recordBookNumber; } + // Методы копирования (with-методы) public Student withGroupNumber(String groupNumber) { return new Student(groupNumber, this.averageGrade, this.recordBookNumber); } From 26843401cee9e090c8bfe5ba1598824a931c659f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Wed, 26 Aug 2026 17:57:14 +0300 Subject: [PATCH 045/110] feat: update StudentBuilder and dependent classes for new Student model --- app/src/main/java/ru/project/student/StudentBuilder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ru/project/student/StudentBuilder.java b/app/src/main/java/ru/project/student/StudentBuilder.java index 3e1ec41..6d7d5fc 100644 --- a/app/src/main/java/ru/project/student/StudentBuilder.java +++ b/app/src/main/java/ru/project/student/StudentBuilder.java @@ -3,7 +3,7 @@ package ru.project.student; public class StudentBuilder { private String groupNumber; private double averageGrade; - private String recordBookNumber; + private int recordBookNumber; public StudentBuilder setGroupNumber(String groupNumber) { this.groupNumber = groupNumber; @@ -15,7 +15,7 @@ public class StudentBuilder { return this; } - public StudentBuilder setRecordBookNumber(String recordBookNumber) { + public StudentBuilder setRecordBookNumber(int recordBookNumber) { this.recordBookNumber = recordBookNumber; return this; } @@ -23,4 +23,4 @@ public class StudentBuilder { public Student build() { return new Student(groupNumber, averageGrade, recordBookNumber); } -} \ No newline at end of file +} From 37dde57c2f70b6222ef200d0c8cd9da7462b73f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Wed, 26 Aug 2026 20:33:15 +0300 Subject: [PATCH 046/110] feat: update Student --- .../main/java/ru/project/student/Student.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index 3e4332f..156059d 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -68,4 +68,21 @@ public class Student { public int hashCode() { return Objects.hash(groupNumber, averageGrade, recordBookNumber); } + + // ------------------------ Статические геттеры для границ и формата ------------------------ + public static double getMinAverageGrade() { + return 0.0; + } + + public static double getMaxAverageGrade() { + return 5.0; + } + + public static int getMinRecordBookNumber() { + return 1; + } + + public static String getGroupNumberPattern() { + return "^[A-Z][0-9]{2}$"; + } } \ No newline at end of file From d615a6e3d6b2a160f44013d1a55e3ca6ec2f2681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 23:36:04 +0400 Subject: [PATCH 047/110] feat(view): add reusable base and action list views --- .../java/ru/project/view/ActionListView.java | 21 +++++++++ .../main/java/ru/project/view/BaseView.java | 23 +++++++++ .../main/java/ru/project/view/PanelView.java | 47 +++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 app/src/main/java/ru/project/view/ActionListView.java create mode 100644 app/src/main/java/ru/project/view/BaseView.java create mode 100644 app/src/main/java/ru/project/view/PanelView.java diff --git a/app/src/main/java/ru/project/view/ActionListView.java b/app/src/main/java/ru/project/view/ActionListView.java new file mode 100644 index 0000000..fa2bb07 --- /dev/null +++ b/app/src/main/java/ru/project/view/ActionListView.java @@ -0,0 +1,21 @@ +package ru.project.view; + +import com.googlecode.lanterna.gui2.ActionListBox; + +public class ActionListView extends BaseView { + + protected final ActionListBox actionList; + + public ActionListView(String title) { + this(title, new ActionListBox()); + } + + private ActionListView(String title, ActionListBox actionList) { + super(title, actionList); + this.actionList = actionList; + } + + public void addItem(String text, Runnable action) { + actionList.addItem(text, action); + } +} diff --git a/app/src/main/java/ru/project/view/BaseView.java b/app/src/main/java/ru/project/view/BaseView.java new file mode 100644 index 0000000..1bbf8c8 --- /dev/null +++ b/app/src/main/java/ru/project/view/BaseView.java @@ -0,0 +1,23 @@ +package ru.project.view; + +import com.googlecode.lanterna.gui2.BasicWindow; +import com.googlecode.lanterna.gui2.Component; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; + +public abstract class BaseView { + + protected final BasicWindow window; + + protected BaseView(String title, Component mainComponent) { + window = new BasicWindow(title); + window.setComponent(mainComponent); + } + + public void show(WindowBasedTextGUI gui) { + gui.addWindowAndWait(window); + } + + public void close() { + window.close(); + } +} diff --git a/app/src/main/java/ru/project/view/PanelView.java b/app/src/main/java/ru/project/view/PanelView.java new file mode 100644 index 0000000..ab36e50 --- /dev/null +++ b/app/src/main/java/ru/project/view/PanelView.java @@ -0,0 +1,47 @@ +package ru.project.view; + +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; + +public class PanelView extends BaseView { + + protected final Panel panel; + + public PanelView(String title) { + this(new Panel(), title); + } + + private PanelView(Panel panel, String title) { + super(title, panel); + + this.panel = panel; + panel.setLayoutManager(new LinearLayout()); + } + + public Button addButton(String text, Runnable action) { + Button button = new Button(text, action); + + panel.addComponent(button); + + return button; + } + + public Label addLabel(String text) { + Label label = new Label(text); + + panel.addComponent(label); + + return label; + } + + public TextBox addTextBox() { + TextBox textBox = new TextBox(); + + panel.addComponent(textBox); + + return textBox; + } +} From 9dc79936d29447eaad3271c9995291a3217ce210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Wed, 26 Aug 2026 23:36:42 +0400 Subject: [PATCH 048/110] feat(presenter): add base presenter abstraction --- .../ru/project/presenter/BasePresenter.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/main/java/ru/project/presenter/BasePresenter.java diff --git a/app/src/main/java/ru/project/presenter/BasePresenter.java b/app/src/main/java/ru/project/presenter/BasePresenter.java new file mode 100644 index 0000000..5683048 --- /dev/null +++ b/app/src/main/java/ru/project/presenter/BasePresenter.java @@ -0,0 +1,17 @@ +package ru.project.presenter; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.model.AppState; + +public abstract class BasePresenter { + + protected final AppState state; + protected final WindowBasedTextGUI gui; + + protected BasePresenter(AppState state, WindowBasedTextGUI gui) { + this.state = state; + this.gui = gui; + } + + public abstract void start(); +} From 9faa6cff1457ec2e140c21682debc4ee8b7c7eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 00:09:05 +0400 Subject: [PATCH 049/110] refactor(view): add default action to base view --- app/src/main/java/ru/project/view/BaseView.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/ru/project/view/BaseView.java b/app/src/main/java/ru/project/view/BaseView.java index 1bbf8c8..dd90a17 100644 --- a/app/src/main/java/ru/project/view/BaseView.java +++ b/app/src/main/java/ru/project/view/BaseView.java @@ -20,4 +20,8 @@ public abstract class BaseView { public void close() { window.close(); } + + protected void defaultAction() { + close(); + } } From a7a295398027ad05ce25e90c106478e3d3301a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 00:09:40 +0400 Subject: [PATCH 050/110] refactor(view): reuse base views and decouple menu actions --- .../java/ru/project/view/CollectionView.java | 40 ++++---------- .../main/java/ru/project/view/MainView.java | 55 +++++++++---------- 2 files changed, 36 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/ru/project/view/CollectionView.java b/app/src/main/java/ru/project/view/CollectionView.java index 27d139e..1704fe4 100644 --- a/app/src/main/java/ru/project/view/CollectionView.java +++ b/app/src/main/java/ru/project/view/CollectionView.java @@ -1,47 +1,27 @@ package ru.project.view; -import com.googlecode.lanterna.gui2.BasicWindow; -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.student.Student; -public class CollectionView { +public class CollectionView extends PanelView { - public void showCollection(WindowBasedTextGUI gui, MyList collection) { - BasicWindow window = new BasicWindow("Текущая коллекция"); - - Panel panel = new Panel(); - panel.setLayoutManager(new LinearLayout()); + public CollectionView() { + super("Текущая коллекция"); + } + public void showCollection(MyList collection) { for (int i = 0; i < collection.size(); i++) { Student student = collection.get(i); - panel.addComponent(new Label((i + 1) + ". " + student)); + addLabel((i + 1) + ". " + student); } - panel.addComponent(new Button("Вернуться", window::close)); - - window.setComponent(panel); - - gui.addWindowAndWait(window); + addButton("Выйти", this::close); } - public void showEmptyMessage(WindowBasedTextGUI gui) { - BasicWindow window = new BasicWindow("Текущая коллекция"); + public void showEmptyMessage() { + addLabel("Текущая коллекция пуста."); - Panel panel = new Panel(); - panel.setLayoutManager(new LinearLayout()); - - panel.addComponent(new Label("Текущая коллекция пуста.")); - - panel.addComponent(new Button("Вернуться", window::close)); - - window.setComponent(panel); - - gui.addWindowAndWait(window); + addButton("Выйти", this::close); } } diff --git a/app/src/main/java/ru/project/view/MainView.java b/app/src/main/java/ru/project/view/MainView.java index 632f2db..55cbdb4 100644 --- a/app/src/main/java/ru/project/view/MainView.java +++ b/app/src/main/java/ru/project/view/MainView.java @@ -1,62 +1,59 @@ package ru.project.view; -import com.googlecode.lanterna.gui2.ActionListBox; -import com.googlecode.lanterna.gui2.BasicWindow; -import com.googlecode.lanterna.gui2.LinearLayout; -import com.googlecode.lanterna.gui2.Panel; -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -public class MainView { +public class MainView extends ActionListView { - private final BasicWindow window; - private final ActionListBox menu; + private Runnable showCollectionAction = this::defaultAction; + private Runnable createCollectionAction = this::defaultAction; + private Runnable saveCollectionAction = this::defaultAction; + private Runnable sortCollectionAction = this::defaultAction; + private Runnable searchCollectionAction = this::defaultAction; + private Runnable countOccurrencesAction = this::defaultAction; + private Runnable exitAction = this::defaultAction; public MainView() { - window = new BasicWindow("Главное меню"); + super("Главное меню"); - Panel panel = new Panel(); - panel.setLayoutManager(new LinearLayout()); + addItem("Показать текущую коллекцию", () -> showCollectionAction.run()); - menu = new ActionListBox(); + addItem("Меню создания новой коллекции", () -> createCollectionAction.run()); - panel.addComponent(menu); + addItem("Меню сохранения текущей коллекции в файл", () -> saveCollectionAction.run()); - window.setComponent(panel); + addItem("Меню сортировки текущей коллекции", () -> sortCollectionAction.run()); + + addItem("Меню поиска элемента в текущей коллекции", () -> searchCollectionAction.run()); + + addItem("Меню подсчёта количества вхождений элемента", () -> countOccurrencesAction.run()); + + addItem("Завершить выполнение программы", () -> exitAction.run()); } public void setShowCollectionAction(Runnable action) { - menu.addItem("Показать текущую коллекцию", action); + showCollectionAction = action; } public void setCreateCollectionAction(Runnable action) { - menu.addItem("Меню создания новой коллекции", action); + createCollectionAction = action; } public void setSaveCollectionAction(Runnable action) { - menu.addItem("Меню сохранения текущей коллекции в файл", action); + saveCollectionAction = action; } public void setSortCollectionAction(Runnable action) { - menu.addItem("Меню сортировки текущей коллекции", action); + sortCollectionAction = action; } public void setSearchCollectionAction(Runnable action) { - menu.addItem("Меню поиска элемента в текущей коллекции", action); + searchCollectionAction = action; } public void setCountOccurrencesAction(Runnable action) { - menu.addItem("Меню подсчёта количества вхождений элемента", action); + countOccurrencesAction = action; } public void setExitAction(Runnable action) { - menu.addItem("Завершить выполнение программы", action); - } - - public void show(WindowBasedTextGUI gui) { - gui.addWindowAndWait(window); - } - - public void close() { - window.close(); + exitAction = action; } } From 140128cde095ff83c7af0654772d71b946d31dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 00:10:05 +0400 Subject: [PATCH 051/110] refactor(presenter): extend base presenter and defer menu actions --- .../presenter/CollectionPresenter.java | 15 ++++----- .../ru/project/presenter/MainPresenter.java | 32 +++++++------------ 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/ru/project/presenter/CollectionPresenter.java b/app/src/main/java/ru/project/presenter/CollectionPresenter.java index f9c6606..db1d09b 100644 --- a/app/src/main/java/ru/project/presenter/CollectionPresenter.java +++ b/app/src/main/java/ru/project/presenter/CollectionPresenter.java @@ -6,26 +6,25 @@ import ru.project.model.AppState; import ru.project.student.Student; import ru.project.view.CollectionView; -public class CollectionPresenter { +public class CollectionPresenter extends BasePresenter { - private final AppState state; private final CollectionView view; - private final WindowBasedTextGUI gui; public CollectionPresenter(AppState state, CollectionView view, WindowBasedTextGUI gui) { - this.state = state; + super(state, gui); this.view = view; - this.gui = gui; } + @Override public void start() { MyList collection = state.getCollection(); if (collection == null || collection.isEmpty()) { - view.showEmptyMessage(gui); - return; + view.showEmptyMessage(); + } else { + view.showCollection(collection); } - view.showCollection(gui, collection); + view.show(gui); } } diff --git a/app/src/main/java/ru/project/presenter/MainPresenter.java b/app/src/main/java/ru/project/presenter/MainPresenter.java index 91b195b..824f030 100644 --- a/app/src/main/java/ru/project/presenter/MainPresenter.java +++ b/app/src/main/java/ru/project/presenter/MainPresenter.java @@ -1,28 +1,22 @@ package ru.project.presenter; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import com.googlecode.lanterna.gui2.dialogs.MessageDialog; -import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton; import ru.project.model.AppState; -import ru.project.view.CollectionView; import ru.project.view.MainView; -public class MainPresenter { +public class MainPresenter extends BasePresenter { - private final AppState state; private final MainView view; - private final WindowBasedTextGUI gui; - private final CollectionPresenter collectionPresenter; public MainPresenter(AppState state, MainView view, WindowBasedTextGUI gui) { - this.state = state; + super(state, gui); this.view = view; - this.gui = gui; - this.collectionPresenter = new CollectionPresenter(state, new CollectionView(), gui); + + bindActions(); } + @Override public void start() { - bindActions(); view.show(gui); } @@ -37,34 +31,30 @@ public class MainPresenter { } private void showCollection() { - collectionPresenter.start(); + // TODO } private void createCollection() { - showMessage("Меню создания коллекции."); + // TODO } private void saveCollection() { - showMessage("Меню сохранения коллекции."); + // TODO } private void sortCollection() { - showMessage("Меню сортировки коллекции."); + // TODO } private void searchCollection() { - showMessage("Меню поиска элемента."); + // TODO } private void countOccurrences() { - showMessage("Меню подсчёта количества вхождений."); + // TODO } private void exit() { view.close(); } - - private void showMessage(String message) { - MessageDialog.showMessageDialog(gui, "Информация", message, MessageDialogButton.OK); - } } From 51e7859520f725b10f4af9fb3b3782ff18f3fd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 00:10:30 +0400 Subject: [PATCH 052/110] feat(fill-method): add fill method selection presenter --- .../java/ru/project/model/FillMethod.java | 7 +++ .../presenter/FillMethodPresenter.java | 44 +++++++++++++++++++ .../java/ru/project/view/FillMethodView.java | 32 ++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 app/src/main/java/ru/project/model/FillMethod.java create mode 100644 app/src/main/java/ru/project/presenter/FillMethodPresenter.java create mode 100644 app/src/main/java/ru/project/view/FillMethodView.java diff --git a/app/src/main/java/ru/project/model/FillMethod.java b/app/src/main/java/ru/project/model/FillMethod.java new file mode 100644 index 0000000..df682af --- /dev/null +++ b/app/src/main/java/ru/project/model/FillMethod.java @@ -0,0 +1,7 @@ +package ru.project.model; + +public enum FillMethod { + CUSTOM, + RANDOM, + FILE +} diff --git a/app/src/main/java/ru/project/presenter/FillMethodPresenter.java b/app/src/main/java/ru/project/presenter/FillMethodPresenter.java new file mode 100644 index 0000000..321326b --- /dev/null +++ b/app/src/main/java/ru/project/presenter/FillMethodPresenter.java @@ -0,0 +1,44 @@ +package ru.project.presenter; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.model.AppState; +import ru.project.model.FillMethod; +import ru.project.view.FillMethodView; + +public class FillMethodPresenter extends BasePresenter { + + private final FillMethodView view; + private final Consumer onMethodSelected; + + public FillMethodPresenter( + AppState state, + FillMethodView view, + WindowBasedTextGUI gui, + Consumer onMethodSelected) { + + super(state, gui); + + this.view = view; + this.onMethodSelected = onMethodSelected; + } + + @Override + public void start() { + bindActions(); + view.show(gui); + } + + private void bindActions() { + view.setCustomAction(() -> selectMethod(FillMethod.CUSTOM)); + + view.setRandomAction(() -> selectMethod(FillMethod.RANDOM)); + + view.setFileAction(() -> selectMethod(FillMethod.FILE)); + } + + private void selectMethod(FillMethod method) { + onMethodSelected.accept(method); + view.close(); + } +} diff --git a/app/src/main/java/ru/project/view/FillMethodView.java b/app/src/main/java/ru/project/view/FillMethodView.java new file mode 100644 index 0000000..027d7fe --- /dev/null +++ b/app/src/main/java/ru/project/view/FillMethodView.java @@ -0,0 +1,32 @@ +package ru.project.view; + +public class FillMethodView extends ActionListView { + + private Runnable customAction = this::defaultAction; + private Runnable randomAction = this::defaultAction; + private Runnable fileAction = this::defaultAction; + + public FillMethodView() { + super("Настроить способ заполнения"); + + addItem("Пользовательские данные", () -> customAction.run()); + + addItem("Случайные данные", () -> randomAction.run()); + + addItem("Данные из файла", () -> fileAction.run()); + + addItem("Выйти", this::close); + } + + public void setCustomAction(Runnable action) { + customAction = action; + } + + public void setRandomAction(Runnable action) { + randomAction = action; + } + + public void setFileAction(Runnable action) { + fileAction = action; + } +} From ea868cc6874a0573cc824854a0cc207b0869bc52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 00:12:39 +0400 Subject: [PATCH 053/110] feat(collection): add collection creation flow --- .../presenter/CreateCollectionPresenter.java | 91 +++++++++++++++++ .../ru/project/view/CreateCollectionView.java | 98 +++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java create mode 100644 app/src/main/java/ru/project/view/CreateCollectionView.java diff --git a/app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java b/app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java new file mode 100644 index 0000000..326466a --- /dev/null +++ b/app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java @@ -0,0 +1,91 @@ +package ru.project.presenter; + +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.model.AppState; +import ru.project.model.FillMethod; +import ru.project.view.CreateCollectionView; +import ru.project.view.FillMethodView; + +public class CreateCollectionPresenter extends BasePresenter { + + private final CreateCollectionView view; + + private int size; + private FillMethod fillMethod; + + public CreateCollectionPresenter( + AppState state, CreateCollectionView view, WindowBasedTextGUI gui) { + + super(state, gui); + + this.view = view; + this.size = 0; + this.fillMethod = FillMethod.CUSTOM; + } + + @Override + public void start() { + bindActions(); + view.show(gui); + } + + private void bindActions() { + view.setSetSizeAction(this::setSize); + view.setConfigureFillMethodAction(this::configureFillMethod); + view.setCreateCollectionAction(this::createCollection); + } + + private void setSize() { + view.showSetSizeDialog(gui, this::handleSizeEntered); + } + + private void handleSizeEntered(int sizeValue) { + if (sizeValue <= 0) { + view.showMessage(gui, "Размер должен быть больше 0."); + return; + } + + size = sizeValue; + view.updateSize(size); + } + + private void configureFillMethod() { + FillMethodView fillMethodView = new FillMethodView(); + + FillMethodPresenter presenter = + new FillMethodPresenter(state, fillMethodView, gui, this::handleFillMethodSelected); + + presenter.start(); + } + + private void handleFillMethodSelected(FillMethod selectedMethod) { + fillMethod = selectedMethod; + + view.updateFillMethod(fillMethod); + } + + private void createCollection() { + if (size <= 0) { + view.showMessage(gui, "Сначала задайте размер коллекции."); + return; + } + + switch (fillMethod) { + case CUSTOM: + view.showMessage(gui, "Выбран способ заполнения: пользовательские данные."); + break; + + case RANDOM: + view.showMessage(gui, "Выбран способ заполнения: случайные данные."); + break; + + case FILE: + view.showMessage(gui, "Выбран способ заполнения: данные из файла."); + break; + } + + // TODO: + // state.setCollection(...); + // view.close(); + } +} diff --git a/app/src/main/java/ru/project/view/CreateCollectionView.java b/app/src/main/java/ru/project/view/CreateCollectionView.java new file mode 100644 index 0000000..55825a1 --- /dev/null +++ b/app/src/main/java/ru/project/view/CreateCollectionView.java @@ -0,0 +1,98 @@ +package ru.project.view; + +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.TextBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.IntConsumer; +import ru.project.model.FillMethod; + +public class CreateCollectionView extends PanelView { + + private final Label sizeLabel; + private final Label fillMethodLabel; + + private Runnable setSizeAction = this::defaultAction; + private Runnable configureFillMethodAction = this::defaultAction; + private Runnable createCollectionAction = this::defaultAction; + + public CreateCollectionView() { + super("Создание новой коллекции"); + + sizeLabel = addLabel("Размер: не задан"); + + fillMethodLabel = addLabel("Способ заполнения: Пользовательские данные"); + + addButton("Задать размер", () -> setSizeAction.run()); + + addButton("Настроить способ заполнения", () -> configureFillMethodAction.run()); + + addButton("Создать новую коллекцию", () -> createCollectionAction.run()); + + addButton("Выйти", this::close); + } + + public void setSetSizeAction(Runnable action) { + setSizeAction = action; + } + + public void setConfigureFillMethodAction(Runnable action) { + configureFillMethodAction = action; + } + + public void setCreateCollectionAction(Runnable action) { + createCollectionAction = action; + } + + public void updateSize(int size) { + sizeLabel.setText("Размер: " + size); + } + + public void updateFillMethod(FillMethod fillMethod) { + fillMethodLabel.setText("Способ заполнения: " + getFillMethodName(fillMethod)); + } + + public void showMessage(WindowBasedTextGUI gui, String message) { + PanelView messageView = new PanelView("Информация"); + + messageView.addLabel(message); + + messageView.addButton("OK", messageView::close); + + messageView.show(gui); + } + + public void showSetSizeDialog(WindowBasedTextGUI gui, IntConsumer onSizeEntered) { + PanelView dialog = new PanelView("Задать размер"); + + dialog.addLabel("Введите размер:"); + + TextBox textBox = dialog.addTextBox(); + + dialog.addButton("Подтвердить", () -> handleSizeInput(gui, dialog, textBox, onSizeEntered)); + + dialog.addButton("Вернуться", dialog::close); + + dialog.show(gui); + } + + private void handleSizeInput( + WindowBasedTextGUI gui, PanelView dialog, TextBox textBox, IntConsumer onSizeEntered) { + try { + int size = Integer.parseInt(textBox.getText().trim()); + + dialog.close(); + onSizeEntered.accept(size); + + } catch (NumberFormatException e) { + showMessage(gui, "Введите целое число."); + } + } + + private String getFillMethodName(FillMethod fillMethod) { + return switch (fillMethod) { + case CUSTOM -> "Пользовательские данные"; + case RANDOM -> "Случайные данные"; + case FILE -> "Данные из файла"; + }; + } +} From abef518d7416f83ba413b5088836bb0c071cf056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 00:23:02 +0400 Subject: [PATCH 054/110] feat(presenter): connect collection creation and viewing flows --- app/src/main/java/ru/project/App.java | 1 + .../java/ru/project/presenter/MainPresenter.java | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index f7c48d9..5c16866 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -23,6 +23,7 @@ public final class App { MultiWindowTextGUI gui = new MultiWindowTextGUI(screen); AppState state = new AppState(); + MainView view = new MainView(); MainPresenter presenter = new MainPresenter(state, view, gui); diff --git a/app/src/main/java/ru/project/presenter/MainPresenter.java b/app/src/main/java/ru/project/presenter/MainPresenter.java index 824f030..e1c90be 100644 --- a/app/src/main/java/ru/project/presenter/MainPresenter.java +++ b/app/src/main/java/ru/project/presenter/MainPresenter.java @@ -2,16 +2,27 @@ package ru.project.presenter; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import ru.project.model.AppState; +import ru.project.view.CollectionView; +import ru.project.view.CreateCollectionView; import ru.project.view.MainView; public class MainPresenter extends BasePresenter { private final MainView view; + private final CollectionPresenter collectionPresenter; + private final CreateCollectionPresenter createCollectionPresenter; + public MainPresenter(AppState state, MainView view, WindowBasedTextGUI gui) { super(state, gui); + this.view = view; + this.collectionPresenter = new CollectionPresenter(state, new CollectionView(), gui); + + this.createCollectionPresenter = + new CreateCollectionPresenter(state, new CreateCollectionView(), gui); + bindActions(); } @@ -31,11 +42,11 @@ public class MainPresenter extends BasePresenter { } private void showCollection() { - // TODO + collectionPresenter.start(); } private void createCollection() { - // TODO + createCollectionPresenter.start(); } private void saveCollection() { From e1d76cf69cbc0d94b1584b74e083bfc0b8c09268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Thu, 27 Aug 2026 18:03:01 +0400 Subject: [PATCH 055/110] refactor: migrate UI from views and presenters to windows and menus --- app/src/main/java/ru/project/App.java | 9 +- .../ru/project/presenter/BasePresenter.java | 17 --- .../presenter/CollectionPresenter.java | 30 ----- .../presenter/CreateCollectionPresenter.java | 91 -------------- .../presenter/FillMethodPresenter.java | 44 ------- .../ru/project/presenter/MainPresenter.java | 71 ----------- .../java/ru/project/ui/base/BaseWindow.java | 19 +++ .../project/ui/menu/CreateCollectionMenu.java | 119 ++++++++++++++++++ .../ui/menu/FillMethodSelectionWindow.java | 37 ++++++ .../java/ru/project/ui/menu/MainMenu.java | 74 +++++++++++ .../ui/window/CollectionSizeInputWindow.java | 54 ++++++++ .../ui/window/CurrentCollectionWindow.java | 41 ++++++ .../ru/project/ui/window/MessageWindow.java | 28 +++++ .../java/ru/project/view/ActionListView.java | 21 ---- .../main/java/ru/project/view/BaseView.java | 27 ---- .../java/ru/project/view/CollectionView.java | 27 ---- .../ru/project/view/CreateCollectionView.java | 98 --------------- .../java/ru/project/view/FillMethodView.java | 32 ----- .../main/java/ru/project/view/MainView.java | 59 --------- .../main/java/ru/project/view/PanelView.java | 47 ------- 20 files changed, 375 insertions(+), 570 deletions(-) delete mode 100644 app/src/main/java/ru/project/presenter/BasePresenter.java delete mode 100644 app/src/main/java/ru/project/presenter/CollectionPresenter.java delete mode 100644 app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java delete mode 100644 app/src/main/java/ru/project/presenter/FillMethodPresenter.java delete mode 100644 app/src/main/java/ru/project/presenter/MainPresenter.java create mode 100644 app/src/main/java/ru/project/ui/base/BaseWindow.java create mode 100644 app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java create mode 100644 app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java create mode 100644 app/src/main/java/ru/project/ui/menu/MainMenu.java create mode 100644 app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java create mode 100644 app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java create mode 100644 app/src/main/java/ru/project/ui/window/MessageWindow.java delete mode 100644 app/src/main/java/ru/project/view/ActionListView.java delete mode 100644 app/src/main/java/ru/project/view/BaseView.java delete mode 100644 app/src/main/java/ru/project/view/CollectionView.java delete mode 100644 app/src/main/java/ru/project/view/CreateCollectionView.java delete mode 100644 app/src/main/java/ru/project/view/FillMethodView.java delete mode 100644 app/src/main/java/ru/project/view/MainView.java delete mode 100644 app/src/main/java/ru/project/view/PanelView.java diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index 5c16866..1e1d933 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -4,8 +4,7 @@ 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.presenter.MainPresenter; -import ru.project.view.MainView; +import ru.project.ui.menu.MainMenu; public final class App { @@ -24,11 +23,9 @@ public final class App { AppState state = new AppState(); - MainView view = new MainView(); + MainMenu mainMenu = new MainMenu(gui, state); - MainPresenter presenter = new MainPresenter(state, view, gui); - - presenter.start(); + mainMenu.show(); } catch (Exception e) { System.err.println("Произошла ошибка при выполнении программы:"); diff --git a/app/src/main/java/ru/project/presenter/BasePresenter.java b/app/src/main/java/ru/project/presenter/BasePresenter.java deleted file mode 100644 index 5683048..0000000 --- a/app/src/main/java/ru/project/presenter/BasePresenter.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.project.presenter; - -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import ru.project.model.AppState; - -public abstract class BasePresenter { - - protected final AppState state; - protected final WindowBasedTextGUI gui; - - protected BasePresenter(AppState state, WindowBasedTextGUI gui) { - this.state = state; - this.gui = gui; - } - - public abstract void start(); -} diff --git a/app/src/main/java/ru/project/presenter/CollectionPresenter.java b/app/src/main/java/ru/project/presenter/CollectionPresenter.java deleted file mode 100644 index db1d09b..0000000 --- a/app/src/main/java/ru/project/presenter/CollectionPresenter.java +++ /dev/null @@ -1,30 +0,0 @@ -package ru.project.presenter; - -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import ru.project.collection.MyList; -import ru.project.model.AppState; -import ru.project.student.Student; -import ru.project.view.CollectionView; - -public class CollectionPresenter extends BasePresenter { - - private final CollectionView view; - - public CollectionPresenter(AppState state, CollectionView view, WindowBasedTextGUI gui) { - super(state, gui); - this.view = view; - } - - @Override - public void start() { - MyList collection = state.getCollection(); - - if (collection == null || collection.isEmpty()) { - view.showEmptyMessage(); - } else { - view.showCollection(collection); - } - - view.show(gui); - } -} diff --git a/app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java b/app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java deleted file mode 100644 index 326466a..0000000 --- a/app/src/main/java/ru/project/presenter/CreateCollectionPresenter.java +++ /dev/null @@ -1,91 +0,0 @@ -package ru.project.presenter; - -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import ru.project.model.AppState; -import ru.project.model.FillMethod; -import ru.project.view.CreateCollectionView; -import ru.project.view.FillMethodView; - -public class CreateCollectionPresenter extends BasePresenter { - - private final CreateCollectionView view; - - private int size; - private FillMethod fillMethod; - - public CreateCollectionPresenter( - AppState state, CreateCollectionView view, WindowBasedTextGUI gui) { - - super(state, gui); - - this.view = view; - this.size = 0; - this.fillMethod = FillMethod.CUSTOM; - } - - @Override - public void start() { - bindActions(); - view.show(gui); - } - - private void bindActions() { - view.setSetSizeAction(this::setSize); - view.setConfigureFillMethodAction(this::configureFillMethod); - view.setCreateCollectionAction(this::createCollection); - } - - private void setSize() { - view.showSetSizeDialog(gui, this::handleSizeEntered); - } - - private void handleSizeEntered(int sizeValue) { - if (sizeValue <= 0) { - view.showMessage(gui, "Размер должен быть больше 0."); - return; - } - - size = sizeValue; - view.updateSize(size); - } - - private void configureFillMethod() { - FillMethodView fillMethodView = new FillMethodView(); - - FillMethodPresenter presenter = - new FillMethodPresenter(state, fillMethodView, gui, this::handleFillMethodSelected); - - presenter.start(); - } - - private void handleFillMethodSelected(FillMethod selectedMethod) { - fillMethod = selectedMethod; - - view.updateFillMethod(fillMethod); - } - - private void createCollection() { - if (size <= 0) { - view.showMessage(gui, "Сначала задайте размер коллекции."); - return; - } - - switch (fillMethod) { - case CUSTOM: - view.showMessage(gui, "Выбран способ заполнения: пользовательские данные."); - break; - - case RANDOM: - view.showMessage(gui, "Выбран способ заполнения: случайные данные."); - break; - - case FILE: - view.showMessage(gui, "Выбран способ заполнения: данные из файла."); - break; - } - - // TODO: - // state.setCollection(...); - // view.close(); - } -} diff --git a/app/src/main/java/ru/project/presenter/FillMethodPresenter.java b/app/src/main/java/ru/project/presenter/FillMethodPresenter.java deleted file mode 100644 index 321326b..0000000 --- a/app/src/main/java/ru/project/presenter/FillMethodPresenter.java +++ /dev/null @@ -1,44 +0,0 @@ -package ru.project.presenter; - -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import java.util.function.Consumer; -import ru.project.model.AppState; -import ru.project.model.FillMethod; -import ru.project.view.FillMethodView; - -public class FillMethodPresenter extends BasePresenter { - - private final FillMethodView view; - private final Consumer onMethodSelected; - - public FillMethodPresenter( - AppState state, - FillMethodView view, - WindowBasedTextGUI gui, - Consumer onMethodSelected) { - - super(state, gui); - - this.view = view; - this.onMethodSelected = onMethodSelected; - } - - @Override - public void start() { - bindActions(); - view.show(gui); - } - - private void bindActions() { - view.setCustomAction(() -> selectMethod(FillMethod.CUSTOM)); - - view.setRandomAction(() -> selectMethod(FillMethod.RANDOM)); - - view.setFileAction(() -> selectMethod(FillMethod.FILE)); - } - - private void selectMethod(FillMethod method) { - onMethodSelected.accept(method); - view.close(); - } -} diff --git a/app/src/main/java/ru/project/presenter/MainPresenter.java b/app/src/main/java/ru/project/presenter/MainPresenter.java deleted file mode 100644 index e1c90be..0000000 --- a/app/src/main/java/ru/project/presenter/MainPresenter.java +++ /dev/null @@ -1,71 +0,0 @@ -package ru.project.presenter; - -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import ru.project.model.AppState; -import ru.project.view.CollectionView; -import ru.project.view.CreateCollectionView; -import ru.project.view.MainView; - -public class MainPresenter extends BasePresenter { - - private final MainView view; - - private final CollectionPresenter collectionPresenter; - private final CreateCollectionPresenter createCollectionPresenter; - - public MainPresenter(AppState state, MainView view, WindowBasedTextGUI gui) { - super(state, gui); - - this.view = view; - - this.collectionPresenter = new CollectionPresenter(state, new CollectionView(), gui); - - this.createCollectionPresenter = - new CreateCollectionPresenter(state, new CreateCollectionView(), gui); - - bindActions(); - } - - @Override - public void start() { - view.show(gui); - } - - private void bindActions() { - view.setShowCollectionAction(this::showCollection); - view.setCreateCollectionAction(this::createCollection); - view.setSaveCollectionAction(this::saveCollection); - view.setSortCollectionAction(this::sortCollection); - view.setSearchCollectionAction(this::searchCollection); - view.setCountOccurrencesAction(this::countOccurrences); - view.setExitAction(this::exit); - } - - private void showCollection() { - collectionPresenter.start(); - } - - private void createCollection() { - createCollectionPresenter.start(); - } - - private void saveCollection() { - // TODO - } - - private void sortCollection() { - // TODO - } - - private void searchCollection() { - // TODO - } - - private void countOccurrences() { - // TODO - } - - private void exit() { - view.close(); - } -} diff --git a/app/src/main/java/ru/project/ui/base/BaseWindow.java b/app/src/main/java/ru/project/ui/base/BaseWindow.java new file mode 100644 index 0000000..90fbb7c --- /dev/null +++ b/app/src/main/java/ru/project/ui/base/BaseWindow.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 BaseWindow extends BasicWindow { + + protected final WindowBasedTextGUI gui; + + protected BaseWindow(String title, WindowBasedTextGUI gui) { + super(title); + + this.gui = gui; + } + + public void show() { + gui.addWindowAndWait(this); + } +} diff --git a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java new file mode 100644 index 0000000..2f7facd --- /dev/null +++ b/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java @@ -0,0 +1,119 @@ +package ru.project.ui.menu; + +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.model.FillMethod; +import ru.project.ui.base.BaseWindow; +import ru.project.ui.window.CollectionSizeInputWindow; +import ru.project.ui.window.MessageWindow; + +public class CreateCollectionMenu extends BaseWindow { + + private int collectionSize; + private FillMethod selectedFillMethod; + + private final Label sizeLabel; + private final Label fillMethodLabel; + + public CreateCollectionMenu(WindowBasedTextGUI gui, AppState state) { + super("Создание новой коллекции", gui); + + collectionSize = 0; + selectedFillMethod = FillMethod.CUSTOM; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + sizeLabel = new Label("Размер: не задан"); + + fillMethodLabel = new Label("Способ заполнения: " + getFillMethodName(selectedFillMethod)); + + panel.addComponent(sizeLabel); + panel.addComponent(fillMethodLabel); + + panel.addComponent(new Button("Задать размер", this::openCollectionSizeInputWindow)); + + panel.addComponent(new Button("Настроить способ заполнения", this::openFillMethodMenu)); + + panel.addComponent(new Button("Создать новую коллекцию", this::createCollection)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void openCollectionSizeInputWindow() { + CollectionSizeInputWindow window = + new CollectionSizeInputWindow( + gui, + size -> { + collectionSize = size; + sizeLabel.setText("Размер: " + collectionSize); + }); + + window.show(); + } + + private void openFillMethodMenu() { + FillMethodSelectionWindow menu = + new FillMethodSelectionWindow( + gui, + fillMethod -> { + selectedFillMethod = fillMethod; + fillMethodLabel.setText( + "Способ заполнения: " + getFillMethodName(selectedFillMethod)); + }); + + menu.show(); + } + + private void createCollection() { + if (collectionSize <= 0) { + MessageWindow.create(gui, "Сначала задайте размер коллекции."); + return; + } + + switch (selectedFillMethod) { + case CUSTOM: + createCollectionFromCustomData(); + break; + + case RANDOM: + createCollectionFromRandomData(); + break; + + case FILE: + createCollectionFromFileData(); + break; + } + } + + private void createCollectionFromCustomData() { + // TODO + MessageWindow.create(gui, "Не реализовано."); + } + + private void createCollectionFromRandomData() { + // TODO + MessageWindow.create(gui, "Не реализовано."); + } + + private void createCollectionFromFileData() { + // TODO + MessageWindow.create(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/menu/FillMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java new file mode 100644 index 0000000..245fd06 --- /dev/null +++ b/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java @@ -0,0 +1,37 @@ +package ru.project.ui.menu; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.model.FillMethod; +import ru.project.ui.base.BaseWindow; + +public class FillMethodSelectionWindow extends BaseWindow { + + 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/menu/MainMenu.java b/app/src/main/java/ru/project/ui/menu/MainMenu.java new file mode 100644 index 0000000..3b3fbcb --- /dev/null +++ b/app/src/main/java/ru/project/ui/menu/MainMenu.java @@ -0,0 +1,74 @@ +package ru.project.ui.menu; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.model.AppState; +import ru.project.ui.base.BaseWindow; +import ru.project.ui.window.CurrentCollectionWindow; +import ru.project.ui.window.MessageWindow; + +public class MainMenu extends BaseWindow { + + private final ActionListBox menu; + private final AppState state; + + public MainMenu(WindowBasedTextGUI gui, AppState state) { + super("Главное меню", gui); + + this.state = state; + + menu = new ActionListBox(); + + menu.addItem("Показать текущую коллекцию", this::showCurrentCollection); + + menu.addItem("Меню создания новой коллекции", this::createCollection); + + menu.addItem("Меню сохранения текущей коллекции в файл", this::saveCollection); + + menu.addItem("Меню сортировки текущей коллекции", this::sortCollection); + + menu.addItem("Меню поиска элемента в текущей коллекции", this::searchCollection); + + menu.addItem("Меню подсчёта количества вхождений элемента", this::countOccurrences); + + menu.addItem("Завершить выполнение программы", this::exit); + + setComponent(menu); + } + + private void showCurrentCollection() { + CurrentCollectionWindow window = new CurrentCollectionWindow(gui, state); + + window.show(); + } + + private void createCollection() { + CreateCollectionMenu window = new CreateCollectionMenu(gui, state); + + window.show(); + } + + private void saveCollection() { + // TODO + MessageWindow.create(gui, "Не реализовано."); + } + + private void sortCollection() { + // TODO + MessageWindow.create(gui, "Не реализовано."); + } + + private void searchCollection() { + // TODO + MessageWindow.create(gui, "Не реализовано."); + } + + private void countOccurrences() { + // TODO + MessageWindow.create(gui, "Не реализовано."); + } + + private void exit() { + close(); + } +} diff --git a/app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java b/app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java new file mode 100644 index 0000000..25b7775 --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java @@ -0,0 +1,54 @@ +package ru.project.ui.window; + +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.IntConsumer; +import ru.project.ui.base.BaseWindow; + +public class CollectionSizeInputWindow extends BaseWindow { + + private final TextBox sizeTextBox; + private final IntConsumer onSizeEntered; + + public CollectionSizeInputWindow(WindowBasedTextGUI gui, IntConsumer onSizeEntered) { + + super("Задать размер", gui); + + this.onSizeEntered = onSizeEntered; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label("Введите размер:")); + + sizeTextBox = new TextBox(); + panel.addComponent(sizeTextBox); + + panel.addComponent(new Button("Подтвердить", this::handleSizeEntered)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void handleSizeEntered() { + try { + int size = Integer.parseInt(sizeTextBox.getText().trim()); + + if (size <= 0) { + MessageWindow.create(gui, "Размер должен быть больше 0."); + return; + } + + close(); + onSizeEntered.accept(size); + + } catch (NumberFormatException e) { + MessageWindow.create(gui, "Введите целое число."); + } + } +} diff --git a/app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java b/app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java new file mode 100644 index 0000000..0fd7e29 --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java @@ -0,0 +1,41 @@ +package ru.project.ui.window; + +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.model.AppState; +import ru.project.student.Student; +import ru.project.ui.base.BaseWindow; + +public class CurrentCollectionWindow extends BaseWindow { + + public CurrentCollectionWindow(WindowBasedTextGUI gui, AppState state) { + super("Текущая коллекция", gui); + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + MyList collection = state.getCollection(); + + if (collection == null || collection.isEmpty()) { + panel.addComponent(new Label("Текущая коллекция пуста.")); + } else { + addStudents(panel, collection); + } + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void addStudents(Panel panel, MyList collection) { + for (int i = 0; i < collection.size(); i++) { + Student student = collection.get(i); + + panel.addComponent(new Label((i + 1) + ". " + student)); + } + } +} diff --git a/app/src/main/java/ru/project/ui/window/MessageWindow.java b/app/src/main/java/ru/project/ui/window/MessageWindow.java new file mode 100644 index 0000000..ee56ea8 --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/MessageWindow.java @@ -0,0 +1,28 @@ +package ru.project.ui.window; + +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.BaseWindow; + +public class MessageWindow extends BaseWindow { + + 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 create(WindowBasedTextGUI gui, String message) { + new MessageWindow(gui, message).show(); + } +} diff --git a/app/src/main/java/ru/project/view/ActionListView.java b/app/src/main/java/ru/project/view/ActionListView.java deleted file mode 100644 index fa2bb07..0000000 --- a/app/src/main/java/ru/project/view/ActionListView.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.project.view; - -import com.googlecode.lanterna.gui2.ActionListBox; - -public class ActionListView extends BaseView { - - protected final ActionListBox actionList; - - public ActionListView(String title) { - this(title, new ActionListBox()); - } - - private ActionListView(String title, ActionListBox actionList) { - super(title, actionList); - this.actionList = actionList; - } - - public void addItem(String text, Runnable action) { - actionList.addItem(text, action); - } -} diff --git a/app/src/main/java/ru/project/view/BaseView.java b/app/src/main/java/ru/project/view/BaseView.java deleted file mode 100644 index dd90a17..0000000 --- a/app/src/main/java/ru/project/view/BaseView.java +++ /dev/null @@ -1,27 +0,0 @@ -package ru.project.view; - -import com.googlecode.lanterna.gui2.BasicWindow; -import com.googlecode.lanterna.gui2.Component; -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; - -public abstract class BaseView { - - protected final BasicWindow window; - - protected BaseView(String title, Component mainComponent) { - window = new BasicWindow(title); - window.setComponent(mainComponent); - } - - public void show(WindowBasedTextGUI gui) { - gui.addWindowAndWait(window); - } - - public void close() { - window.close(); - } - - protected void defaultAction() { - close(); - } -} diff --git a/app/src/main/java/ru/project/view/CollectionView.java b/app/src/main/java/ru/project/view/CollectionView.java deleted file mode 100644 index 1704fe4..0000000 --- a/app/src/main/java/ru/project/view/CollectionView.java +++ /dev/null @@ -1,27 +0,0 @@ -package ru.project.view; - -import ru.project.collection.MyList; -import ru.project.student.Student; - -public class CollectionView extends PanelView { - - public CollectionView() { - super("Текущая коллекция"); - } - - public void showCollection(MyList collection) { - for (int i = 0; i < collection.size(); i++) { - Student student = collection.get(i); - - addLabel((i + 1) + ". " + student); - } - - addButton("Выйти", this::close); - } - - public void showEmptyMessage() { - addLabel("Текущая коллекция пуста."); - - addButton("Выйти", this::close); - } -} diff --git a/app/src/main/java/ru/project/view/CreateCollectionView.java b/app/src/main/java/ru/project/view/CreateCollectionView.java deleted file mode 100644 index 55825a1..0000000 --- a/app/src/main/java/ru/project/view/CreateCollectionView.java +++ /dev/null @@ -1,98 +0,0 @@ -package ru.project.view; - -import com.googlecode.lanterna.gui2.Label; -import com.googlecode.lanterna.gui2.TextBox; -import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -import java.util.function.IntConsumer; -import ru.project.model.FillMethod; - -public class CreateCollectionView extends PanelView { - - private final Label sizeLabel; - private final Label fillMethodLabel; - - private Runnable setSizeAction = this::defaultAction; - private Runnable configureFillMethodAction = this::defaultAction; - private Runnable createCollectionAction = this::defaultAction; - - public CreateCollectionView() { - super("Создание новой коллекции"); - - sizeLabel = addLabel("Размер: не задан"); - - fillMethodLabel = addLabel("Способ заполнения: Пользовательские данные"); - - addButton("Задать размер", () -> setSizeAction.run()); - - addButton("Настроить способ заполнения", () -> configureFillMethodAction.run()); - - addButton("Создать новую коллекцию", () -> createCollectionAction.run()); - - addButton("Выйти", this::close); - } - - public void setSetSizeAction(Runnable action) { - setSizeAction = action; - } - - public void setConfigureFillMethodAction(Runnable action) { - configureFillMethodAction = action; - } - - public void setCreateCollectionAction(Runnable action) { - createCollectionAction = action; - } - - public void updateSize(int size) { - sizeLabel.setText("Размер: " + size); - } - - public void updateFillMethod(FillMethod fillMethod) { - fillMethodLabel.setText("Способ заполнения: " + getFillMethodName(fillMethod)); - } - - public void showMessage(WindowBasedTextGUI gui, String message) { - PanelView messageView = new PanelView("Информация"); - - messageView.addLabel(message); - - messageView.addButton("OK", messageView::close); - - messageView.show(gui); - } - - public void showSetSizeDialog(WindowBasedTextGUI gui, IntConsumer onSizeEntered) { - PanelView dialog = new PanelView("Задать размер"); - - dialog.addLabel("Введите размер:"); - - TextBox textBox = dialog.addTextBox(); - - dialog.addButton("Подтвердить", () -> handleSizeInput(gui, dialog, textBox, onSizeEntered)); - - dialog.addButton("Вернуться", dialog::close); - - dialog.show(gui); - } - - private void handleSizeInput( - WindowBasedTextGUI gui, PanelView dialog, TextBox textBox, IntConsumer onSizeEntered) { - try { - int size = Integer.parseInt(textBox.getText().trim()); - - dialog.close(); - onSizeEntered.accept(size); - - } catch (NumberFormatException e) { - showMessage(gui, "Введите целое число."); - } - } - - private String getFillMethodName(FillMethod fillMethod) { - return switch (fillMethod) { - case CUSTOM -> "Пользовательские данные"; - case RANDOM -> "Случайные данные"; - case FILE -> "Данные из файла"; - }; - } -} diff --git a/app/src/main/java/ru/project/view/FillMethodView.java b/app/src/main/java/ru/project/view/FillMethodView.java deleted file mode 100644 index 027d7fe..0000000 --- a/app/src/main/java/ru/project/view/FillMethodView.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.project.view; - -public class FillMethodView extends ActionListView { - - private Runnable customAction = this::defaultAction; - private Runnable randomAction = this::defaultAction; - private Runnable fileAction = this::defaultAction; - - public FillMethodView() { - super("Настроить способ заполнения"); - - addItem("Пользовательские данные", () -> customAction.run()); - - addItem("Случайные данные", () -> randomAction.run()); - - addItem("Данные из файла", () -> fileAction.run()); - - addItem("Выйти", this::close); - } - - public void setCustomAction(Runnable action) { - customAction = action; - } - - public void setRandomAction(Runnable action) { - randomAction = action; - } - - public void setFileAction(Runnable action) { - fileAction = action; - } -} diff --git a/app/src/main/java/ru/project/view/MainView.java b/app/src/main/java/ru/project/view/MainView.java deleted file mode 100644 index 55cbdb4..0000000 --- a/app/src/main/java/ru/project/view/MainView.java +++ /dev/null @@ -1,59 +0,0 @@ -package ru.project.view; - - -public class MainView extends ActionListView { - - private Runnable showCollectionAction = this::defaultAction; - private Runnable createCollectionAction = this::defaultAction; - private Runnable saveCollectionAction = this::defaultAction; - private Runnable sortCollectionAction = this::defaultAction; - private Runnable searchCollectionAction = this::defaultAction; - private Runnable countOccurrencesAction = this::defaultAction; - private Runnable exitAction = this::defaultAction; - - public MainView() { - super("Главное меню"); - - addItem("Показать текущую коллекцию", () -> showCollectionAction.run()); - - addItem("Меню создания новой коллекции", () -> createCollectionAction.run()); - - addItem("Меню сохранения текущей коллекции в файл", () -> saveCollectionAction.run()); - - addItem("Меню сортировки текущей коллекции", () -> sortCollectionAction.run()); - - addItem("Меню поиска элемента в текущей коллекции", () -> searchCollectionAction.run()); - - addItem("Меню подсчёта количества вхождений элемента", () -> countOccurrencesAction.run()); - - addItem("Завершить выполнение программы", () -> exitAction.run()); - } - - public void setShowCollectionAction(Runnable action) { - showCollectionAction = action; - } - - public void setCreateCollectionAction(Runnable action) { - createCollectionAction = action; - } - - public void setSaveCollectionAction(Runnable action) { - saveCollectionAction = action; - } - - public void setSortCollectionAction(Runnable action) { - sortCollectionAction = action; - } - - public void setSearchCollectionAction(Runnable action) { - searchCollectionAction = action; - } - - public void setCountOccurrencesAction(Runnable action) { - countOccurrencesAction = action; - } - - public void setExitAction(Runnable action) { - exitAction = action; - } -} diff --git a/app/src/main/java/ru/project/view/PanelView.java b/app/src/main/java/ru/project/view/PanelView.java deleted file mode 100644 index ab36e50..0000000 --- a/app/src/main/java/ru/project/view/PanelView.java +++ /dev/null @@ -1,47 +0,0 @@ -package ru.project.view; - -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; - -public class PanelView extends BaseView { - - protected final Panel panel; - - public PanelView(String title) { - this(new Panel(), title); - } - - private PanelView(Panel panel, String title) { - super(title, panel); - - this.panel = panel; - panel.setLayoutManager(new LinearLayout()); - } - - public Button addButton(String text, Runnable action) { - Button button = new Button(text, action); - - panel.addComponent(button); - - return button; - } - - public Label addLabel(String text) { - Label label = new Label(text); - - panel.addComponent(label); - - return label; - } - - public TextBox addTextBox() { - TextBox textBox = new TextBox(); - - panel.addComponent(textBox); - - return textBox; - } -} From 74effff84e62a94347e272dea393cfbc4f0502cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Thu, 27 Aug 2026 18:30:31 +0300 Subject: [PATCH 056/110] feat: implement BubbleSortStrategy --- .../project/sorting/BubbleSortStrategy.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/src/main/java/ru/project/sorting/BubbleSortStrategy.java 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..38172db --- /dev/null +++ b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java @@ -0,0 +1,24 @@ +package ru.project.sorting; + +import ru.project.list.MyList; +import java.util.Comparator; + +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); + } + } + } + } +} \ No newline at end of file From 7a856903108701d64c3c2629577c88a87781f5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Thu, 27 Aug 2026 18:44:46 +0300 Subject: [PATCH 057/110] feat: add constants for validation, use them in constructor; implement with-methods --- .../main/java/ru/project/student/Student.java | 67 ++++++++++--------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index 9dc74e2..be6fa91 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -3,28 +3,34 @@ 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) { - // Проверка groupNumber на формат - if (groupNumber == null || !groupNumber.matches("^[A-Z][0-9]{2}$")) { - throw new IllegalArgumentException("Group number must match format: one uppercase letter + two digits (e.g. A12)"); + if (groupNumber == null || !groupNumber.matches(GROUP_NUMBER_PATTERN)) { + throw new IllegalArgumentException( + "Group number must match pattern: " + GROUP_NUMBER_PATTERN); } - if (averageGrade < 0.0 || averageGrade > 5.0) { - throw new IllegalArgumentException("Average grade must be between 0.0 and 5.0"); + 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; } - // Геттеры — исправлен тип groupNumber (теперь String) public String getGroupNumber() { return groupNumber; } @@ -37,7 +43,6 @@ public class Student { return recordBookNumber; } - // Методы копирования (with-методы) public Student withGroupNumber(String groupNumber) { return new Student(groupNumber, this.averageGrade, this.recordBookNumber); } @@ -50,7 +55,20 @@ public class Student { return new Student(this.groupNumber, this.averageGrade, recordBookNumber); } - @Override + } + + 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 + '\'' + @@ -61,8 +79,12 @@ public class Student { @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } Student student = (Student) obj; return Double.compare(averageGrade, student.averageGrade) == 0 && recordBookNumber == student.recordBookNumber @@ -73,21 +95,4 @@ public class Student { public int hashCode() { return Objects.hash(groupNumber, averageGrade, recordBookNumber); } - - // ------------------------ Статические геттеры для границ и формата ------------------------ - public static double getMinAverageGrade() { - return 0.0; - } - - public static double getMaxAverageGrade() { - return 5.0; - } - - public static int getMinRecordBookNumber() { - return 1; - } - - public static String getGroupNumberPattern() { - return "^[A-Z][0-9]{2}$"; - } -} \ No newline at end of file +} From a7a253d7264bf677f320386761bac38931fd91c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Thu, 27 Aug 2026 00:43:53 +0300 Subject: [PATCH 058/110] fix(sorting): update MyList package imports --- app/src/main/java/ru/project/sorting/MergeSortStrategy.java | 2 +- app/src/main/java/ru/project/sorting/SortStrategy.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java index ab22562..598f754 100644 --- a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java +++ b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java @@ -1,6 +1,6 @@ package ru.project.sorting; -import ru.project.list.MyList; +import ru.project.collection.MyList; import java.util.Comparator; diff --git a/app/src/main/java/ru/project/sorting/SortStrategy.java b/app/src/main/java/ru/project/sorting/SortStrategy.java index 7dc9983..059d908 100644 --- a/app/src/main/java/ru/project/sorting/SortStrategy.java +++ b/app/src/main/java/ru/project/sorting/SortStrategy.java @@ -1,6 +1,7 @@ package ru.project.sorting; -import ru.project.list.MyList; +import ru.project.collection.MyList; + import java.util.Comparator; public interface SortStrategy { From 4794f03f3cdfefa98209be816fc333cf86a8406d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Thu, 27 Aug 2026 01:14:23 +0300 Subject: [PATCH 059/110] feat(sorting): add even-only merge sort strategy --- .../sorting/EvenOnlyMergeSortStrategy.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java diff --git a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java new file mode 100644 index 0000000..a784d7a --- /dev/null +++ b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java @@ -0,0 +1,128 @@ +package ru.project.sorting; + +import ru.project.collection.MyList; + +import java.util.Comparator; +import java.util.function.ToIntFunction; + +public class EvenOnlyMergeSortStrategy implements SortStrategy { + private final ToIntFunction valueExtractor; + + public EvenOnlyMergeSortStrategy(ToIntFunction valueExtractor) { + this.valueExtractor = valueExtractor; + } + + @Override + public void sort(MyList list, Comparator comparator) { + + 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; + } + + @SuppressWarnings("unchecked") + T[] evenElements = (T[]) new Object[evenCount]; + + 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[evenIndex] = element; + evenIndexes[evenIndex] = i; + evenIndex++; + } + } + mergeSort(evenElements, 0, evenElements.length - 1, comparator); + + for (int i = 0; i < evenIndexes.length; i++) { + list.set(evenIndexes[i], evenElements[i]); + } + } + + private void mergeSort( + T[] elements, + int left, + int right, + Comparator comparator + ) { + if (left >= right) { + return; + } + + int mid = (left + right) / 2; + + mergeSort(elements, left, mid, comparator); + mergeSort(elements, mid + 1, right, comparator); + + merge(elements, left, mid, right, comparator); + } + private void merge( + T[] elements, + 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] = elements[left + i]; + } + + for (int j = 0; j < rightSize; j++) { + rightArray[j] = elements[mid + 1 + j]; + } + + int i = 0; + int j = 0; + int k = left; + + while (i < leftSize && j < rightSize) { + if (comparator.compare(leftArray[i], rightArray[j]) <= 0) { + elements[k] = leftArray[i]; + i++; + } else { + elements[k] = rightArray[j]; + j++; + } + + k++; + } + + while (i < leftSize) { + elements[k] = leftArray[i]; + i++; + k++; + } + + while (j < rightSize) { + elements[k] = rightArray[j]; + j++; + k++; + } + } +} + From a9f578ad363dacf59018c76f2dfbec5886688524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Thu, 27 Aug 2026 23:09:20 +0300 Subject: [PATCH 060/110] fix(collection): add package declaration to MyLinkedList --- app/src/main/java/ru/project/collection/MyLinkedList.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 23b2b75..270689c 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -1,3 +1,4 @@ +package ru.project.collection; import java.util.Arrays; public class MyLinkedList implements MyList { From 62f45cea65b3088a6c65606531cc4efe40bc5cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Thu, 27 Aug 2026 23:54:34 +0300 Subject: [PATCH 061/110] fix(sorting): update Student imports --- .../java/ru/project/sorting/StudentAverageGradeComparator.java | 2 +- .../main/java/ru/project/sorting/StudentGroupComparator.java | 2 +- .../java/ru/project/sorting/StudentRecordBookComparator.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java index 488aff9..fea72f6 100644 --- a/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java @@ -1,6 +1,6 @@ package ru.project.sorting; -import ru.project.model.Student; +import ru.project.student.Student; import java.util.Comparator; public class StudentAverageGradeComparator implements Comparator { diff --git a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java index a43c0e1..b23496e 100644 --- a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java @@ -1,6 +1,6 @@ package ru.project.sorting; -import ru.project.model.Student; +import ru.project.student.Student; import java.util.Comparator; public class StudentGroupComparator implements Comparator { diff --git a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java index fd72fb4..0e4a714 100644 --- a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java @@ -1,6 +1,6 @@ package ru.project.sorting; -import ru.project.model.Student; +import ru.project.student.Student; import java.util.Comparator; public class StudentRecordBookComparator implements Comparator { From da8a42d89523e16a440d49fda4bbf78c42d9aaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Fri, 28 Aug 2026 00:07:16 +0300 Subject: [PATCH 062/110] test(sorting): add even-only merge sort tests --- .../EvenOnlyMergeSortStrategyTest.java | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java diff --git a/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java b/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java new file mode 100644 index 0000000..b423cba --- /dev/null +++ b/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java @@ -0,0 +1,149 @@ +package ru.project.sorting; + +import org.junit.jupiter.api.Test; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EvenOnlyMergeSortStrategyTest { + + @Test + void shouldSortOnlyEvenElements() { + MyList list = new MyLinkedList<>(); + + list.add(7); + list.add(8); + list.add(3); + list.add(2); + list.add(5); + list.add(4); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(7, list.get(0)); + assertEquals(2, list.get(1)); + assertEquals(3, list.get(2)); + assertEquals(4, list.get(3)); + assertEquals(5, list.get(4)); + assertEquals(8, list.get(5)); + + assertEquals(6, list.size()); + } + + @Test + void shouldKeepAllOddElementsUnchanged() { + MyList list = new MyLinkedList<>(); + + list.add(7); + list.add(3); + list.add(5); + list.add(1); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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 shouldSortAllEvenElements() { + MyList list = new MyLinkedList<>(); + + list.add(8); + list.add(2); + list.add(6); + list.add(4); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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 shouldKeepSingleElementUnchanged() { + MyList list = new MyLinkedList<>(); + + list.add(4); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(4, list.get(0)); + assertEquals(1, list.size()); + } + + @Test + void shouldHandleEmptyList() { + MyList list = new MyLinkedList<>(); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(value -> value); + + strategy.sort(list, Integer::compare); + + assertEquals(0, list.size()); + } + + @Test + void shouldUseValueExtractorToDetermineEvenElements() { + MyList list = new MyLinkedList<>(); + + list.add(4); + list.add(3); + list.add(2); + list.add(1); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(value -> value + 1); + + strategy.sort(list, Integer::compare); + + assertEquals(4, list.get(0)); + assertEquals(1, list.get(1)); + assertEquals(2, list.get(2)); + assertEquals(3, list.get(3)); + } + + @Test + void shouldUseProvidedComparator() { + MyList list = new MyLinkedList<>(); + + list.add(7); + list.add(2); + list.add(3); + list.add(8); + list.add(5); + list.add(4); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(value -> value); + + strategy.sort(list, (first, second) -> Integer.compare(second, first)); + + assertEquals(7, list.get(0)); + assertEquals(8, list.get(1)); + assertEquals(3, list.get(2)); + assertEquals(4, list.get(3)); + assertEquals(5, list.get(4)); + assertEquals(2, list.get(5)); + } +} + + From 9a670a7380280a0adebccb0d152cedc5e593373b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Thu, 27 Aug 2026 18:51:52 +0300 Subject: [PATCH 063/110] feat: add EvenOnlyMergeSortStrategy --- .../sorting/EvenOnlyMergeSortStrategy.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java diff --git a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java new file mode 100644 index 0000000..dea539e --- /dev/null +++ b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java @@ -0,0 +1,84 @@ +package ru.project.sorting; + +import ru.project.list.MyList; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.ToIntFunction; + +public class EvenOnlyMergeSortStrategy implements SortStrategy { + + private final ToIntFunction valueExtractor; + + public EvenOnlyMergeSortStrategy(ToIntFunction valueExtractor) { + this.valueExtractor = valueExtractor; + } + + @Override + public void sort(MyList list, Comparator comparator) { + if (list == null || list.size() <= 1) { + return; + } + + List evenIndices = new ArrayList<>(); + List evenElements = new ArrayList<>(); + + for (int i = 0; i < list.size(); i++) { + T element = list.get(i); + int value = valueExtractor.applyAsInt(element); + if (value % 2 == 0) { + evenIndices.add(i); + evenElements.add(element); + } + } + + if (evenElements.size() <= 1) { + return; + } + + List sortedEven = mergeSort(evenElements, comparator); + + for (int j = 0; j < evenIndices.size(); j++) { + list.set(evenIndices.get(j), sortedEven.get(j)); + } + } + + private List mergeSort(List list, Comparator comparator) { + if (list.size() <= 1) { + return list; + } + + int mid = list.size() / 2; + List left = mergeSort(list.subList(0, mid), comparator); + List right = mergeSort(list.subList(mid, list.size()), comparator); + + return merge(left, right, comparator); + } + + private List merge(List left, List right, Comparator comparator) { + List result = new ArrayList<>(); + int i = 0, j = 0; + + while (i < left.size() && j < right.size()) { + if (comparator.compare(left.get(i), right.get(j)) <= 0) { + result.add(left.get(i)); + i++; + } else { + result.add(right.get(j)); + j++; + } + } + + while (i < left.size()) { + result.add(left.get(i)); + i++; + } + + while (j < right.size()) { + result.add(right.get(j)); + j++; + } + + return result; + } +} \ No newline at end of file From 80ebc71366a1e45234b0b0be0fda4d6a694c3456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 11:53:00 +0400 Subject: [PATCH 064/110] test: add comprehensive unit tests for Student model --- .../java/ru/project/student/StudentTest.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 app/src/main/java/ru/project/student/StudentTest.java diff --git a/app/src/main/java/ru/project/student/StudentTest.java b/app/src/main/java/ru/project/student/StudentTest.java new file mode 100644 index 0000000..cdce6d8 --- /dev/null +++ b/app/src/main/java/ru/project/student/StudentTest.java @@ -0,0 +1,139 @@ +package ru.project.student; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +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"); + } +} \ No newline at end of file From daa658347064bba0cff87a777e077b905c632bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 12:06:00 +0400 Subject: [PATCH 065/110] test: reorganize Student tests and fix duplicate test file --- .../main/java/ru/project/student/Student.java | 2 - .../java/ru/project/student/StudentTest.java | 139 ------------ .../java/ru/project/student/StudentTest.java | 209 ++++++++---------- 3 files changed, 93 insertions(+), 257 deletions(-) delete mode 100644 app/src/main/java/ru/project/student/StudentTest.java diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index be6fa91..b5d64d2 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -55,8 +55,6 @@ public class Student { return new Student(this.groupNumber, this.averageGrade, recordBookNumber); } - } - public static double getMaxAverageGrade() { return MAX_AVERAGE_GRADE; } diff --git a/app/src/main/java/ru/project/student/StudentTest.java b/app/src/main/java/ru/project/student/StudentTest.java deleted file mode 100644 index cdce6d8..0000000 --- a/app/src/main/java/ru/project/student/StudentTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package ru.project.student; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -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"); - } -} \ No newline at end of file diff --git a/app/src/test/java/ru/project/student/StudentTest.java b/app/src/test/java/ru/project/student/StudentTest.java index e9a7434..09cf2e7 100644 --- a/app/src/test/java/ru/project/student/StudentTest.java +++ b/app/src/test/java/ru/project/student/StudentTest.java @@ -2,167 +2,144 @@ package ru.project.student; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; class StudentTest { - @Test - void shouldCreateValidStudent() { - Student student = new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); - assertEquals("101", student.getGroupNumber()); + @Test + void testValidStudentCreation() { + Student student = new Student("A12", 4.5, 12345); + assertEquals("A12", student.getGroupNumber()); assertEquals(4.5, student.getAverageGrade()); - assertEquals("12345", student.getRecordBookNumber()); + assertEquals(12345, student.getRecordBookNumber()); } @Test - void shouldThrowExceptionWhenGroupNumberIsEmpty() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber("") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); - }); + 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 shouldThrowExceptionWhenAverageGradeIsGreaterThanFive() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(6.0) - .setRecordBookNumber("12345") - .build(); - }); + 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 shouldThrowExceptionWhenAverageGradeIsLessThanZero() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(-1.0) - .setRecordBookNumber("12345") - .build(); - }); + void testValidAverageGrades() { + assertDoesNotThrow(() -> new Student("A12", 0.0, 1)); + assertDoesNotThrow(() -> new Student("A12", 5.0, 1)); } @Test - void shouldCreateStudentWhenAverageGradeIsZero() { - Student student = new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(0.0) - .setRecordBookNumber("12345") - .build(); - - assertEquals(0.0, student.getAverageGrade()); + void testInvalidAverageGrades() { + assertThrows(IllegalArgumentException.class, () -> new Student("A12", -1.0, 1)); + assertThrows(IllegalArgumentException.class, () -> new Student("A12", 6.0, 1)); } @Test - void shouldCreateStudentWhenAverageGradeIsFive() { - Student student = new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(5.0) - .setRecordBookNumber("12345") - .build(); - - assertEquals(5.0, student.getAverageGrade()); + void testValidRecordBookNumbers() { + assertDoesNotThrow(() -> new Student("A12", 4.5, 1)); + assertDoesNotThrow(() -> new Student("A12", 4.5, 12345)); } @Test - void shouldThrowExceptionWhenRecordBookNumberIsEmpty() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber("") - .build(); - }); + void testInvalidRecordBookNumbers() { + assertThrows(IllegalArgumentException.class, () -> new Student("A12", 4.5, 0)); + assertThrows(IllegalArgumentException.class, () -> new Student("A12", 4.5, -1)); } @Test - void shouldThrowExceptionWhenGroupNumberIsNull() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber(null) - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); - }); + 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 shouldThrowExceptionWhenGroupNumberIsBlank() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber(" ") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); - }); + 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 shouldThrowExceptionWhenRecordBookNumberIsNull() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber(null) - .build(); - }); + 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 shouldThrowExceptionWhenRecordBookNumberIsBlank() { - assertThrows(IllegalArgumentException.class, () -> { - new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber(" ") - .build(); - }); + void testWithInvalidGroupNumber() { + Student original = new Student("A12", 4.5, 12345); + assertThrows(IllegalArgumentException.class, + () -> original.withGroupNumber("a12")); + assertEquals("A12", original.getGroupNumber()); // не изменился } @Test - void shouldReturnTrueWhenStudentsAreEqual() { - Student student1 = new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); - - Student student2 = new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); - - assertEquals(student1, student2); - assertEquals(student1.hashCode(), student2.hashCode()); + void testWithInvalidAverageGrade() { + Student original = new Student("A12", 4.5, 12345); + assertThrows(IllegalArgumentException.class, + () -> original.withAverageGrade(6.0)); + assertEquals(4.5, original.getAverageGrade()); } @Test - void shouldReturnFalseWhenStudentsAreDifferent() { - Student student1 = new StudentBuilder() - .setGroupNumber("101") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); + void testWithInvalidRecordBookNumber() { + Student original = new Student("A12", 4.5, 12345); + assertThrows(IllegalArgumentException.class, + () -> original.withRecordBookNumber(0)); + assertEquals(12345, original.getRecordBookNumber()); + } - Student student2 = new StudentBuilder() - .setGroupNumber("102") - .setAverageGrade(4.5) - .setRecordBookNumber("12345") - .build(); + @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); - assertNotEquals(student1, student2); + assertEquals(s1, s2); + assertEquals(s1.hashCode(), s2.hashCode()); + + assertNotEquals(s1, s3); + assertNotEquals(s1, s4); + assertNotEquals(s1, s5); + assertNotEquals(s1, null); + assertNotEquals(s1, "some string"); } } From e47732b9ec2d1758912b53de8445418d16248fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 12:14:10 +0400 Subject: [PATCH 066/110] fix: correct student group comparator --- .../main/java/ru/project/sorting/StudentGroupComparator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java index b23496e..75cae82 100644 --- a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java @@ -6,7 +6,7 @@ import java.util.Comparator; public class StudentGroupComparator implements Comparator { @Override - public String compare(Student s1, Student s2) { - return Integer.compare(s1.getGroupNumber(), s2.getGroupNumber()); + public int compare(Student s1, Student s2) { + return s1.getGroupNumber().compareTo(s2.getGroupNumber()); } } From e53e1798e59b825645923add2d40c0012ce0bf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Fri, 28 Aug 2026 17:50:35 +0300 Subject: [PATCH 067/110] fix: make constants private, return hashCode check in equals --- .../main/java/ru/project/student/Student.java | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java index b5d64d2..1e271a4 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -3,11 +3,10 @@ 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 static final double MIN_AVERAGE_GRADE = 0.0; + private static final double MAX_AVERAGE_GRADE = 5.0; + private static final int MIN_RECORD_BOOK_NUMBER = 1; + private static final String GROUP_NUMBER_PATTERN = "^[A-Z][0-9]{2}$"; private final String groupNumber; private final double averageGrade; @@ -16,15 +15,18 @@ public class Student { 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); + "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); + "Average grade must be between " + MIN_AVERAGE_GRADE + " and " + MAX_AVERAGE_GRADE + ); } if (recordBookNumber < MIN_RECORD_BOOK_NUMBER) { throw new IllegalArgumentException( - "Record book number must be at least " + MIN_RECORD_BOOK_NUMBER); + "Record book number must be at least " + MIN_RECORD_BOOK_NUMBER + ); } this.groupNumber = groupNumber; this.averageGrade = averageGrade; @@ -43,16 +45,8 @@ public class Student { 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() { @@ -67,6 +61,19 @@ public class Student { return GROUP_NUMBER_PATTERN; } + 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); + } + + @Override public String toString() { return "Student{" + "groupNumber='" + groupNumber + '\'' + @@ -83,6 +90,9 @@ public class Student { 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 @@ -93,4 +103,4 @@ public class Student { public int hashCode() { return Objects.hash(groupNumber, averageGrade, recordBookNumber); } -} +} \ No newline at end of file From 2e44ba8fd5d83ab8116b86d0b2bc4dafb2e7ee34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Fri, 28 Aug 2026 17:53:14 +0300 Subject: [PATCH 068/110] refactor: move StudentTest to test directory --- .../java/ru/project/student/StudentTest1.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 app/src/test/java/ru/project/student/StudentTest1.java diff --git a/app/src/test/java/ru/project/student/StudentTest1.java b/app/src/test/java/ru/project/student/StudentTest1.java new file mode 100644 index 0000000..cdce6d8 --- /dev/null +++ b/app/src/test/java/ru/project/student/StudentTest1.java @@ -0,0 +1,139 @@ +package ru.project.student; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +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"); + } +} \ No newline at end of file From 18e1608103f9c116fbb912a1bfe6af6878874c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Fri, 28 Aug 2026 18:17:53 +0300 Subject: [PATCH 069/110] test: add comprehensive tests for all SortStrategy implementations --- .../ru/project/Sorting/SortStrategyTest.java | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 app/src/test/java/ru/project/Sorting/SortStrategyTest.java 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..6e0472c --- /dev/null +++ b/app/src/test/java/ru/project/Sorting/SortStrategyTest.java @@ -0,0 +1,163 @@ +package ru.project.sorting; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.project.list.MyList; +import ru.project.list.MyLinkedList; +import ru.project.student.Student; + +import java.util.Comparator; + +import static org.junit.jupiter.api.Assertions.*; + +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)); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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)); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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)); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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()); + } + } +} \ No newline at end of file From faae240b1d9d57bc48f0a20cf159570c41d397af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 19:34:56 +0400 Subject: [PATCH 070/110] refactor: rename MessageWindow create method to show --- app/src/main/java/ru/project/ui/window/MessageWindow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ru/project/ui/window/MessageWindow.java b/app/src/main/java/ru/project/ui/window/MessageWindow.java index ee56ea8..d6d592c 100644 --- a/app/src/main/java/ru/project/ui/window/MessageWindow.java +++ b/app/src/main/java/ru/project/ui/window/MessageWindow.java @@ -22,7 +22,7 @@ public class MessageWindow extends BaseWindow { setComponent(panel); } - public static void create(WindowBasedTextGUI gui, String message) { + public static void show(WindowBasedTextGUI gui, String message) { new MessageWindow(gui, message).show(); } } From bce5707f5e9725232086b59142f5bd775714af83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 19:35:33 +0400 Subject: [PATCH 071/110] feat: add input value window Add a reusable window for entering and editing values. --- .../ui/window/CollectionSizeInputWindow.java | 54 ------------------ .../project/ui/window/InputValueWindow.java | 56 +++++++++++++++++++ 2 files changed, 56 insertions(+), 54 deletions(-) delete mode 100644 app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java create mode 100644 app/src/main/java/ru/project/ui/window/InputValueWindow.java diff --git a/app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java b/app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java deleted file mode 100644 index 25b7775..0000000 --- a/app/src/main/java/ru/project/ui/window/CollectionSizeInputWindow.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.project.ui.window; - -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.IntConsumer; -import ru.project.ui.base.BaseWindow; - -public class CollectionSizeInputWindow extends BaseWindow { - - private final TextBox sizeTextBox; - private final IntConsumer onSizeEntered; - - public CollectionSizeInputWindow(WindowBasedTextGUI gui, IntConsumer onSizeEntered) { - - super("Задать размер", gui); - - this.onSizeEntered = onSizeEntered; - - Panel panel = new Panel(); - panel.setLayoutManager(new LinearLayout()); - - panel.addComponent(new Label("Введите размер:")); - - sizeTextBox = new TextBox(); - panel.addComponent(sizeTextBox); - - panel.addComponent(new Button("Подтвердить", this::handleSizeEntered)); - - panel.addComponent(new Button("Выйти", this::close)); - - setComponent(panel); - } - - private void handleSizeEntered() { - try { - int size = Integer.parseInt(sizeTextBox.getText().trim()); - - if (size <= 0) { - MessageWindow.create(gui, "Размер должен быть больше 0."); - return; - } - - close(); - onSizeEntered.accept(size); - - } catch (NumberFormatException e) { - MessageWindow.create(gui, "Введите целое число."); - } - } -} diff --git a/app/src/main/java/ru/project/ui/window/InputValueWindow.java b/app/src/main/java/ru/project/ui/window/InputValueWindow.java new file mode 100644 index 0000000..d21a79b --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/InputValueWindow.java @@ -0,0 +1,56 @@ +package ru.project.ui.window; + +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.BaseWindow; + +public class InputValueWindow extends BaseWindow { + + 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.show(gui, "Значение не может быть пустым."); + return; + } + + if (onSave.test(value)) { + close(); + } + } +} From 5d0052805c98fd8ea5c190c0b043553eebeaac40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 19:37:27 +0400 Subject: [PATCH 072/110] refactor: replace current collection window Replace CurrentCollectionWindow with CollectionDisplayWindow. --- .../ui/window/CollectionDisplayWindow.java | 102 ++++++++++++++++++ .../ui/window/CurrentCollectionWindow.java | 41 ------- 2 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java delete mode 100644 app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java diff --git a/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java b/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java new file mode 100644 index 0000000..4ca3c5e --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java @@ -0,0 +1,102 @@ +package ru.project.ui.window; + +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.BaseWindow; + +public class CollectionDisplayWindow extends BaseWindow { + + 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/window/CurrentCollectionWindow.java b/app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java deleted file mode 100644 index 0fd7e29..0000000 --- a/app/src/main/java/ru/project/ui/window/CurrentCollectionWindow.java +++ /dev/null @@ -1,41 +0,0 @@ -package ru.project.ui.window; - -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.model.AppState; -import ru.project.student.Student; -import ru.project.ui.base.BaseWindow; - -public class CurrentCollectionWindow extends BaseWindow { - - public CurrentCollectionWindow(WindowBasedTextGUI gui, AppState state) { - super("Текущая коллекция", gui); - - Panel panel = new Panel(); - panel.setLayoutManager(new LinearLayout()); - - MyList collection = state.getCollection(); - - if (collection == null || collection.isEmpty()) { - panel.addComponent(new Label("Текущая коллекция пуста.")); - } else { - addStudents(panel, collection); - } - - panel.addComponent(new Button("Выйти", this::close)); - - setComponent(panel); - } - - private void addStudents(Panel panel, MyList collection) { - for (int i = 0; i < collection.size(); i++) { - Student student = collection.get(i); - - panel.addComponent(new Label((i + 1) + ". " + student)); - } - } -} From ff0c063078ccedd59dca3c139a932ca48819d7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 19:51:47 +0400 Subject: [PATCH 073/110] feat: add custom data collection settings Add support for storing and configuring custom student data collections in AppState. Introduce a custom data settings window for entering and validating student fields, selecting an object index, previewing student data, and applying prepared data to the current collection. Refactor collection creation to use reusable input handling and update collection display and message window usage. --- .../main/java/ru/project/model/AppState.java | 23 +- .../project/ui/menu/CreateCollectionMenu.java | 73 +++-- .../java/ru/project/ui/menu/MainMenu.java | 18 +- .../ui/window/CustomDataSettingsWindow.java | 265 ++++++++++++++++++ 4 files changed, 350 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java diff --git a/app/src/main/java/ru/project/model/AppState.java b/app/src/main/java/ru/project/model/AppState.java index 9b2a61e..e430057 100644 --- a/app/src/main/java/ru/project/model/AppState.java +++ b/app/src/main/java/ru/project/model/AppState.java @@ -6,20 +6,31 @@ import ru.project.student.Student; public class AppState { - private MyList collection; + private MyList currentCollection; + private MyList customDataCollection; private String currentFilePath; public AppState() { - this.collection = new MyLinkedList<>(); + this.currentCollection = new MyLinkedList<>(); + this.customDataCollection = new MyLinkedList<>(); this.currentFilePath = null; } - public MyList getCollection() { - return collection; + public MyList getCurrentCollection() { + return currentCollection; } - public void setCollection(MyList collection) { - this.collection = collection; + public void setCurrentCollection(MyList currentCollection) { + this.currentCollection = currentCollection; + } + + public MyList getCustomDataCollection() { + return customDataCollection; + } + + public void setCustomDataCollection(MyList customDataCollection) { + + this.customDataCollection = customDataCollection; } public String getCurrentFilePath() { diff --git a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java index 2f7facd..48d2f0c 100644 --- a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java @@ -8,7 +8,8 @@ import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import ru.project.model.AppState; import ru.project.model.FillMethod; import ru.project.ui.base.BaseWindow; -import ru.project.ui.window.CollectionSizeInputWindow; +import ru.project.ui.window.CustomDataSettingsWindow; +import ru.project.ui.window.InputValueWindow; import ru.project.ui.window.MessageWindow; public class CreateCollectionMenu extends BaseWindow { @@ -19,9 +20,13 @@ public class CreateCollectionMenu extends BaseWindow { private final Label sizeLabel; private final Label fillMethodLabel; + private final AppState state; + public CreateCollectionMenu(WindowBasedTextGUI gui, AppState state) { super("Создание новой коллекции", gui); + this.state = state; + collectionSize = 0; selectedFillMethod = FillMethod.CUSTOM; @@ -35,9 +40,12 @@ public class CreateCollectionMenu extends BaseWindow { panel.addComponent(sizeLabel); panel.addComponent(fillMethodLabel); - panel.addComponent(new Button("Задать размер", this::openCollectionSizeInputWindow)); + panel.addComponent(new Button("Задать размер", this::openCollectionSizeInputValueWindow)); - panel.addComponent(new Button("Настроить способ заполнения", this::openFillMethodMenu)); + panel.addComponent(new Button("Выбрать способ заполнения", this::selectFillMethod)); + + panel.addComponent( + new Button("Настроить текущий способ заполнения", this::setupCurrentFillMethod)); panel.addComponent(new Button("Создать новую коллекцию", this::createCollection)); @@ -46,20 +54,53 @@ public class CreateCollectionMenu extends BaseWindow { setComponent(panel); } - private void openCollectionSizeInputWindow() { - CollectionSizeInputWindow window = - new CollectionSizeInputWindow( + private void openCollectionSizeInputValueWindow() { + InputValueWindow window = + new InputValueWindow( gui, - size -> { - collectionSize = size; - sizeLabel.setText("Размер: " + collectionSize); - }); + "Размер коллекции", + "Введите размер коллекции:", + collectionSize > 0 ? String.valueOf(collectionSize) : null, + this::handleSizeEntered); window.show(); } - private void openFillMethodMenu() { - FillMethodSelectionWindow menu = + private boolean handleSizeEntered(String value) { + try { + int enteredSize = Integer.parseInt(value); + if (enteredSize <= 0) { + MessageWindow.show(gui, "Размер должен быть больше 0."); + return false; + } + collectionSize = enteredSize; + sizeLabel.setText("Размер: " + collectionSize); + return true; + } catch (NumberFormatException e) { + MessageWindow.show(gui, "Введите целое число."); + return false; + } + } + + private void setupCurrentFillMethod() { + switch (selectedFillMethod) { + case CUSTOM -> { + BaseWindow window = new CustomDataSettingsWindow(gui, state, collectionSize); + + window.show(); + } + + case FILE -> + // TODO + MessageWindow.show(gui, "Не реализовано."); + + case RANDOM -> + MessageWindow.show(gui, "Для случайных данных дополнительные настройки не требуются."); + } + } + + private void selectFillMethod() { + BaseWindow menu = new FillMethodSelectionWindow( gui, fillMethod -> { @@ -73,7 +114,7 @@ public class CreateCollectionMenu extends BaseWindow { private void createCollection() { if (collectionSize <= 0) { - MessageWindow.create(gui, "Сначала задайте размер коллекции."); + MessageWindow.show(gui, "Сначала задайте размер коллекции."); return; } @@ -94,17 +135,17 @@ public class CreateCollectionMenu extends BaseWindow { private void createCollectionFromCustomData() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private void createCollectionFromRandomData() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private void createCollectionFromFileData() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private String getFillMethodName(FillMethod fillMethod) { diff --git a/app/src/main/java/ru/project/ui/menu/MainMenu.java b/app/src/main/java/ru/project/ui/menu/MainMenu.java index 3b3fbcb..308277e 100644 --- a/app/src/main/java/ru/project/ui/menu/MainMenu.java +++ b/app/src/main/java/ru/project/ui/menu/MainMenu.java @@ -2,9 +2,11 @@ package ru.project.ui.menu; import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.collection.MyList; import ru.project.model.AppState; +import ru.project.student.Student; import ru.project.ui.base.BaseWindow; -import ru.project.ui.window.CurrentCollectionWindow; +import ru.project.ui.window.CollectionDisplayWindow; import ru.project.ui.window.MessageWindow; public class MainMenu extends BaseWindow { @@ -37,35 +39,37 @@ public class MainMenu extends BaseWindow { } private void showCurrentCollection() { - CurrentCollectionWindow window = new CurrentCollectionWindow(gui, state); + MyList currentCollection = state.getCurrentCollection(); + + BaseWindow window = new CollectionDisplayWindow(gui, currentCollection); window.show(); } private void createCollection() { - CreateCollectionMenu window = new CreateCollectionMenu(gui, state); + BaseWindow window = new CreateCollectionMenu(gui, state); window.show(); } private void saveCollection() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private void sortCollection() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private void searchCollection() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private void countOccurrences() { // TODO - MessageWindow.create(gui, "Не реализовано."); + MessageWindow.show(gui, "Не реализовано."); } private void exit() { diff --git a/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java b/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java new file mode 100644 index 0000000..b59f805 --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java @@ -0,0 +1,265 @@ +package ru.project.ui.window; + +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.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.model.AppState; +import ru.project.student.Student; +import ru.project.student.StudentBuilder; +import ru.project.ui.base.BaseWindow; + +public class CustomDataSettingsWindow extends BaseWindow { + + private final AppState state; + private final int collectionSize; + private final StudentBuilder studentBuilder; + + private String groupNumber; + private Double averageGrade; + private Integer recordBookNumber; + private Integer selectedObjectIndex; + + private final Label groupNumberLabel; + private final Label averageGradeLabel; + private final Label recordBookNumberLabel; + private final Label indexLabel; + + private final MyList preparedCustomDataCollection; + + public CustomDataSettingsWindow(WindowBasedTextGUI gui, AppState state, int collectionSize) { + + super("Настройка пользовательских данных", gui); + + this.state = state; + this.collectionSize = collectionSize; + this.studentBuilder = new StudentBuilder(); + this.preparedCustomDataCollection = new MyLinkedList<>(); + + MyList customDataCollection = state.getCustomDataCollection(); + + int elementsToCopy = Math.min(customDataCollection.size(), collectionSize); + + for (int i = 0; i < elementsToCopy; i++) { + preparedCustomDataCollection.add(customDataCollection.get(i)); + } + + for (int i = elementsToCopy; i < collectionSize; i++) { + preparedCustomDataCollection.add(null); + } + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + groupNumberLabel = new Label("Номер группы: не задан"); + + averageGradeLabel = new Label("Средний балл: не задан"); + + recordBookNumberLabel = new Label("Номер зачётной книжки: не задан"); + + indexLabel = new Label("Индекс объекта: не задан"); + + panel.addComponent(groupNumberLabel); + panel.addComponent(averageGradeLabel); + panel.addComponent(recordBookNumberLabel); + panel.addComponent(indexLabel); + + 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 setGroupNumber() { + InputValueWindow window = + new InputValueWindow( + gui, + "Номер группы", + "Введите номер группы (например, A12):", + groupNumber, + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + MessageWindow.show(gui, "Номер группы должен соответствовать формату A12."); + return false; + } + + groupNumber = value; + studentBuilder.setGroupNumber(value); + + groupNumberLabel.setText("Номер группы: " + groupNumber); + + return true; + }); + + window.show(); + } + + 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.show(gui, "Средний балл должен быть от 0.0 до 5.0."); + return false; + } + + averageGrade = parsedValue; + studentBuilder.setAverageGrade(parsedValue); + + averageGradeLabel.setText("Средний балл: " + averageGrade); + + } catch (NumberFormatException e) { + MessageWindow.show(gui, "Введите корректное число."); + return false; + } + + return true; + }); + + window.show(); + } + + 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.show(gui, "Номер зачётной книжки должен быть положительным."); + return false; + } + + recordBookNumber = parsedValue; + studentBuilder.setRecordBookNumber(parsedValue); + + recordBookNumberLabel.setText("Номер зачётной книжки: " + recordBookNumber); + + } catch (NumberFormatException e) { + MessageWindow.show(gui, "Введите целое число."); + return false; + } + + return true; + }); + + window.show(); + } + + private void setObjectIndex() { + InputValueWindow window = + new InputValueWindow( + gui, + "Индекс объекта", + "Введите индекс объекта:", + selectedObjectIndex == null ? null : String.valueOf(selectedObjectIndex), + value -> { + try { + int index = Integer.parseInt(value); + + if (index < 0) { + MessageWindow.show(gui, "Индекс объекта не может быть отрицательным."); + return false; + } + + if (index >= collectionSize) { + MessageWindow.show(gui, "Индекс должен быть меньше размера коллекции."); + return false; + } + + selectedObjectIndex = index; + + indexLabel.setText("Индекс объекта: " + selectedObjectIndex); + + return true; + + } catch (NumberFormatException e) { + MessageWindow.show(gui, "Введите целое число."); + return false; + } + }); + + window.show(); + } + + private void editObject() { + if (!hasStudentData()) { + MessageWindow.show(gui, "Сначала задайте все поля студента."); + return; + } + + if (selectedObjectIndex == null) { + MessageWindow.show(gui, "Сначала задайте индекс объекта."); + return; + } + + if (selectedObjectIndex >= preparedCustomDataCollection.size()) { + MessageWindow.show(gui, "Индекс объекта выходит за границы подготовленной коллекции."); + return; + } + + Student student = studentBuilder.build(); + + preparedCustomDataCollection.set(selectedObjectIndex, student); + + MessageWindow.show(gui, "Данные объекта успешно заданы."); + } + + private void apply() { + MyList currentCollection = new MyLinkedList<>(); + + for (int i = 0; i < preparedCustomDataCollection.size(); i++) { + currentCollection.add(preparedCustomDataCollection.get(i)); + } + + state.setCustomDataCollection(preparedCustomDataCollection); + state.setCurrentCollection(currentCollection); + + close(); + } + + private void preview() { + if (!hasStudentData()) { + MessageWindow.show(gui, "Для предпросмотра сначала задайте все поля студента."); + return; + } + + Student student = studentBuilder.build(); + + MessageWindow.show(gui, student.toString()); + } + + private boolean hasStudentData() { + return groupNumber != null && averageGrade != null && recordBookNumber != null; + } +} From c8c9c05a655f33202764a466be45fd9be6534834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 21:10:13 +0400 Subject: [PATCH 074/110] docs: replace data source task with csv student io task --- docs/tasks/CsvStudentIO.md | 324 ++++++++++++++++++++++++++ docs/tasks/DataSource.md | 399 --------------------------------- docs/tasks/DataSourceUpdate.md | 81 ------- 3 files changed, 324 insertions(+), 480 deletions(-) create mode 100644 docs/tasks/CsvStudentIO.md delete mode 100644 docs/tasks/DataSource.md delete mode 100644 docs/tasks/DataSourceUpdate.md diff --git a/docs/tasks/CsvStudentIO.md b/docs/tasks/CsvStudentIO.md new file mode 100644 index 0000000..89e2ad5 --- /dev/null +++ b/docs/tasks/CsvStudentIO.md @@ -0,0 +1,324 @@ +# Техническое задание: чтение и дозапись `Student` в CSV + +## Задача + +Реализовать два независимых класса для работы с объектами `Student` в формате CSV: + +- `StudentCsvWriter` — дозапись студентов в файл; +- `StudentCsvReader` — последовательное чтение студентов из файла. + +Для работы с форматом CSV использовать готовую библиотеку для работы с CSV. Самостоятельно реализовывать парсинг CSV запрещено. + +Оба класса должны принимать **абсолютный путь к файлу**. + +Исключения `Student` и `StudentBuilder` не обрабатывать. + +--- + +# Формат CSV-файла + +Первая строка файла должна содержать заголовок: + +```text +groupNumber,averageGrade,recordBookNumber +``` + +Каждая следующая строка содержит данные одного студента: + +```text +groupNumber,averageGrade,recordBookNumber +``` + +Пример: + +```text +groupNumber,averageGrade,recordBookNumber +A12,4.5,10001 +B07,3.8,10002 +C25,4.9,10003 +``` + +Для формирования и чтения CSV использовать стандартную библиотеку для работы с форматом CSV. + +--- + +# `StudentCsvWriter` + +## Назначение + +Дозаписывать одного студента в указанный CSV-файл. + +## Контракт + +Конструктор: + +```java +public StudentCsvWriter(String absoluteFilePath) +``` + +Метод: + +```java +public void write(Student student) throws IOException +``` + +### Требования + +1. Метод принимает одного `Student`. +2. Запись выполняется в режиме добавления. +3. Существующие записи не изменяются. +4. Если файл существует, заголовок повторно не записывается. +5. Если файл не существует, необходимо создать файл. +6. При создании нового файла имя должно соответствовать формату: + +```text +students-yyyy-MM-dd-HH-mm.csv +``` + +Например: + +```text +students-2026-08-28-20-45.csv +``` + +1. Новый файл создаётся в директории, указанной абсолютным путём. +2. При создании нового файла сначала записывается заголовок. +3. При успешной записи метод завершается без исключения. +4. Метод не возвращает значение. +5. Исключения, связанные с записью файла, не обрабатывать внутри метода — они должны передаваться вызывающему коду. +6. Исключения `Student` и `StudentBuilder` также не обрабатывать. + +## Невозможность записи + +Если студент не может быть записан, например: + +- отсутствуют права на запись; +- невозможно создать файл; +- файл недоступен; +- произошла ошибка файловой системы; + +метод должен выбросить соответствующее исключение, например `IOException`. + +Запись не должна возвращать специальное значение вроде `0` для обозначения ошибки. + +--- + +# `StudentCsvReader` + +## Назначение + +Последовательно читать студентов из CSV-файла. + +## Контракт + +Класс должен реализовывать: + +```java +Iterator +``` + +Конструктор: + +```java +public StudentCsvReader(String absoluteFilePath) +``` + +Методы: + +```java +@Override +public boolean hasNext() +``` + +```java +@Override +public Student next() +``` + +### Требования к `hasNext()` + +1. Проверяет наличие следующей записи `Student`. +2. Заголовок CSV не считается записью. +3. Повторный вызов `hasNext()` не должен пропускать следующую запись. +4. После окончания файла метод возвращает `false`. + +### Требования к `next()` + +1. Возвращает следующего `Student`. +2. `Student` создаётся через существующий `StudentBuilder`. +3. При отсутствии следующего элемента выбрасывается: + +```text +NoSuchElementException +``` + +1. Весь файл не должен предварительно загружаться в коллекцию. + +## Невозможность прочитать студента + +Если следующую запись невозможно прочитать или преобразовать в `Student`, например: + +- некорректный формат CSV; +- отсутствует обязательное поле; +- значение невозможно преобразовать в нужный тип; +- данные не проходят валидацию `Student`; + +исключение должно передаваться вызывающему коду. + +Исключения `Student` и `StudentBuilder` **не перехватывать и не обрабатывать**. + +Некорректные записи не должны молча пропускаться. + +--- + +# Общие требования + +1. Оба класса принимают абсолютный путь к файлу. +2. Использовать готовую библиотеку для работы с CSV. +3. Не реализовывать собственный CSV-парсер. +4. Не использовать стандартные коллекции Java как основное хранилище студентов. +5. Не дублировать в этих классах валидацию `Student`. +6. Не дублировать логику создания `Student` вне `StudentBuilder`. +7. `StudentCsvWriter` отвечает только за запись. +8. `StudentCsvReader` отвечает только за чтение. + +--- + +# Примеры тестов + +## `StudentCsvWriter` + +### Создание нового файла + +Передать путь к несуществующему файлу и вызвать: + +```java +writer.write(student); +``` + +Проверить: + +- файл создан; +- имя соответствует `students-yyyy-MM-dd-HH-mm.csv`; +- присутствует заголовок; +- студент записан; +- метод завершился без исключения. + +### Дозапись + +Создать файл с одним студентом и выполнить: + +```java +writer.write(student2); +``` + +Проверить, что: + +- первая запись сохранилась; +- `student2` добавлен в конец; +- заголовок присутствует только один раз. + +### Несколько последовательных записей + +```java +writer.write(student1); +writer.write(student2); +writer.write(student3); +``` + +Проверить наличие трёх записей в правильном порядке. + +### Невозможность записи + +Передать путь, по которому невозможно создать или открыть файл для записи. + +Проверить, что `write()` выбрасывает `IOException`. + +Не проверять специальное возвращаемое значение — метод ничего не возвращает. + +--- + +## `StudentCsvReader` + +### Чтение корректного файла + +Для файла: + +```text +groupNumber,averageGrade,recordBookNumber +A12,4.5,10001 +B07,3.8,10002 +C25,4.9,10003 +``` + +Проверить: + +- `hasNext()` возвращает `true` до последнего студента; +- `next()` возвращает студентов в правильном порядке; +- после последнего студента `hasNext()` возвращает `false`. + +### Повторный вызов `hasNext()` + +```java +reader.hasNext(); +reader.hasNext(); +Student student = reader.next(); +``` + +Проверить, что первый студент не был пропущен. + +### `next()` после окончания файла + +После чтения всех студентов вызвать: + +```java +reader.next(); +``` + +Проверить выброс: + +```text +NoSuchElementException +``` + +### Некорректная запись + +Передать файл: + +```text +groupNumber,averageGrade,recordBookNumber +A12,4.5,10001 +INVALID +C25,4.9,10003 +``` + +Проверить, что ошибка при чтении некорректной записи передаётся вызывающему коду. + +### Некорректные данные `Student` + +Передать данные, нарушающие ограничения `Student`. + +Проверить, что исключение от `Student` или `StudentBuilder` выходит наружу и не перехватывается `StudentCsvReader`. + +--- + +# Названия файлов + +```text +StudentCsvWriter.java +StudentCsvReader.java +``` + +# Git + +Название ветки: + +```text +feature/csv-student-io +``` + +Conventional Commit: + +```text +feat: add csv student reader and writer +``` diff --git a/docs/tasks/DataSource.md b/docs/tasks/DataSource.md deleted file mode 100644 index 79a6c14..0000000 --- a/docs/tasks/DataSource.md +++ /dev/null @@ -1,399 +0,0 @@ -# Техническое задание: источники данных - -## Задача - -Реализовать три независимых источника данных для формирования `MyList`: - -- `ManualDataSource`; -- `RandomDataSource`; -- `FileDataSource`. - -Все источники должны формировать только валидные объекты `Student`. - -Класс `Student` содержит: - -```java -private final String groupNumber; -private final double averageGrade; -private final String recordBookNumber; -``` - -Создание объектов `Student` выполнять через `StudentBuilder`. - ---- - -# Общий контракт - -Создать интерфейс `DataSource`: - -```java -public interface DataSource { - - MyList create(); -} -``` - -Источник данных не должен самостоятельно получать данные от пользователя. - -Все необходимые входные значения должны передаваться извне. - -Результатом работы каждого источника является `MyList`. - -Стандартные `List`, `ArrayList` и `LinkedList` для хранения результата не использовать. - ---- - -# `ManualDataSource` - -## Назначение - -Постепенно формировать коллекцию `Student` на основе значений, передаваемых извне для каждого отдельного объекта. - -## Входные данные одного объекта - -Для создания одного `Student` передаются: - -```text -groupNumber -averageGrade -recordBookNumber -``` - -Также передаётся `index` — позиция в коллекции, на которой должен находиться создаваемый объект. - -Размер результирующей коллекции задаётся заранее. - -## Контракт - -`ManualDataSource` должен: - -1. Принимать требуемый размер коллекции. -2. Принимать значения одного `Student`: - - - `groupNumber`; - - `averageGrade`; - - `recordBookNumber`. - -3. Принимать индекс, на котором должен находиться этот объект. -4. Создавать новый `Student` через `StudentBuilder`. -5. Добавлять созданный объект в нужную позицию коллекции. -6. Позволять повторно передавать данные для существующего индекса, заменяя объект на новый. -7. Проверять допустимость индекса. -8. Не изменять уже созданный объект `Student`. -9. Позволять получить текущее состояние коллекции для просмотра. -10. После заполнения вернуть сформированную `MyList`. - -Рекомендуемый метод для передачи одного объекта: - -```java -setStudent( - int index, - String groupNumber, - double averageGrade, - String recordBookNumber -) -``` - -Таким образом, пользовательский код может последовательно передать данные для всех элементов: - -```java -dataSource.setStudent(0, "101", 4.5, "10001"); -dataSource.setStudent(1, "102", 3.8, "10002"); -dataSource.setStudent(2, "101", 4.9, "10003"); -``` - -После заполнения: - -```java -MyList students = dataSource.create(); -``` - -### Изменение объекта - -Для изменения объекта повторно вызывается `setStudent(...)` с нужным индексом: - -```java -dataSource.setStudent(1, "105", 4.2, "10025"); -``` - -Объект на позиции `1` заменяется новым валидным `Student`. - -### Размер коллекции - -Если задан: - -```text -size = 5 -``` - -то для успешного создания коллекции должны быть заданы данные для всех пяти позиций: - -```text -0 -1 -2 -3 -4 -``` - -Порядок заполнения не обязан совпадать с порядком индексов: - -```java -dataSource.setStudent(3, "103", 4.1, "10004"); -dataSource.setStudent(0, "101", 4.5, "10001"); -dataSource.setStudent(4, "102", 3.9, "10005"); -dataSource.setStudent(1, "101", 4.8, "10002"); -dataSource.setStudent(2, "104", 3.7, "10003"); -``` - -После заполнения коллекция должна содержать ровно пять объектов в соответствии с указанными индексами. - -Если хотя бы одна позиция не заполнена, `create()` должен сообщить об ошибке и не возвращать неполную коллекцию. - ---- - -# `RandomDataSource` - -## Назначение - -Создавать заданное количество `Student` со случайными значениями в диапазонах, заданных извне. - -## Входные данные - -Передаются: - -```text -size -``` - -и диапазоны для каждого поля: - -```text -groupNumber: - minimum - maximum - -averageGrade: - minimum - maximum - -recordBookNumber: - minimum - maximum -``` - -## Контракт - -`RandomDataSource` должен: - -1. Принять размер создаваемой коллекции. -2. Принять минимальное и максимальное значение каждого поля. -3. Проверить корректность каждого диапазона. -4. Сгенерировать значения в заданных пределах. -5. Создать каждый объект через `StudentBuilder`. -6. Поместить объекты в `MyList`. -7. Вернуть коллекцию заданного размера. - -Минимальное и максимальное значения включаются в диапазон генерации. - -`RandomDataSource` не должен самостоятельно получать или отображать параметры генерации. - ---- - -# `FileDataSource` - -## Назначение - -Создавать `MyList` на основании содержимого файла. - -## Входные данные - -Передаётся **абсолютный путь к файлу**. - -Относительный путь не является входным параметром `FileDataSource`. - -Формат одной записи: - -```text -groupNumber;averageGrade;recordBookNumber -``` - -Пример: - -```text -101;4.75;123456 -102;3.80;123457 -103;4.20;123458 -``` - -## Контракт - -`FileDataSource` должен: - -1. Получить абсолютный путь к файлу. -2. Открыть файл. -3. Последовательно обработать его записи. -4. Разделить каждую запись на три значения. -5. Создать `Student` через `StudentBuilder`. -6. Добавить только валидные объекты в `MyList`. -7. Пропустить некорректную запись и продолжить обработку остальных. -8. При невозможности открыть файл сообщить об ошибке. -9. Вернуть сформированную `MyList`. - ---- - -# Общие требования - -1. Все источники должны создавать `Student` только через `StudentBuilder`. -2. Источник данных не должен самостоятельно вводить данные. -3. Источник данных не должен зависеть от интерфейса пользователя. -4. Источник данных не должен содержать логику сортировки. -5. Результат хранить только в `MyList`. -6. Невалидный `Student` не должен попасть в коллекцию. -7. Проверка корректности самого объекта выполняется средствами `Student`. -8. Для ручного заполнения изменение существующего `Student` выполнять заменой объекта, а не изменением его полей. - ---- - -# Примеры тестов - -## `ManualDataSource` - -### Заполнение всей коллекции - -Создать источник размера `3`: - -```java -ManualDataSource dataSource = new ManualDataSource(3); - -dataSource.setStudent(0, "101", 4.5, "10001"); -dataSource.setStudent(1, "102", 3.8, "10002"); -dataSource.setStudent(2, "103", 4.2, "10003"); - -MyList result = dataSource.create(); -``` - -Проверить: - -```text -result.size() == 3 -``` - -и соответствие каждого объекта своему индексу. - -### Заполнение в произвольном порядке - -```java -dataSource.setStudent(2, "103", 4.2, "10003"); -dataSource.setStudent(0, "101", 4.5, "10001"); -dataSource.setStudent(1, "102", 3.8, "10002"); -``` - -Проверить правильное расположение объектов. - -### Изменение объекта - -После заполнения: - -```java -dataSource.setStudent(1, "105", 4.9, "10025"); -``` - -Проверить, что изменился только объект с индексом `1`. - -### Незаданный индекс - -Для коллекции размера `3` вызвать: - -```java -dataSource.setStudent(3, ...); -``` - -Проверить выброс исключения. - -### Неполное заполнение - -Заполнить только две позиции из трёх и вызвать `create()`. - -Проверить, что неполная коллекция не возвращается. - ---- - -## `RandomDataSource` - -### Корректная генерация - -Задать размер `100` и допустимые диапазоны для всех трёх полей. - -Проверить: - -```text -result.size() == 100 -``` - -и нахождение каждого значения внутри заданного диапазона. - -### Граничный диапазон - -Задать одинаковые минимальное и максимальное значения. - -Проверить, что все сгенерированные значения соответствуют этому значению. - -### Некорректный диапазон - -Задать: - -```text -minimum > maximum -``` - -Проверить выброс исключения. - ---- - -## `FileDataSource` - -### Корректный файл - -Для файла: - -```text -101;4.75;123456 -102;3.80;123457 -103;4.20;123458 -``` - -проверить получение трёх корректных объектов. - -### Файл с некорректной записью - -```text -101;4.75;123456 -invalid;abc;123457 -103;4.20;123458 -``` - -Проверить, что в коллекцию попадают только две корректные записи. - -### Пустой файл - -Проверить получение пустой коллекции. - -### Несуществующий файл - -Передать путь к несуществующему файлу и проверить корректную обработку ошибки. - ---- - -# Результат - -Реализовать: - -```text -DataSource.java -ManualDataSource.java -RandomDataSource.java -FileDataSource.java -``` - -Допускается создание вспомогательных классов для чтения файла и генерации случайных значений. diff --git a/docs/tasks/DataSourceUpdate.md b/docs/tasks/DataSourceUpdate.md deleted file mode 100644 index c175d9c..0000000 --- a/docs/tasks/DataSourceUpdate.md +++ /dev/null @@ -1,81 +0,0 @@ -# Дополнение к техническому заданию: изменение типа `recordBookNumber` - -Данное дополнение имеет **приоритет над основным ТЗ `DataSource.md`**. - -## Изменение модели `Student` - -Поле: - -```java -private final String recordBookNumber; -``` - -заменено на: - -```java -private final int recordBookNumber; -``` - -Метод: - -```java -getRecordBookNumber() -``` - -возвращает `int`. - -## Требования к `DataSource` - -Во всех реализациях источников данных необходимо учитывать новый тип `recordBookNumber`. - -### `ManualDataSource` - -`recordBookNumber` должен передаваться как `int`: - -```java -setStudent( - int index, - String groupNumber, - double averageGrade, - int recordBookNumber -); -``` - -Строковое значение номера зачётной книжки больше не используется. - -### `RandomDataSource` - -Для `recordBookNumber` необходимо задавать и генерировать **числовой диапазон**: - -```text -минимальное значение — int -максимальное значение — int -``` - -Генерируемое значение должно быть целым числом и соответствовать ограничениям `Student`. - -### `FileDataSource` - -В файле `recordBookNumber` должен записываться как целое число. - -Формат записи: - -```text -groupNumber;averageGrade;recordBookNumber -``` - -Пример: - -```text -A12;4.75;12345 -B32;3.80;12346 -C17;4.20;12347 -``` - -При чтении значение необходимо преобразовать в `int` до создания `Student`. - -## Приоритет - -При противоречии между данным дополнением и основным `DataSource.md` использовать требования этого дополнения. - -Все остальные требования основного технического задания остаются без изменений. From 7c8ff139cf215e57e21d2e4b29deeddad888d671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 21:32:47 +0400 Subject: [PATCH 075/110] docs: add even-only sort strategy task --- docs/tasks/EvenOnlySortStrategy.md | 368 +++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 docs/tasks/EvenOnlySortStrategy.md diff --git a/docs/tasks/EvenOnlySortStrategy.md b/docs/tasks/EvenOnlySortStrategy.md new file mode 100644 index 0000000..306c38c --- /dev/null +++ b/docs/tasks/EvenOnlySortStrategy.md @@ -0,0 +1,368 @@ +# Техническое задание: `EvenOnlySortStrategy` + +## Задача + +Создать класс `EvenOnlySortStrategy`, реализующий дополнительное задание по сортировке элементов с чётным значением выбранного числового поля. + +Класс должен использовать **паттерн Стратегия**: он не реализует собственный алгоритм сортировки, а делегирует сортировку переданной реализации `SortStrategy`. + +## Контракт + +Класс: + +```java +public class EvenOnlySortStrategy implements SortStrategy +``` + +Должен содержать два конструктора: + +```java +public EvenOnlySortStrategy() +``` + +```java +public EvenOnlySortStrategy(SortStrategy sortStrategy) +``` + +Также реализовать метод: + +```java +public void setSortStrategy(SortStrategy sortStrategy) +``` + +и основной метод интерфейса `SortStrategy`: + +```java +@Override +public void sort( + MyList list, + Comparator comparator +) +``` + +Для определения чётности числового поля использовать: + +```java +ToIntFunction +``` + +Поэтому рекомендуется также передавать функцию получения числового значения через конструктор или отдельный метод настройки. + +Для `Student` в качестве функции использовать: + +```java +Student::getRecordBookNumber +``` + +## Поведение при отсутствии стратегии + +Если `sortStrategy` не задан: + +```java +EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(); +``` + +то вызов: + +```java +strategy.sort(students, comparator); +``` + +должен завершаться контролируемой ошибкой. + +Рекомендуется выбрасывать: + +```java +IllegalStateException +``` + +с понятным сообщением о том, что стратегия сортировки не задана. + +Не следует использовать `null` как признак успешного выполнения сортировки. + +## Поведение `setSortStrategy` + +Метод: + +```java +setSortStrategy(SortStrategy sortStrategy) +``` + +заменяет текущую стратегию. + +После вызова метода последующая сортировка должна выполняться именно переданной стратегией. + +Передача `null` должна приводить к контролируемой ошибке, например: + +```java +IllegalArgumentException +``` + +## Основная бизнес-логика + +`EvenOnlySortStrategy` **не должен содержать код какого-либо алгоритма сортировки**. + +Запрещено реализовывать внутри класса: + +- Merge Sort; +- Bubble Sort; +- Selection Sort; +- другие алгоритмы сортировки; +- вызовы стандартных методов сортировки Java. + +Класс отвечает только за: + +1. определение элементов, участвующих в специальной сортировке; +2. сохранение исходных позиций нечётных элементов; +3. передачу подходящих элементов переданной `SortStrategy`; +4. восстановление результата на исходные позиции; +5. делегирование самого алгоритма сортировки. + +Таким образом: + +```text +EvenOnlySortStrategy + │ + ├── определяет, какие элементы сортировать + │ + ├── передаёт сортируемые элементы + │ ↓ + │ SortStrategy + │ ↓ + │ MergeSortStrategy + │ QuickSortStrategy + │ ... + │ + └── возвращает результат на исходные позиции +``` + +Один `EvenOnlySortStrategy` должен работать с **любой реализацией `SortStrategy`**. + +## Рекомендуемый алгоритм + +Рекомендуется использовать следующий подход. + +Исходная коллекция: + +```text +[7, 8, 3, 2, 5, 4] +``` + +При выборе `recordBookNumber`: + +```text +нечётные: 7, 3, 5 +чётные: 8, 2, 4 +``` + +1. Пройти исходный `MyList`. +2. С помощью `ToIntFunction` определить чётность каждого элемента. +3. Запомнить позиции элементов с чётным значением. +4. Сформировать отдельный `MyList` только из чётных элементов. +5. Передать эту коллекцию в `sortStrategy.sort(...)`. +6. Полученные отсортированные элементы последовательно вернуть на сохранённые позиции. +7. Элементы с нечётными значениями не изменять. + +Например, переданная стратегия `MergeSortStrategy` сортирует: + +```text +[8, 2, 4] +``` + +в: + +```text +[2, 4, 8] +``` + +После возвращения элементов на исходные позиции: + +```text +[7, 2, 3, 4, 5, 8] +``` + +При этом `EvenOnlySortStrategy` не знает, каким способом были отсортированы `8`, `2`, `4`. + +## Работа с любой `SortStrategy` + +Должно быть возможно использовать: + +```java +EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>( + new MergeSortStrategy<>(), + Student::getRecordBookNumber + ); +``` + +А также после создания заменить стратегию: + +```java +strategy.setSortStrategy(new AnotherSortStrategy<>()); +``` + +После этого специальная сортировка должна использовать новую стратегию без изменения `EvenOnlySortStrategy`. + +## Требования к `ToIntFunction` + +Функция получения числового значения должна передаваться извне. + +Для `Student`: + +```java +Student::getRecordBookNumber +``` + +Стратегия не должна напрямую обращаться к: + +```java +student.getRecordBookNumber() +``` + +или к другим полям `Student`. + +Это позволяет использовать класс с другими типами объектов и другими числовыми полями. + +## Требования к коллекциям + +Работать необходимо с: + +```java +MyList +``` + +Использование `ArrayList`, `LinkedList` и других стандартных коллекций для хранения элементов запрещено. + +Вспомогательная коллекция чётных элементов также должна быть `MyList`. + +## Тесты + +### Переданная стратегия используется + +Передать `MergeSortStrategy`. + +Проверить, что сортировка чётных элементов выполняется переданной стратегией. + +### Замена стратегии + +1. Создать `EvenOnlySortStrategy` с одной реализацией `SortStrategy`. +2. Выполнить сортировку. +3. Вызвать `setSortStrategy(...)`. +4. Выполнить сортировку повторно. +5. Проверить, что используется новая стратегия. + +### Конструктор без стратегии + +Создать: + +```java +new EvenOnlySortStrategy<>(); +``` + +Вызвать `sort()`. + +Проверить выброс `IllegalStateException`. + +### Конструктор со стратегией + +Создать объект с `MergeSortStrategy`. + +Проверить успешное выполнение сортировки. + +### Передача `null` в `setSortStrategy` + +Проверить выброс `IllegalArgumentException`. + +### Все элементы нечётные + +```text +[7, 3, 5, 1] +``` + +Результат должен полностью совпадать с исходной коллекцией. + +### Все элементы чётные + +```text +[8, 2, 6, 4] +``` + +Ожидаемый результат: + +```text +[2, 4, 6, 8] +``` + +### Смешанные значения + +```text +[7, 8, 3, 2, 5, 4] +``` + +Ожидаемый результат: + +```text +[7, 2, 3, 4, 5, 8] +``` + +### Повторяющиеся значения + +Проверить корректность сортировки при наличии одинаковых чётных значений. + +### Один элемент + +Проверить отдельно чётный и нечётный элемент. + +### Пустая коллекция + +Сортировка не должна приводить к ошибке. + +### Проверка сохранения нечётных элементов + +Проверить не только значения, но и то, что объекты с нечётным `recordBookNumber` остались на тех же индексах. + +### Проверка разных реализаций + +Использовать `EvenOnlySortStrategy` с каждой доступной реализацией `SortStrategy`. + +Проверить, что результат специальной сортировки соответствует контракту независимо от конкретного алгоритма. + +### Проверка делегирования + +Создать тестовую реализацию `SortStrategy`, которая фиксирует факт вызова `sort()`. + +Передать её в `EvenOnlySortStrategy`. + +Проверить, что: + +- `sort()` был вызван; +- в стратегию передана коллекция только с чётными элементами; +- сама `EvenOnlySortStrategy` не выполняет сортировку самостоятельно. + +## Результат + +Реализовать: + +```text +EvenOnlySortStrategy.java +``` + +Класс должен зависеть от абстракции: + +```text +SortStrategy +MyList +Comparator +ToIntFunction +``` + +и **не должен содержать реализации конкретного алгоритма сортировки**. + +## Git + +Ветка: + +```text +feature/even-only-sort-strategy +``` From 81afad5ecd8779fbfcd9743b1a08498d65509606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 22:28:12 +0400 Subject: [PATCH 076/110] docs: add composite student search task --- docs/tasks/CompositeStudentSearch.md | 455 +++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 docs/tasks/CompositeStudentSearch.md diff --git a/docs/tasks/CompositeStudentSearch.md b/docs/tasks/CompositeStudentSearch.md new file mode 100644 index 0000000..8ab2a72 --- /dev/null +++ b/docs/tasks/CompositeStudentSearch.md @@ -0,0 +1,455 @@ +# Техническое задание: поиск студентов с использованием паттерна Компоновщик + +## Задача + +Реализовать механизм поиска студентов по диапазону значений с использованием паттерна **Компоновщик**. + +Должны поддерживаться следующие критерии поиска: + +- диапазон `groupNumber`; +- диапазон `averageGrade`; +- диапазон `recordBookNumber`. + +Критерии должны поддерживать объединение в составной критерий. + +Режим объединения критериев — **объединение (ИЛИ)** или **пересечение (И)** — задаётся в конструкторе основного класса. + +--- + +# Названия классов и интерфейсов + +## `StudentSearchCriteria` + +Интерфейс отдельного критерия поиска. + +### Контракт + +```java +boolean matches(Student student); +``` + +Метод возвращает `true`, если студент соответствует критерию. + +--- + +## `GroupNumberRangeCriteria` + +Критерий поиска по диапазону `groupNumber`. + +### Конструктор + +```java +GroupNumberRangeCriteria(String min, String max); +``` + +### Метод + +```java +boolean matches(Student student); +``` + +`groupNumber` имеет тип `String`, поэтому сравнение выполнять лексикографически. + +Границы диапазона включаются. + +--- + +## `AverageGradeRangeCriteria` + +Критерий поиска по диапазону `averageGrade`. + +### Конструктор + +```java +AverageGradeRangeCriteria(double min, double max); +``` + +### Метод + +```java +boolean matches(Student student); +``` + +Студент соответствует критерию, если: + +```text +min <= averageGrade <= max +``` + +--- + +## `RecordBookNumberRangeCriteria` + +Критерий поиска по диапазону `recordBookNumber`. + +### Конструктор + +```java +RecordBookNumberRangeCriteria(int min, int max); +``` + +### Метод + +```java +boolean matches(Student student); +``` + +Студент соответствует критерию, если: + +```text +min <= recordBookNumber <= max +``` + +--- + +# `CompositeStudentSearchCriteria` + +Компоновщик критериев поиска. + +Класс реализует: + +```java +StudentSearchCriteria +``` + +и содержит другие реализации `StudentSearchCriteria`. + +### Методы + +```java +void add(StudentSearchCriteria criteria); +``` + +```java +void remove(StudentSearchCriteria criteria); +``` + +```java +boolean matches(Student student); +``` + +### Логика + +`CompositeStudentSearchCriteria` должен поддерживать два режима: + +```text +UNION — объединение условий, ИЛИ +INTERSECTION — пересечение условий, И +``` + +Режим должен задаваться при создании компоновщика: + +```java +CompositeStudentSearchCriteria( + SearchOperation operation +); +``` + +где `SearchOperation` — перечисление: + +```java +public enum SearchOperation { + UNION, + INTERSECTION +} +``` + +Для `UNION` студент подходит, если выполняется **хотя бы один** вложенный критерий. + +Для `INTERSECTION` студент подходит, если выполняются **все** вложенные критерии. + +--- + +# `StudentSearchService` + +Основной класс поиска. + +Он должен принимать интерфейс `StudentSearchCriteria`, а не конкретную реализацию критерия. + +### Конструктор + +```java +StudentSearchService(StudentSearchCriteria criteria); +``` + +### Метод + +```java +MyList find(MyList students); +``` + +### Поведение + +Метод `find()` должен: + +1. Последовательно проверить студентов исходной коллекции. +2. Передать каждого студента в: + +```java +criteria.matches(student) +``` + +1. Добавить подходящих студентов в новую `MyList`. +2. Вернуть новую коллекцию. + +`StudentSearchService` не должен содержать логику конкретных диапазонов или объединения критериев. + +### Обработка отсутствующего критерия + +Если `StudentSearchService` создан без критерия: + +```java +new StudentSearchService(null); +``` + +необходимо выбросить: + +```java +IllegalArgumentException +``` + +Класс не должен выполнять поиск без заданного критерия. + +--- + +# Пример использования + +### Один критерий + +```java +StudentSearchCriteria criteria = + new AverageGradeRangeCriteria(4.0, 5.0); + +StudentSearchService service = + new StudentSearchService(criteria); + +MyList result = + service.find(students); +``` + +### Пересечение критериев + +Например, студент должен одновременно: + +- находиться в группе от `A10` до `C30`; +- иметь средний балл от `4.0` до `5.0`. + +```java +CompositeStudentSearchCriteria criteria = + new CompositeStudentSearchCriteria( + SearchOperation.INTERSECTION + ); + +criteria.add( + new GroupNumberRangeCriteria("A10", "C30") +); + +criteria.add( + new AverageGradeRangeCriteria(4.0, 5.0) +); + +StudentSearchService service = + new StudentSearchService(criteria); +``` + +### Объединение критериев + +Студент должен соответствовать хотя бы одному условию: + +```java +CompositeStudentSearchCriteria criteria = + new CompositeStudentSearchCriteria( + SearchOperation.UNION + ); + +criteria.add( + new AverageGradeRangeCriteria(4.5, 5.0) +); + +criteria.add( + new RecordBookNumberRangeCriteria(10000, 20000) +); + +StudentSearchService service = + new StudentSearchService(criteria); +``` + +--- + +# Общие требования + +1. `StudentSearchService` должен работать только через `StudentSearchCriteria`. +2. `StudentSearchService` не должен знать о конкретных критериях. +3. `CompositeStudentSearchCriteria` должен реализовывать тот же интерфейс `StudentSearchCriteria`. +4. Должна поддерживаться вложенность компоновщиков. + +Например: + +```text +Composite + ├── GroupNumberRangeCriteria + └── Composite + ├── AverageGradeRangeCriteria + └── RecordBookNumberRangeCriteria +``` + +1. Исходную `MyList` не изменять. +2. Результат возвращать в виде новой `MyList`. +3. Порядок найденных студентов должен сохраняться. +4. Стандартные `List`, `ArrayList`, `LinkedList` не использовать. +5. Готовые методы поиска не использовать. +6. При отсутствии совпадений возвращать пустую `MyList`. +7. Для диапазонов `min > max` выбрасывать `IllegalArgumentException`. + +--- + +# Примеры тестов + +## `GroupNumberRangeCriteria` + +Проверить: + +- значение внутри диапазона; +- значение равное нижней границе; +- значение равное верхней границе; +- значение вне диапазона. + +## `AverageGradeRangeCriteria` + +Проверить: + +```text +3.9 → false +4.0 → true +4.5 → true +5.0 → true +5.1 → false +``` + +## `RecordBookNumberRangeCriteria` + +Проверить значения внутри и вне диапазона, включая обе границы. + +## Некорректный диапазон + +Для каждого критерия проверить: + +```text +min > max +``` + +Ожидается: + +```java +IllegalArgumentException +``` + +## `INTERSECTION` + +Создать два критерия и проверить: + +```text +критерий A = true +критерий B = true +→ true +``` + +```text +критерий A = true +критерий B = false +→ false +``` + +## `UNION` + +Проверить: + +```text +A = true +B = false +→ true +``` + +```text +A = false +B = true +→ true +``` + +```text +A = false +B = false +→ false +``` + +## Компоновщик без критериев + +Проверить поведение: + +```java +new CompositeStudentSearchCriteria( + SearchOperation.INTERSECTION +); +``` + +и: + +```java +new CompositeStudentSearchCriteria( + SearchOperation.UNION +); +``` + +Поведение должно быть явно определено и закреплено тестами. + +Рекомендуется: + +- пустое `INTERSECTION` → `true`; +- пустое `UNION` → `false`. + +## `StudentSearchService` + +Проверить: + +- корректный поиск по одному критерию; +- пустой результат; +- пустую исходную коллекцию; +- сохранение порядка; +- неизменность исходной коллекции; +- работу с `CompositeStudentSearchCriteria`. + +## Проверка вложенного Компоновщика + +Создать компоновщик с вложенным компоновщиком и проверить корректность вычисления результата. + +## Проверка отсутствующего критерия + +Создать: + +```java +new StudentSearchService(null); +``` + +Проверить `IllegalArgumentException`. + +## Результат + +Реализовать: + +```text +StudentSearchCriteria.java +GroupNumberRangeCriteria.java +AverageGradeRangeCriteria.java +RecordBookNumberRangeCriteria.java +CompositeStudentSearchCriteria.java +SearchOperation.java +StudentSearchService.java +``` + +## Git + +Название ветки: + +```text +feature/composite-student-search +``` From b6354bf133d6acbfebb75a964d8037331f04bb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Fri, 28 Aug 2026 23:11:28 +0400 Subject: [PATCH 077/110] docs: add student occurrence index finder task --- docs/tasks/StudentOccurrenceIndexFinder.md | 688 +++++++++++++++++++++ 1 file changed, 688 insertions(+) create mode 100644 docs/tasks/StudentOccurrenceIndexFinder.md diff --git a/docs/tasks/StudentOccurrenceIndexFinder.md b/docs/tasks/StudentOccurrenceIndexFinder.md new file mode 100644 index 0000000..0c6f202 --- /dev/null +++ b/docs/tasks/StudentOccurrenceIndexFinder.md @@ -0,0 +1,688 @@ +# Техническое задание: многопоточный поиск индексов `Student` + +## Задача + +Реализовать многопоточный поиск всех вхождений заданного объекта `Student` в `MyList`. + +Метод **не должен выводить никаких сообщений в консоль**. + +Результатом работы должен быть массив `int[]`, содержащий индексы всех элементов исходной коллекции, которые равны переданному `Student`. + +Сравнение выполнять через существующий `Student.equals()`. + +--- + +## Входная информация + +Основной метод принимает: + +```java +MyList students +``` + +Исходная коллекция студентов. + +```java +Student target +``` + +Студент, вхождения которого необходимо найти. + +--- + +## Выходная информация + +Метод возвращает: + +```java +int[] +``` + +Массив должен содержать индексы всех совпавших элементов. + +Пример: + +```text +Исходная коллекция: + +[Student A, Student B, Student A, Student C, Student A] + +Результат: + +[0, 2, 4] +``` + +Индексы должны быть расположены в порядке возрастания. + +При отсутствии совпадений: + +```text +[] +``` + +При пустой коллекции: + +```text +[] +``` + +--- + +# Название класса + +```text +StudentOccurrenceIndexFinder +``` + +--- + +# Публичные методы и контракты + +## `findOccurrences` + +```java +public int[] findOccurrences( + MyList students, + Student target +); +``` + +Метод должен: + +1. Проверить входные параметры. +2. Разделить коллекцию на диапазоны индексов. +3. Передать каждый диапазон отдельной задаче. +4. Выполнить поиск в нескольких потоках. +5. Получить от каждого потока найденные индексы. +6. Объединить результаты. +7. Вернуть индексы в порядке возрастания. +8. Не изменять исходную коллекцию. +9. Не изменять объекты `Student`. +10. Не выводить ничего в консоль. + +--- + +# Вспомогательные методы + +Количество и вид `private` методов могут быть выбраны разработчиком. + +Рекомендуется выделить: + +```java +private int calculateThreadCount(int size); +``` + +Определяет количество рабочих потоков. + +```java +private int countChunkSize(int size, int threadCount); +``` + +Определяет размер диапазона для одной задачи. + +```java +private MyList findInRange( + MyList students, + Student target, + int fromIndex, + int toIndex +); +``` + +Обрабатывает диапазон: + +```text +[fromIndex, toIndex) +``` + +и возвращает индексы найденных совпадений. + +--- + +# Работа с многопоточностью + +## Рекомендуемый подход + +Не использовать общий контейнер результатов, к которому одновременно обращаются несколько потоков. + +Также **не рекомендуется использовать мьютексы** (`synchronized`, `Lock`, `ReentrantLock`) для хранения результата. + +Лучше использовать модель: + +```text + MyList + │ + ┌────────────┼────────────┐ + ↓ ↓ ↓ + Поток 1 Поток 2 Поток 3 + диапазон диапазон диапазон + │ │ │ + ↓ ↓ ↓ + MyList MyList MyList + └────────────┼────────────┘ + ↓ + основной поток + ↓ + merge + ↓ + int[] +``` + +Каждый поток создаёт **собственный контейнер `MyList`** и записывает найденные индексы только в него. + +Это позволяет избежать состояния гонки без синхронизации общей структуры. + +--- + +# Разбиение коллекции на потоки + +Количество потоков рекомендуется определять на основании: + +```java +Runtime.getRuntime().availableProcessors() +``` + +Количество потоков не должно превышать количество элементов: + +```text +threadCount = min( + availableProcessors(), + students.size() +) +``` + +### Пример + +Для коллекции из `10000` элементов и четырёх рабочих потоков: + +```text +Поток 1 → [0, 2500) +Поток 2 → [2500, 5000) +Поток 3 → [5000, 7500) +Поток 4 → [7500, 10000) +``` + +Каждый индекс должен принадлежать **ровно одному диапазону**. + +Не допускаются: + +- пропущенные индексы; +- обработка одного индекса несколькими потоками. + +--- + +# Поиск внутри потока + +Каждая задача обрабатывает только свой диапазон. + +Для каждого индекса: + +```java +Student student = students.get(index); +``` + +затем: + +```java +target.equals(student) +``` + +Если результат `true`, индекс добавляется в локальный: + +```java +MyList +``` + +Пример: + +```text +Поток 1 → [0, 8, 15] +Поток 2 → [21, 24] +Поток 3 → [30, 42, 48] +Поток 4 → [63] +``` + +--- + +# Объединение результатов + +Основной поток после завершения всех задач должен получить локальные результаты и объединить их. + +Рекомендуется передавать задачи в порядке возрастания диапазонов: + +```text +Результат 1 → диапазон 0 +Результат 2 → диапазон 1 +Результат 3 → диапазон 2 +... +``` + +Если каждая задача добавляет индексы в возрастающем порядке, а результаты объединяются в порядке диапазонов, итоговый массив уже будет отсортирован: + +```text +[0, 8, 15, 21, 24, 30, 42, 48, 63] +``` + +В этом случае дополнительная сортировка результата не требуется. + +**`Arrays.sort()` и другие готовые методы сортировки использовать не следует**, поскольку порядок можно гарантировать самим алгоритмом объединения. + +--- + +# Потокобезопасность + +## Мьютексы + +Использование мьютексов для результата **не требуется**. + +Не рекомендуется использовать: + +```java +synchronized +``` + +```java +Lock +``` + +```java +ReentrantLock +``` + +```java +AtomicInteger +``` + +для общего результата. + +Причина: потоки не должны изменять одну общую структуру. + +Каждый поток работает со своим: + +```text +MyList +``` + +а объединение выполняется после завершения параллельной части одним потоком. + +--- + +# Рекомендуемые классы и импорты + +Для реализации рекомендуется использовать стандартные средства Java: + +```java +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +``` + +Рекомендуемая схема: + +```text +ExecutorService + ↓ +Callable> + ↓ +Future> +``` + +Каждый `Callable` обрабатывает свой диапазон и возвращает собственный `MyList`. + +После получения всех `Future` основной метод объединяет результаты. + +После завершения работы `ExecutorService` должен быть корректно закрыт. + +При `InterruptedException` необходимо восстановить статус прерывания текущего потока: + +```java +Thread.currentThread().interrupt(); +``` + +--- + +# Использование `MyList` + +Сторонние контейнеры **не допускаются**. + +Запрещено использовать: + +```text +ArrayList +LinkedList +Vector +CopyOnWriteArrayList +ConcurrentLinkedQueue +и другие стандартные контейнеры +``` + +как хранилище промежуточных или итоговых результатов. + +Для результатов каждого потока использовать: + +```java +MyList +``` + +Итог преобразовать в: + +```java +int[] +``` + +--- + +# Требуется ли изменять `MyList` + +Текущего интерфейса **достаточно для реализации задачи**: + +```java +public interface MyList { + + void add(T element); + + T get(int index); + + T set(int index, T element); + + T remove(int index); + + int size(); + + boolean isEmpty(); +} +``` + +Дополнительные методы добавлять **не требуется**. + +Для задачи достаточно: + +```java +size() +get(index) +add(index) +``` + +Поскольку поиск выполняется только на чтение, а результаты каждого потока добавляются в его собственный `MyList`, существующего интерфейса достаточно. + +### Важно + +Метод: + +```java +void add(T element); +``` + +должен добавлять элемент в конец коллекции. + +Это позволяет каждому потоку формировать свои индексы в порядке их обнаружения. + +--- + +# Использование `Student` + +Изменения в `Student` **не требуются**. + +Использовать существующий метод: + +```java +equals(Object obj) +``` + +Сравнение выполнять: + +```java +target.equals(students.get(index)) +``` + +Не использовать: + +```text +== +``` + +Не сравнивать объекты вручную по отдельным полям. + +Не использовать `toString()` для определения совпадения. + +--- + +# `StudentBuilder` + +Изменения в `StudentBuilder` **не требуются**. + +Он не участвует в алгоритме поиска. + +--- + +# Обработка входных данных + +Если: + +```java +students == null +``` + +выбросить: + +```java +IllegalArgumentException +``` + +Если: + +```java +target == null +``` + +выбросить: + +```java +IllegalArgumentException +``` + +Для пустой коллекции вернуть: + +```java +new int[0] +``` + +Без создания рабочих потоков. + +--- + +# Обобщённый алгоритм реализации + +```text +1. Проверить students и target. +2. Получить размер коллекции. +3. Если размер равен 0 → вернуть []. +4. Вычислить количество потоков. +5. Разделить индексы коллекции на непересекающиеся диапазоны. +6. Создать Callable для каждого диапазона. +7. Запустить задачи через ExecutorService. +8. Каждый поток: + - получает fromIndex и toIndex; + - проходит свой диапазон; + - сравнивает Student через equals(); + - записывает найденные индексы в собственный MyList. +9. Основной поток получает результаты через Future. +10. Объединяет MyList в порядке диапазонов. +11. Преобразует результат в int[]. +12. Возвращает массив. +``` + +--- + +# Примеры тестов + +## Одно совпадение + +```text +[A, B, C, D] +``` + +Искомый: + +```text +C +``` + +Ожидается: + +```text +[2] +``` + +## Несколько совпадений + +```text +[A, B, A, C, A] +``` + +Ожидается: + +```text +[0, 2, 4] +``` + +## Нет совпадений + +```text +[A, B, C] +``` + +Искомый: + +```text +D +``` + +Ожидается: + +```text +[] +``` + +## Все элементы совпадают + +```text +[A, A, A, A] +``` + +Ожидается: + +```text +[0, 1, 2, 3] +``` + +## Пустая коллекция + +Ожидается: + +```text +[] +``` + +## Один элемент + +Проверить: + +```text +[A] + A → [0] +[A] + B → [] +``` + +## Проверка границ диапазонов потоков + +Проверить коллекции размеров: + +```text +1 +2 +3 +4 +10 +100 +1000 +``` + +Особое внимание уделить совпадениям: + +```text +на последнем элементе одного диапазона; +на первом элементе следующего диапазона. +``` + +Каждый индекс должен быть найден ровно один раз. + +## Проверка порядка результата + +Создать совпадения в разных диапазонах. + +Проверить, что результат имеет вид: + +```text +[0, 3, 7, 12, 25, 49, 100] +``` + +и находится в порядке возрастания без использования дополнительной сортировки. + +## Проверка `Student.equals()` + +Создать два разных экземпляра `Student` с одинаковыми значениями полей. + +Один поместить в коллекцию, второй передать как `target`. + +Проверить, что индекс считается совпадением согласно `equals()`. + +## Проверка неизменности коллекции + +После выполнения метода проверить: + +- размер `MyList`; +- порядок элементов; +- значения всех элементов. + +## Проверка отсутствия вывода + +Перехватить стандартный вывод и убедиться, что выполнение `findOccurrences()` не выводит сообщений в консоль. + +## Большая коллекция + +Создать коллекцию, например из `100000` студентов, с заранее известными индексами совпадений. + +Проверить: + +- все ожидаемые индексы найдены; +- лишних индексов нет; +- индексы идут по возрастанию; +- итоговый массив имеет правильный размер. + +## Git + +Название ветки: + +```text +feature/student-occurrence-index-finder +``` + +## Результат + +Необходимо реализовать: + +```text +StudentOccurrenceIndexFinder.java +``` + +Существующие интерфейсы и классы: + +```text +MyList.java +Student.java +StudentBuilder.java +``` + +изменять не требуется. From 62ef9540bfdba4eba2adb5656722173fbe927a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Fri, 28 Aug 2026 22:54:04 +0300 Subject: [PATCH 078/110] refactor(sorting): reuse merge sort strategy --- .../sorting/EvenOnlyMergeSortStrategy.java | 82 ++----------------- 1 file changed, 7 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java index a784d7a..45f5b78 100644 --- a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java +++ b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java @@ -1,5 +1,6 @@ package ru.project.sorting; +import ru.project.collection.MyLinkedList; import ru.project.collection.MyList; import java.util.Comparator; @@ -7,9 +8,11 @@ import java.util.function.ToIntFunction; public class EvenOnlyMergeSortStrategy implements SortStrategy { private final ToIntFunction valueExtractor; + private final SortStrategy mergeSortStrategy; public EvenOnlyMergeSortStrategy(ToIntFunction valueExtractor) { this.valueExtractor = valueExtractor; + this.mergeSortStrategy = new MergeSortStrategy<>(); } @Override @@ -32,8 +35,7 @@ public class EvenOnlyMergeSortStrategy implements SortStrategy { return; } - @SuppressWarnings("unchecked") - T[] evenElements = (T[]) new Object[evenCount]; + MyList evenElements = new MyLinkedList<>(); int[] evenIndexes = new int[evenCount]; @@ -43,85 +45,15 @@ public class EvenOnlyMergeSortStrategy implements SortStrategy { T element = list.get(i); if (valueExtractor.applyAsInt(element) % 2 == 0) { - evenElements[evenIndex] = element; + evenElements.add(element); evenIndexes[evenIndex] = i; evenIndex++; } } - mergeSort(evenElements, 0, evenElements.length - 1, comparator); + mergeSortStrategy.sort(evenElements, comparator); for (int i = 0; i < evenIndexes.length; i++) { - list.set(evenIndexes[i], evenElements[i]); - } - } - - private void mergeSort( - T[] elements, - int left, - int right, - Comparator comparator - ) { - if (left >= right) { - return; - } - - int mid = (left + right) / 2; - - mergeSort(elements, left, mid, comparator); - mergeSort(elements, mid + 1, right, comparator); - - merge(elements, left, mid, right, comparator); - } - private void merge( - T[] elements, - 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] = elements[left + i]; - } - - for (int j = 0; j < rightSize; j++) { - rightArray[j] = elements[mid + 1 + j]; - } - - int i = 0; - int j = 0; - int k = left; - - while (i < leftSize && j < rightSize) { - if (comparator.compare(leftArray[i], rightArray[j]) <= 0) { - elements[k] = leftArray[i]; - i++; - } else { - elements[k] = rightArray[j]; - j++; - } - - k++; - } - - while (i < leftSize) { - elements[k] = leftArray[i]; - i++; - k++; - } - - while (j < rightSize) { - elements[k] = rightArray[j]; - j++; - k++; + list.set(evenIndexes[i], evenElements.get(i)); } } } From 61280132f1f7948825742564c97b54f091d5fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Fri, 28 Aug 2026 22:55:25 +0300 Subject: [PATCH 079/110] test(sorting): add student record book case --- .../EvenOnlyMergeSortStrategyTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java b/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java index b423cba..ffaef36 100644 --- a/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java +++ b/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java @@ -3,6 +3,7 @@ package ru.project.sorting; import org.junit.jupiter.api.Test; import ru.project.collection.MyLinkedList; import ru.project.collection.MyList; +import ru.project.student.Student; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -144,6 +145,33 @@ class EvenOnlyMergeSortStrategyTest { assertEquals(5, list.get(4)); assertEquals(2, list.get(5)); } + + @Test + void shouldUseStudentRecordBookNumberToDetermineEvenElements() { + 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)); + + SortStrategy strategy = new EvenOnlyMergeSortStrategy<>( + 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()); + } } From a8e9855ba64995a6e4301d4b4238f2dc21380a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 11:30:48 +0400 Subject: [PATCH 080/110] docs: remove obsolete EvenOnlyMergeSort task specification --- docs/tasks/EvenOnlyMergeSort.md | 209 -------------------------------- 1 file changed, 209 deletions(-) delete mode 100644 docs/tasks/EvenOnlyMergeSort.md diff --git a/docs/tasks/EvenOnlyMergeSort.md b/docs/tasks/EvenOnlyMergeSort.md deleted file mode 100644 index ad5c4d3..0000000 --- a/docs/tasks/EvenOnlyMergeSort.md +++ /dev/null @@ -1,209 +0,0 @@ -# Техническое задание: сортировка по чётным значениям - -## Задача - -Дополнить существующую сортировку слиянием специальной стратегией `EvenOnlyMergeSortStrategy`. - -Стратегия должна сортировать только объекты, у которых выбранное числовое поле имеет чётное значение. Объекты с нечётным значением должны сохранять свои исходные позиции. - -## Контракты - -1. Стратегия должна реализовывать существующий интерфейс: - -```java -SortStrategy -``` - -1. Метод сортировки должен работать с: - -```java -MyList -``` - -1. Для сортировки чётных элементов использовать алгоритм **Merge Sort**. - -2. Готовые методы сортировки Java использовать запрещено. - -3. Стратегия не должна зависеть непосредственно от класса `Student`. - -4. Числовое поле передавать в стратегию извне через: - -```java -ToIntFunction -``` - -1. Сохранить переданный `ToIntFunction` во внутреннем поле стратегии. - -2. Для определения чётности элемента использовать: - -```java -valueExtractor.applyAsInt(element) -``` - -1. `ToIntFunction` используется только для получения числового значения выбранного поля. Стратегия не должна знать, какое именно поле объекта используется. - -2. Для `Student` в качестве числового поля используется: - -```java -Student::getRecordBookNumber -``` - - 1. `getRecordBookNumber()` должен возвращать `int`. - -## Правила алгоритма - -Стратегия должна: - -1. Пройти по исходной `MyList`. -2. Определить элементы с чётным значением, используя `ToIntFunction`. -3. Запомнить позиции этих элементов. -4. Отдельно отсортировать только чётные элементы с помощью **Merge Sort** и переданного `Comparator`. -5. Вернуть отсортированные чётные элементы на позиции, которые занимали чётные элементы до сортировки. -6. Не изменять позиции элементов с нечётным значением. -7. Не изменять размер коллекции. - -### Пример - -Исходные значения выбранного числового поля: - -```text -[7, 8, 3, 2, 5, 4] -``` - -Чётные элементы: - -```text -8, 2, 4 -``` - -После Merge Sort: - -```text -2, 4, 8 -``` - -Результат: - -```text -[7, 2, 3, 4, 5, 8] -``` - -Нечётные элементы `7`, `3`, `5` остались на исходных позициях. - -## Пример использования - -```java -SortStrategy strategy = - new EvenOnlyMergeSortStrategy<>( - Student::getRecordBookNumber - ); - -strategy.sort( - students, - new StudentRecordBookComparator() -); -``` - -Стратегия получает функцию выбора числового поля извне и не содержит логики, специфичной для `Student`. - -## Примеры тестов - -### Обычный случай - -Создать студентов с `recordBookNumber`: - -```text -7, 8, 3, 2, 5, 4 -``` - -После сортировки ожидается: - -```text -7, 2, 3, 4, 5, 8 -``` - -Проверить: - -- нечётные элементы сохранили свои позиции; -- чётные элементы отсортированы по возрастанию; -- количество элементов не изменилось. - -### Все значения нечётные - -```text -1, 3, 5, 7 -``` - -Ожидаемый результат: - -```text -1, 3, 5, 7 -``` - -### Все значения чётные - -```text -8, 2, 6, 4 -``` - -Ожидаемый результат: - -```text -2, 4, 6, 8 -``` - -### Один элемент - -```text -4 -``` - -Ожидаемый результат: - -```text -4 -``` - -### Пустая коллекция - -Пустая коллекция должна остаться пустой и не приводить к ошибке. - -### Проверка `ToIntFunction` - -Передать: - -```java -Student::getRecordBookNumber -``` - -и проверить, что именно `recordBookNumber` используется для определения чётности. - -## Ограничения - -Запрещено: - -- использовать `Arrays.sort()`, `Collections.sort()`, `List.sort()` и другие готовые алгоритмы сортировки; -- перемещать элементы с нечётным значением; -- использовать отдельный алгоритм сортировки вместо Merge Sort; -- напрямую обращаться к полям `Student` внутри `EvenOnlyMergeSortStrategy`; -- выбирать поле через `if` или `switch`; -- использовать стандартные коллекции Java в качестве основной коллекции. - -## Результат - -Реализовать: - -```text -EvenOnlyMergeSortStrategy.java -``` - -Класс должен использовать: - -```text -SortStrategy.java -MyList.java -ToIntFunction -Comparator -``` - -и не должен иметь зависимости от конкретного класса `Student`. From b932636409c2a5807e66acd47969dc1fff7e0df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 18:37:48 +0400 Subject: [PATCH 081/110] refactor(ui): rename BaseWindow to BaseModalWindow --- .../ui/base/{BaseWindow.java => BaseModalWindow.java} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename app/src/main/java/ru/project/ui/base/{BaseWindow.java => BaseModalWindow.java} (62%) diff --git a/app/src/main/java/ru/project/ui/base/BaseWindow.java b/app/src/main/java/ru/project/ui/base/BaseModalWindow.java similarity index 62% rename from app/src/main/java/ru/project/ui/base/BaseWindow.java rename to app/src/main/java/ru/project/ui/base/BaseModalWindow.java index 90fbb7c..0f0abde 100644 --- a/app/src/main/java/ru/project/ui/base/BaseWindow.java +++ b/app/src/main/java/ru/project/ui/base/BaseModalWindow.java @@ -3,17 +3,17 @@ package ru.project.ui.base; import com.googlecode.lanterna.gui2.BasicWindow; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; -public abstract class BaseWindow extends BasicWindow { +public abstract class BaseModalWindow extends BasicWindow { protected final WindowBasedTextGUI gui; - protected BaseWindow(String title, WindowBasedTextGUI gui) { + protected BaseModalWindow(String title, WindowBasedTextGUI gui) { super(title); this.gui = gui; } - public void show() { + public void showModal() { gui.addWindowAndWait(this); } } From 266aaa4c7777374a21e846a52284982903a71916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 18:38:45 +0400 Subject: [PATCH 082/110] refactor(model): rename currentCollection to mainCollection --- app/src/main/java/ru/project/model/AppState.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/ru/project/model/AppState.java b/app/src/main/java/ru/project/model/AppState.java index e430057..375877e 100644 --- a/app/src/main/java/ru/project/model/AppState.java +++ b/app/src/main/java/ru/project/model/AppState.java @@ -6,22 +6,22 @@ import ru.project.student.Student; public class AppState { - private MyList currentCollection; + private MyList mainCollection; private MyList customDataCollection; private String currentFilePath; public AppState() { - this.currentCollection = new MyLinkedList<>(); + this.mainCollection = new MyLinkedList<>(); this.customDataCollection = new MyLinkedList<>(); this.currentFilePath = null; } - public MyList getCurrentCollection() { - return currentCollection; + public MyList getMainCollection() { + return mainCollection; } - public void setCurrentCollection(MyList currentCollection) { - this.currentCollection = currentCollection; + public void setMainCollection(MyList currentCollection) { + this.mainCollection = currentCollection; } public MyList getCustomDataCollection() { From 5c459a101767d962879b617094c1a8ee75bd7575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 18:39:24 +0400 Subject: [PATCH 083/110] refactor(ui): update MessageWindow for modal base class --- app/src/main/java/ru/project/ui/window/MessageWindow.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ru/project/ui/window/MessageWindow.java b/app/src/main/java/ru/project/ui/window/MessageWindow.java index d6d592c..76ad9a1 100644 --- a/app/src/main/java/ru/project/ui/window/MessageWindow.java +++ b/app/src/main/java/ru/project/ui/window/MessageWindow.java @@ -5,9 +5,9 @@ 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.BaseWindow; +import ru.project.ui.base.BaseModalWindow; -public class MessageWindow extends BaseWindow { +public class MessageWindow extends BaseModalWindow { public MessageWindow(WindowBasedTextGUI gui, String message) { super("Информация", gui); @@ -22,7 +22,7 @@ public class MessageWindow extends BaseWindow { setComponent(panel); } - public static void show(WindowBasedTextGUI gui, String message) { - new MessageWindow(gui, message).show(); + public static void showModal(WindowBasedTextGUI gui, String message) { + new MessageWindow(gui, message).showModal(); } } From 8f5548644988364bfe88c1009df48cbe56cffd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 18:42:22 +0400 Subject: [PATCH 084/110] feat(collection): implement custom collection creation flow - rename window base class usage to BaseModalWindow and update modal method calls - add persistent CustomDataSettingsWindow instance to CreateCollectionMenu - prepare custom data collection dynamically before showing the settings window - apply custom data to AppState without immediately replacing mainCollection - create mainCollection from prepared custom student data - replace student preview message with collection preview window - update input validation and StudentBuilder state handling - rename collection-related methods and variables for clearer semantics --- app/src/main/java/ru/project/App.java | 2 +- .../project/ui/menu/CreateCollectionMenu.java | 71 +++++----- .../ui/menu/FillMethodSelectionWindow.java | 4 +- .../java/ru/project/ui/menu/MainMenu.java | 35 +++-- .../ui/window/CollectionDisplayWindow.java | 4 +- .../ui/window/CustomDataSettingsWindow.java | 125 ++++++++++-------- .../project/ui/window/InputValueWindow.java | 6 +- 7 files changed, 139 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index 1e1d933..fa4ee64 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -25,7 +25,7 @@ public final class App { MainMenu mainMenu = new MainMenu(gui, state); - mainMenu.show(); + mainMenu.showModal(); } catch (Exception e) { System.err.println("Произошла ошибка при выполнении программы:"); diff --git a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java index 48d2f0c..dd9785a 100644 --- a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java @@ -5,14 +5,17 @@ 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.MyLinkedList; +import ru.project.collection.MyList; import ru.project.model.AppState; import ru.project.model.FillMethod; -import ru.project.ui.base.BaseWindow; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; import ru.project.ui.window.CustomDataSettingsWindow; import ru.project.ui.window.InputValueWindow; import ru.project.ui.window.MessageWindow; -public class CreateCollectionMenu extends BaseWindow { +public class CreateCollectionMenu extends BaseModalWindow { private int collectionSize; private FillMethod selectedFillMethod; @@ -20,16 +23,16 @@ public class CreateCollectionMenu extends BaseWindow { private final Label sizeLabel; private final Label fillMethodLabel; - private final AppState state; + private final CustomDataSettingsWindow customDataSettingsWindow; public CreateCollectionMenu(WindowBasedTextGUI gui, AppState state) { super("Создание новой коллекции", gui); - this.state = state; - collectionSize = 0; selectedFillMethod = FillMethod.CUSTOM; + customDataSettingsWindow = new CustomDataSettingsWindow(gui, state, () -> collectionSize); + Panel panel = new Panel(); panel.setLayoutManager(new LinearLayout()); @@ -40,21 +43,21 @@ public class CreateCollectionMenu extends BaseWindow { panel.addComponent(sizeLabel); panel.addComponent(fillMethodLabel); - panel.addComponent(new Button("Задать размер", this::openCollectionSizeInputValueWindow)); + panel.addComponent(new Button("Задать размер", this::showCollectionSizeInputValueWindow)); panel.addComponent(new Button("Выбрать способ заполнения", this::selectFillMethod)); panel.addComponent( new Button("Настроить текущий способ заполнения", this::setupCurrentFillMethod)); - panel.addComponent(new Button("Создать новую коллекцию", this::createCollection)); + panel.addComponent(new Button("Создать новую коллекцию", () -> updateMainCollection(state))); panel.addComponent(new Button("Выйти", this::close)); setComponent(panel); } - private void openCollectionSizeInputValueWindow() { + private void showCollectionSizeInputValueWindow() { InputValueWindow window = new InputValueWindow( gui, @@ -63,21 +66,21 @@ public class CreateCollectionMenu extends BaseWindow { collectionSize > 0 ? String.valueOf(collectionSize) : null, this::handleSizeEntered); - window.show(); + window.showModal(); } private boolean handleSizeEntered(String value) { try { int enteredSize = Integer.parseInt(value); if (enteredSize <= 0) { - MessageWindow.show(gui, "Размер должен быть больше 0."); + MessageWindow.showModal(gui, "Размер должен быть больше 0."); return false; } collectionSize = enteredSize; sizeLabel.setText("Размер: " + collectionSize); return true; } catch (NumberFormatException e) { - MessageWindow.show(gui, "Введите целое число."); + MessageWindow.showModal(gui, "Введите целое число."); return false; } } @@ -85,22 +88,21 @@ public class CreateCollectionMenu extends BaseWindow { private void setupCurrentFillMethod() { switch (selectedFillMethod) { case CUSTOM -> { - BaseWindow window = new CustomDataSettingsWindow(gui, state, collectionSize); - - window.show(); + customDataSettingsWindow.showModal(); } case FILE -> // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); case RANDOM -> - MessageWindow.show(gui, "Для случайных данных дополнительные настройки не требуются."); + MessageWindow.showModal( + gui, "Для случайных данных дополнительные настройки не требуются."); } } private void selectFillMethod() { - BaseWindow menu = + BaseModalWindow menu = new FillMethodSelectionWindow( gui, fillMethod -> { @@ -109,43 +111,52 @@ public class CreateCollectionMenu extends BaseWindow { "Способ заполнения: " + getFillMethodName(selectedFillMethod)); }); - menu.show(); + menu.showModal(); } - private void createCollection() { + private void updateMainCollection(AppState state) { if (collectionSize <= 0) { - MessageWindow.show(gui, "Сначала задайте размер коллекции."); + MessageWindow.showModal(gui, "Сначала задайте размер коллекции."); return; } switch (selectedFillMethod) { case CUSTOM: - createCollectionFromCustomData(); + createCollectionFromCustomData(state); break; case RANDOM: - createCollectionFromRandomData(); + createCollectionFromRandomData(state); break; case FILE: - createCollectionFromFileData(); + createCollectionFromFileData(state); break; } } - private void createCollectionFromCustomData() { - // TODO - MessageWindow.show(gui, "Не реализовано."); + private void createCollectionFromCustomData(AppState state) { + MyList customDataCollection = state.getCustomDataCollection(); + + MyList mainCollection = new MyLinkedList<>(); + + for (int i = 0; i < customDataCollection.size(); i++) { + mainCollection.add(customDataCollection.get(i)); + } + + state.setMainCollection(mainCollection); + + MessageWindow.showModal(gui, "Коллекция успешно создана."); } - private void createCollectionFromRandomData() { + private void createCollectionFromRandomData(AppState state) { // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); } - private void createCollectionFromFileData() { + private void createCollectionFromFileData(AppState state) { // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); } private String getFillMethodName(FillMethod fillMethod) { diff --git a/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java index 245fd06..f408210 100644 --- a/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java +++ b/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java @@ -4,9 +4,9 @@ import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import java.util.function.Consumer; import ru.project.model.FillMethod; -import ru.project.ui.base.BaseWindow; +import ru.project.ui.base.BaseModalWindow; -public class FillMethodSelectionWindow extends BaseWindow { +public class FillMethodSelectionWindow extends BaseModalWindow { private final Consumer onFillMethodSelected; diff --git a/app/src/main/java/ru/project/ui/menu/MainMenu.java b/app/src/main/java/ru/project/ui/menu/MainMenu.java index 308277e..93cef48 100644 --- a/app/src/main/java/ru/project/ui/menu/MainMenu.java +++ b/app/src/main/java/ru/project/ui/menu/MainMenu.java @@ -5,25 +5,26 @@ import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import ru.project.collection.MyList; import ru.project.model.AppState; import ru.project.student.Student; -import ru.project.ui.base.BaseWindow; +import ru.project.ui.base.BaseModalWindow; import ru.project.ui.window.CollectionDisplayWindow; import ru.project.ui.window.MessageWindow; -public class MainMenu extends BaseWindow { +public class MainMenu extends BaseModalWindow { private final ActionListBox menu; - private final AppState state; + + private final BaseModalWindow createCollectionMenu; public MainMenu(WindowBasedTextGUI gui, AppState state) { super("Главное меню", gui); - this.state = state; + this.createCollectionMenu = new CreateCollectionMenu(gui, state); menu = new ActionListBox(); - menu.addItem("Показать текущую коллекцию", this::showCurrentCollection); + menu.addItem("Показать текущую коллекцию", () -> showMainCollection(state)); - menu.addItem("Меню создания новой коллекции", this::createCollection); + menu.addItem("Меню создания новой коллекции", this::showCreateCollectionMenu); menu.addItem("Меню сохранения текущей коллекции в файл", this::saveCollection); @@ -38,38 +39,36 @@ public class MainMenu extends BaseWindow { setComponent(menu); } - private void showCurrentCollection() { - MyList currentCollection = state.getCurrentCollection(); + private void showMainCollection(AppState state) { + MyList mainCollection = state.getMainCollection(); - BaseWindow window = new CollectionDisplayWindow(gui, currentCollection); + BaseModalWindow window = new CollectionDisplayWindow(gui, mainCollection); - window.show(); + window.showModal(); } - private void createCollection() { - BaseWindow window = new CreateCollectionMenu(gui, state); - - window.show(); + private void showCreateCollectionMenu() { + createCollectionMenu.showModal(); } private void saveCollection() { // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); } private void sortCollection() { // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); } private void searchCollection() { // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); } private void countOccurrences() { // TODO - MessageWindow.show(gui, "Не реализовано."); + MessageWindow.showModal(gui, "Не реализовано."); } private void exit() { diff --git a/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java b/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java index 4ca3c5e..f5da901 100644 --- a/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java +++ b/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java @@ -12,9 +12,9 @@ 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.BaseWindow; +import ru.project.ui.base.BaseModalWindow; -public class CollectionDisplayWindow extends BaseWindow { +public class CollectionDisplayWindow extends BaseModalWindow { private final MyList collection; diff --git a/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java b/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java index b59f805..377b417 100644 --- a/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java +++ b/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java @@ -5,17 +5,18 @@ 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.Supplier; import ru.project.collection.MyLinkedList; import ru.project.collection.MyList; import ru.project.model.AppState; import ru.project.student.Student; import ru.project.student.StudentBuilder; -import ru.project.ui.base.BaseWindow; +import ru.project.ui.base.BaseModalWindow; -public class CustomDataSettingsWindow extends BaseWindow { +public class CustomDataSettingsWindow extends BaseModalWindow { private final AppState state; - private final int collectionSize; + private final Supplier requestedCollectionSize; private final StudentBuilder studentBuilder; private String groupNumber; @@ -28,28 +29,17 @@ public class CustomDataSettingsWindow extends BaseWindow { private final Label recordBookNumberLabel; private final Label indexLabel; - private final MyList preparedCustomDataCollection; + private MyList preparedCustomDataCollection; - public CustomDataSettingsWindow(WindowBasedTextGUI gui, AppState state, int collectionSize) { + public CustomDataSettingsWindow( + WindowBasedTextGUI gui, AppState state, Supplier collectionSize) { super("Настройка пользовательских данных", gui); this.state = state; - this.collectionSize = collectionSize; + this.requestedCollectionSize = collectionSize; + this.studentBuilder = new StudentBuilder(); - this.preparedCustomDataCollection = new MyLinkedList<>(); - - MyList customDataCollection = state.getCustomDataCollection(); - - int elementsToCopy = Math.min(customDataCollection.size(), collectionSize); - - for (int i = 0; i < elementsToCopy; i++) { - preparedCustomDataCollection.add(customDataCollection.get(i)); - } - - for (int i = elementsToCopy; i < collectionSize; i++) { - preparedCustomDataCollection.add(null); - } Panel panel = new Panel(); panel.setLayoutManager(new LinearLayout()); @@ -86,6 +76,29 @@ public class CustomDataSettingsWindow extends BaseWindow { setComponent(panel); } + @Override + public void showModal() { + prepareCustomDataCollection(); + super.showModal(); + } + + private void prepareCustomDataCollection() { + preparedCustomDataCollection = new MyLinkedList<>(); + + int targetCollectionSize = requestedCollectionSize.get(); + int currentCustomDataCollectionSize = state.getCustomDataCollection().size(); + + int elementsToCopy = Math.min(currentCustomDataCollectionSize, targetCollectionSize); + + for (int i = 0; i < elementsToCopy; i++) { + preparedCustomDataCollection.add(state.getCustomDataCollection().get(i)); + } + + for (int i = elementsToCopy; i < targetCollectionSize; i++) { + preparedCustomDataCollection.add(null); + } + } + private void setGroupNumber() { InputValueWindow window = new InputValueWindow( @@ -95,7 +108,7 @@ public class CustomDataSettingsWindow extends BaseWindow { groupNumber, value -> { if (!value.matches(Student.getGroupNumberPattern())) { - MessageWindow.show(gui, "Номер группы должен соответствовать формату A12."); + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); return false; } @@ -107,7 +120,7 @@ public class CustomDataSettingsWindow extends BaseWindow { return true; }); - window.show(); + window.showModal(); } private void setAverageGrade() { @@ -124,24 +137,27 @@ public class CustomDataSettingsWindow extends BaseWindow { if (parsedValue < Student.getMinAverageGrade() || parsedValue > Student.getMaxAverageGrade()) { - MessageWindow.show(gui, "Средний балл должен быть от 0.0 до 5.0."); + MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0."); + return false; } averageGrade = parsedValue; + studentBuilder.setAverageGrade(parsedValue); averageGradeLabel.setText("Средний балл: " + averageGrade); + return true; + } catch (NumberFormatException e) { - MessageWindow.show(gui, "Введите корректное число."); + MessageWindow.showModal(gui, "Введите корректное число."); + return false; } - - return true; }); - window.show(); + window.showModal(); } private void setRecordBookNumber() { @@ -156,24 +172,28 @@ public class CustomDataSettingsWindow extends BaseWindow { int parsedValue = Integer.parseInt(value); if (parsedValue < Student.getMinRecordBookNumber()) { - MessageWindow.show(gui, "Номер зачётной книжки должен быть положительным."); + + MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным."); + return false; } recordBookNumber = parsedValue; + studentBuilder.setRecordBookNumber(parsedValue); recordBookNumberLabel.setText("Номер зачётной книжки: " + recordBookNumber); + return true; + } catch (NumberFormatException e) { - MessageWindow.show(gui, "Введите целое число."); + MessageWindow.showModal(gui, "Введите целое число."); + return false; } - - return true; }); - window.show(); + window.showModal(); } private void setObjectIndex() { @@ -188,12 +208,16 @@ public class CustomDataSettingsWindow extends BaseWindow { int index = Integer.parseInt(value); if (index < 0) { - MessageWindow.show(gui, "Индекс объекта не может быть отрицательным."); + MessageWindow.showModal(gui, "Индекс объекта не может быть отрицательным."); + return false; } - if (index >= collectionSize) { - MessageWindow.show(gui, "Индекс должен быть меньше размера коллекции."); + int size = requestedCollectionSize.get(); + + if (index >= size) { + MessageWindow.showModal(gui, "Индекс должен быть меньше размера коллекции."); + return false; } @@ -204,27 +228,28 @@ public class CustomDataSettingsWindow extends BaseWindow { return true; } catch (NumberFormatException e) { - MessageWindow.show(gui, "Введите целое число."); + MessageWindow.showModal(gui, "Введите целое число."); + return false; } }); - window.show(); + window.showModal(); } private void editObject() { if (!hasStudentData()) { - MessageWindow.show(gui, "Сначала задайте все поля студента."); + MessageWindow.showModal(gui, "Сначала задайте все поля студента."); return; } if (selectedObjectIndex == null) { - MessageWindow.show(gui, "Сначала задайте индекс объекта."); + MessageWindow.showModal(gui, "Сначала задайте индекс объекта."); return; } if (selectedObjectIndex >= preparedCustomDataCollection.size()) { - MessageWindow.show(gui, "Индекс объекта выходит за границы подготовленной коллекции."); + MessageWindow.showModal(gui, "Индекс объекта выходит за границы подготовленной коллекции."); return; } @@ -232,31 +257,27 @@ public class CustomDataSettingsWindow extends BaseWindow { preparedCustomDataCollection.set(selectedObjectIndex, student); - MessageWindow.show(gui, "Данные объекта успешно заданы."); + MessageWindow.showModal(gui, "Данные объекта успешно заданы."); } private void apply() { - MyList currentCollection = new MyLinkedList<>(); - - for (int i = 0; i < preparedCustomDataCollection.size(); i++) { - currentCollection.add(preparedCustomDataCollection.get(i)); - } - state.setCustomDataCollection(preparedCustomDataCollection); - state.setCurrentCollection(currentCollection); + prepareCustomDataCollection(); - close(); + MessageWindow.showModal(gui, "Пользовательские данные успешно применены."); } private void preview() { - if (!hasStudentData()) { - MessageWindow.show(gui, "Для предпросмотра сначала задайте все поля студента."); + if (preparedCustomDataCollection == null || preparedCustomDataCollection.isEmpty()) { + + MessageWindow.showModal(gui, "Подготовленная коллекция пуста."); + return; } - Student student = studentBuilder.build(); + CollectionDisplayWindow window = new CollectionDisplayWindow(gui, preparedCustomDataCollection); - MessageWindow.show(gui, student.toString()); + window.showModal(); } private boolean hasStudentData() { diff --git a/app/src/main/java/ru/project/ui/window/InputValueWindow.java b/app/src/main/java/ru/project/ui/window/InputValueWindow.java index d21a79b..dc16dbb 100644 --- a/app/src/main/java/ru/project/ui/window/InputValueWindow.java +++ b/app/src/main/java/ru/project/ui/window/InputValueWindow.java @@ -7,9 +7,9 @@ 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.BaseWindow; +import ru.project.ui.base.BaseModalWindow; -public class InputValueWindow extends BaseWindow { +public class InputValueWindow extends BaseModalWindow { private final TextBox textBox; private final Predicate onSave; @@ -45,7 +45,7 @@ public class InputValueWindow extends BaseWindow { String value = textBox.getText().trim(); if (value.isEmpty()) { - MessageWindow.show(gui, "Значение не может быть пустым."); + MessageWindow.showModal(gui, "Значение не может быть пустым."); return; } From 0d1f0cbfc45618fb620947970f0a705c7f09e122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sat, 29 Aug 2026 16:16:17 +0300 Subject: [PATCH 085/110] feat: implement composite pattern for student search --- .../search/AverageGradeRangeCriteria.java | 22 ++++++++ .../CompositeStudentSearchCriteria.java | 51 +++++++++++++++++++ .../search/GroupNumberRangeCriteria.java | 25 +++++++++ .../search/RecordBookNumberRangeCriteria.java | 22 ++++++++ .../ru/project/search/SearchOperation.java | 6 +++ .../project/search/StudentSearchCriteria.java | 7 +++ .../project/search/StudentSearchService.java | 30 +++++++++++ 7 files changed, 163 insertions(+) create mode 100644 app/src/main/java/ru/project/search/AverageGradeRangeCriteria.java create mode 100644 app/src/main/java/ru/project/search/CompositeStudentSearchCriteria.java create mode 100644 app/src/main/java/ru/project/search/GroupNumberRangeCriteria.java create mode 100644 app/src/main/java/ru/project/search/RecordBookNumberRangeCriteria.java create mode 100644 app/src/main/java/ru/project/search/SearchOperation.java create mode 100644 app/src/main/java/ru/project/search/StudentSearchCriteria.java create mode 100644 app/src/main/java/ru/project/search/StudentSearchService.java 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..a53a7c5 --- /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; + } +} \ No newline at end of file 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..063d087 --- /dev/null +++ b/app/src/main/java/ru/project/search/CompositeStudentSearchCriteria.java @@ -0,0 +1,51 @@ +package ru.project.search; + +import ru.project.student.Student; +import java.util.ArrayList; +import java.util.List; + +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; + } + } +} \ No newline at end of file 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..61ea406 --- /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; + } +} \ No newline at end of file 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..41131b4 --- /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; + } +} \ No newline at end of file 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..d616f97 --- /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 +} \ No newline at end of file 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..f157982 --- /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); +} \ No newline at end of file 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..4ced3c9 --- /dev/null +++ b/app/src/main/java/ru/project/search/StudentSearchService.java @@ -0,0 +1,30 @@ +package ru.project.search; + +import ru.project.list.MyList; +import ru.project.list.MyLinkedList; +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; + } +} \ No newline at end of file From e02e519b2c630d1cd40e6397d1030774108c71aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Sat, 29 Aug 2026 12:47:02 +0300 Subject: [PATCH 086/110] feat(sorting): add even-only sort strategy --- .../project/sorting/EvenOnlySortStrategy.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java 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..32349c1 --- /dev/null +++ b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java @@ -0,0 +1,94 @@ +package ru.project.sorting; + +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; + +import java.util.Comparator; +import java.util.function.ToIntFunction; + +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)); + } + } +} \ No newline at end of file From 715b411eeadc7009e46ffbd38f90852f3324d91a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=8E=D1=89=D0=B8=D0=BA=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D1=81=D0=B8=D1=8F?= Date: Sat, 29 Aug 2026 12:47:56 +0300 Subject: [PATCH 087/110] test(sorting): add even-only sort strategy tests --- .../sorting/EvenOnlySortStrategyTest.java | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java 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..8d482bd --- /dev/null +++ b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java @@ -0,0 +1,301 @@ +package ru.project.sorting; + +import org.junit.jupiter.api.Test; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.student.Student; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.Comparator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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); + } + } + } +} \ No newline at end of file From 56652fda719eeb95460aa2c60e175f7ebc6311e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 21:21:53 +0400 Subject: [PATCH 088/110] refactor(sorting): remove redundant even-only merge sort strategy --- .../sorting/EvenOnlyMergeSortStrategy.java | 60 ------ .../EvenOnlyMergeSortStrategyTest.java | 177 ------------------ 2 files changed, 237 deletions(-) delete mode 100644 app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java delete mode 100644 app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java diff --git a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java deleted file mode 100644 index 45f5b78..0000000 --- a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java +++ /dev/null @@ -1,60 +0,0 @@ -package ru.project.sorting; - -import ru.project.collection.MyLinkedList; -import ru.project.collection.MyList; - -import java.util.Comparator; -import java.util.function.ToIntFunction; - -public class EvenOnlyMergeSortStrategy implements SortStrategy { - private final ToIntFunction valueExtractor; - private final SortStrategy mergeSortStrategy; - - public EvenOnlyMergeSortStrategy(ToIntFunction valueExtractor) { - this.valueExtractor = valueExtractor; - this.mergeSortStrategy = new MergeSortStrategy<>(); - } - - @Override - public void sort(MyList list, Comparator comparator) { - - 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++; - } - } - mergeSortStrategy.sort(evenElements, comparator); - - for (int i = 0; i < evenIndexes.length; i++) { - list.set(evenIndexes[i], evenElements.get(i)); - } - } -} - diff --git a/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java b/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java deleted file mode 100644 index ffaef36..0000000 --- a/app/src/test/java/ru/project/sorting/EvenOnlyMergeSortStrategyTest.java +++ /dev/null @@ -1,177 +0,0 @@ -package ru.project.sorting; - -import org.junit.jupiter.api.Test; -import ru.project.collection.MyLinkedList; -import ru.project.collection.MyList; -import ru.project.student.Student; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class EvenOnlyMergeSortStrategyTest { - - @Test - void shouldSortOnlyEvenElements() { - MyList list = new MyLinkedList<>(); - - list.add(7); - list.add(8); - list.add(3); - list.add(2); - list.add(5); - list.add(4); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(value -> value); - - strategy.sort(list, Integer::compare); - - assertEquals(7, list.get(0)); - assertEquals(2, list.get(1)); - assertEquals(3, list.get(2)); - assertEquals(4, list.get(3)); - assertEquals(5, list.get(4)); - assertEquals(8, list.get(5)); - - assertEquals(6, list.size()); - } - - @Test - void shouldKeepAllOddElementsUnchanged() { - MyList list = new MyLinkedList<>(); - - list.add(7); - list.add(3); - list.add(5); - list.add(1); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(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 shouldSortAllEvenElements() { - MyList list = new MyLinkedList<>(); - - list.add(8); - list.add(2); - list.add(6); - list.add(4); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(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 shouldKeepSingleElementUnchanged() { - MyList list = new MyLinkedList<>(); - - list.add(4); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(value -> value); - - strategy.sort(list, Integer::compare); - - assertEquals(4, list.get(0)); - assertEquals(1, list.size()); - } - - @Test - void shouldHandleEmptyList() { - MyList list = new MyLinkedList<>(); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(value -> value); - - strategy.sort(list, Integer::compare); - - assertEquals(0, list.size()); - } - - @Test - void shouldUseValueExtractorToDetermineEvenElements() { - MyList list = new MyLinkedList<>(); - - list.add(4); - list.add(3); - list.add(2); - list.add(1); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(value -> value + 1); - - strategy.sort(list, Integer::compare); - - assertEquals(4, list.get(0)); - assertEquals(1, list.get(1)); - assertEquals(2, list.get(2)); - assertEquals(3, list.get(3)); - } - - @Test - void shouldUseProvidedComparator() { - MyList list = new MyLinkedList<>(); - - list.add(7); - list.add(2); - list.add(3); - list.add(8); - list.add(5); - list.add(4); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(value -> value); - - strategy.sort(list, (first, second) -> Integer.compare(second, first)); - - assertEquals(7, list.get(0)); - assertEquals(8, list.get(1)); - assertEquals(3, list.get(2)); - assertEquals(4, list.get(3)); - assertEquals(5, list.get(4)); - assertEquals(2, list.get(5)); - } - - @Test - void shouldUseStudentRecordBookNumberToDetermineEvenElements() { - 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)); - - SortStrategy strategy = new EvenOnlyMergeSortStrategy<>( - 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()); - } -} - - From 479ea98237789c646ad1c5c2a844f136c2603918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 10:53:48 +0300 Subject: [PATCH 089/110] feat: add MyListCollector for streaming into MyList --- app/src/main/java/ru/project/MyListCollector.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 app/src/main/java/ru/project/MyListCollector.java diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java new file mode 100644 index 0000000..c2b21c5 --- /dev/null +++ b/app/src/main/java/ru/project/MyListCollector.java @@ -0,0 +1,4 @@ +public class MyListCollector implements Collector, MyList> +{ + +} \ No newline at end of file From e4a48381f0f1915d0eef7f16451685da223d1b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 11:11:12 +0300 Subject: [PATCH 090/110] feat:add supplier for MyListCollector --- app/src/main/java/ru/project/MyListCollector.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java index c2b21c5..eba69de 100644 --- a/app/src/main/java/ru/project/MyListCollector.java +++ b/app/src/main/java/ru/project/MyListCollector.java @@ -1,4 +1,16 @@ +package ru.test.maven.spring; +import java.util.Collections; +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; public class MyListCollector implements Collector, MyList> { - + @Override + public Supplier> supplier() + { + return () -> new MyLinkedList<>(); + } } \ No newline at end of file From 501236f881eea9a7cbffa2d7d0a54df9f1e55a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 11:13:45 +0300 Subject: [PATCH 091/110] feat: add accumulator to MyListCollector --- app/src/main/java/ru/project/MyListCollector.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java index eba69de..da5c8db 100644 --- a/app/src/main/java/ru/project/MyListCollector.java +++ b/app/src/main/java/ru/project/MyListCollector.java @@ -13,4 +13,9 @@ public class MyListCollector implements Collector, MyList> { return () -> new MyLinkedList<>(); } + @Override + public BiConsumer, T> accumulator() { + + return (list, element) -> list.add(element); + } } \ No newline at end of file From 02c11a040db307d980f7ae248e6afb80ab40057e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 12:09:13 +0300 Subject: [PATCH 092/110] feat: add combiner to MyListCollector --- app/src/main/java/ru/project/MyListCollector.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java index da5c8db..7c79282 100644 --- a/app/src/main/java/ru/project/MyListCollector.java +++ b/app/src/main/java/ru/project/MyListCollector.java @@ -18,4 +18,13 @@ public class MyListCollector implements Collector, MyList> return (list, element) -> list.add(element); } + @Override + public BinaryOperator> combiner() { + return (list1, list2) -> { + for (int i = 0; i < list2.size(); i++) { + list1.add(list2.get(i)); + } + return list1; + }; + } } \ No newline at end of file From fe41b5d67da43c5294e5f19dd3b477bad1ece228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 12:12:03 +0300 Subject: [PATCH 093/110] feat: implement identity finisher() for MyListCollector --- app/src/main/java/ru/project/MyListCollector.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java index 7c79282..c976681 100644 --- a/app/src/main/java/ru/project/MyListCollector.java +++ b/app/src/main/java/ru/project/MyListCollector.java @@ -27,4 +27,9 @@ public class MyListCollector implements Collector, MyList> return list1; }; } + @Override + public Function, MyList> finisher() + { + return list -> list; + } } \ No newline at end of file From 82713a80c6edb2274a1ef96bab979c4a498b1557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 12:14:27 +0300 Subject: [PATCH 094/110] feat: add characteristics to MyListCollector --- app/src/main/java/ru/project/MyListCollector.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java index c976681..f731379 100644 --- a/app/src/main/java/ru/project/MyListCollector.java +++ b/app/src/main/java/ru/project/MyListCollector.java @@ -32,4 +32,10 @@ public class MyListCollector implements Collector, MyList> { return list -> list; } + @Override + public Set characteristics() { + return java.util.Set.of( + Collector.Characteristics.IDENTITY_FINISH + ); + } } \ No newline at end of file From a63a8fdaacdb6cee4f829bb1893bb9602c6aec37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 12:17:22 +0300 Subject: [PATCH 095/110] feat: create MyCollectors with toMyList factory --- app/src/main/java/ru/project/MyCollectors.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/src/main/java/ru/project/MyCollectors.java diff --git a/app/src/main/java/ru/project/MyCollectors.java b/app/src/main/java/ru/project/MyCollectors.java new file mode 100644 index 0000000..20840c3 --- /dev/null +++ b/app/src/main/java/ru/project/MyCollectors.java @@ -0,0 +1,10 @@ +package ru.test.maven.spring; +import java.util.stream.Collector; +public class MyCollectors +{ + private MyCollectors() { + } + public static Collector, MyList> toMyList() { + return new MyListCollector<>(); + } +} \ No newline at end of file From 4e64c3e35b2957308fede8f8ef6d72cf05ea6006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 12:23:37 +0300 Subject: [PATCH 096/110] test: add tests for MyListCollector --- .../java/ru/project/MyListCollectorTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/src/test/java/ru/project/MyListCollectorTest.java diff --git a/app/src/test/java/ru/project/MyListCollectorTest.java b/app/src/test/java/ru/project/MyListCollectorTest.java new file mode 100644 index 0000000..6155d57 --- /dev/null +++ b/app/src/test/java/ru/project/MyListCollectorTest.java @@ -0,0 +1,63 @@ +package ru.test.maven.spring; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.IntStream; +import java.util.Collections; +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)); + } + +} From e7e709a5720042eb887a038e88d5209cb41f4d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sat, 29 Aug 2026 21:18:14 +0300 Subject: [PATCH 097/110] remove obsolete test file StudentTest1 --- .../java/ru/project/student/StudentTest1.java | 139 ------------------ 1 file changed, 139 deletions(-) delete mode 100644 app/src/test/java/ru/project/student/StudentTest1.java diff --git a/app/src/test/java/ru/project/student/StudentTest1.java b/app/src/test/java/ru/project/student/StudentTest1.java deleted file mode 100644 index cdce6d8..0000000 --- a/app/src/test/java/ru/project/student/StudentTest1.java +++ /dev/null @@ -1,139 +0,0 @@ -package ru.project.student; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -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"); - } -} \ No newline at end of file From 1de5a59e93a2b81028f943a29207279ee04c4833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 22:58:09 +0400 Subject: [PATCH 098/110] feat(ui): add collection sorting menu and sort method selection --- .../java/ru/project/model/SortMethod.java | 6 ++ .../java/ru/project/ui/menu/MainMenu.java | 5 +- .../project/ui/menu/SortCollectionMenu.java | 101 ++++++++++++++++++ .../ui/window/SortMethodSelectionWindow.java | 33 ++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/ru/project/model/SortMethod.java create mode 100644 app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java create mode 100644 app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java diff --git a/app/src/main/java/ru/project/model/SortMethod.java b/app/src/main/java/ru/project/model/SortMethod.java new file mode 100644 index 0000000..8cf4c6e --- /dev/null +++ b/app/src/main/java/ru/project/model/SortMethod.java @@ -0,0 +1,6 @@ +package ru.project.model; + +public enum SortMethod { + NORMAL, + PARITY +} diff --git a/app/src/main/java/ru/project/ui/menu/MainMenu.java b/app/src/main/java/ru/project/ui/menu/MainMenu.java index 93cef48..32999d7 100644 --- a/app/src/main/java/ru/project/ui/menu/MainMenu.java +++ b/app/src/main/java/ru/project/ui/menu/MainMenu.java @@ -14,11 +14,13 @@ public class MainMenu extends BaseModalWindow { private final ActionListBox menu; private final BaseModalWindow createCollectionMenu; + private final BaseModalWindow sortCollectionMenu; public MainMenu(WindowBasedTextGUI gui, AppState state) { super("Главное меню", gui); this.createCollectionMenu = new CreateCollectionMenu(gui, state); + this.sortCollectionMenu = new SortCollectionMenu(gui, state); menu = new ActionListBox(); @@ -57,8 +59,7 @@ public class MainMenu extends BaseModalWindow { } private void sortCollection() { - // TODO - MessageWindow.showModal(gui, "Не реализовано."); + sortCollectionMenu.showModal(); } private void searchCollection() { diff --git a/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java new file mode 100644 index 0000000..5c9ea61 --- /dev/null +++ b/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java @@ -0,0 +1,101 @@ +package ru.project.ui.menu; + +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.model.SortMethod; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.window.MessageWindow; +import ru.project.ui.window.SortMethodSelectionWindow; + +public class SortCollectionMenu extends BaseModalWindow { + + private final AppState state; + + private final Label sortMethodLabel; + private final SortMethodSelectionWindow sortMethodSelectionWindow; + + private SortMethod selectedSortMethod; + + public SortCollectionMenu(WindowBasedTextGUI gui, AppState state) { + + super("Меню сортировки текущей коллекции", gui); + + this.state = state; + this.selectedSortMethod = SortMethod.NORMAL; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + this.sortMethodLabel = new Label("Способ сортировки: " + getSortMethodName(selectedSortMethod)); + + this.sortMethodSelectionWindow = + new SortMethodSelectionWindow( + gui, + sortMethod -> { + selectedSortMethod = sortMethod; + + sortMethodLabel.setText( + "Способ сортировки: " + getSortMethodName(selectedSortMethod)); + }); + + panel.addComponent(sortMethodLabel); + + panel.addComponent(new Button("Выбрать способ сортировки", this::selectSortMethod)); + + panel.addComponent( + new Button("Настроить текущий способ сортировки", this::setupCurrentSortMethod)); + + panel.addComponent(new Button("Сортировать текущую коллекцию", this::sortCurrentCollection)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setupCurrentSortMethod() { + MessageWindow.showModal(gui, "Настройка способа сортировки пока не реализована."); + } + + private void selectSortMethod() { + sortMethodSelectionWindow.showModal(); + } + + private void sortCurrentCollection() { + if (state.getMainCollection() == null || state.getMainCollection().isEmpty()) { + + MessageWindow.showModal(gui, "Текущая коллекция пуста."); + return; + } + + switch (selectedSortMethod) { + case NORMAL: + sortNormally(); + break; + + case PARITY: + sortByParity(); + break; + } + } + + private void sortNormally() { + // TODO + MessageWindow.showModal(gui, "Обычная сортировка не реализована."); + } + + private void sortByParity() { + // TODO + MessageWindow.showModal(gui, "Сортировка по чётности не реализована."); + } + + private String getSortMethodName(SortMethod sortMethod) { + return switch (sortMethod) { + case NORMAL -> "Обычная сортировка"; + case PARITY -> "Сортировка по чётности"; + }; + } +} diff --git a/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java new file mode 100644 index 0000000..ea234ef --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java @@ -0,0 +1,33 @@ +package ru.project.ui.window; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import ru.project.model.SortMethod; +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)); + + setComponent(menu); + } + + private void selectSortMethod(SortMethod sortMethod) { + onSortMethodSelected.accept(sortMethod); + close(); + } +} From f655be382090773ad87a74a99685f21b1135a876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 23:24:22 +0400 Subject: [PATCH 099/110] feat(ui): add exit action to sort method selection --- .../java/ru/project/ui/window/SortMethodSelectionWindow.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java index ea234ef..331f430 100644 --- a/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java +++ b/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java @@ -23,6 +23,8 @@ public class SortMethodSelectionWindow extends BaseModalWindow { menu.addItem("Сортировка по чётности", () -> selectSortMethod(SortMethod.PARITY)); + menu.addItem("Выйти", this::close); + setComponent(menu); } From 0f4df4b81157f3b098e1a2f2be8340426b19a5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 23:25:37 +0400 Subject: [PATCH 100/110] feat(ui): add sort field configuration and parity sort settings --- .../main/java/ru/project/model/SortField.java | 7 +++ .../project/ui/menu/SortCollectionMenu.java | 44 +++++++++++++++--- .../ui/window/NormalSortSettingsWindow.java | 46 +++++++++++++++++++ .../ui/window/ParitySortSettingsWindow.java | 24 ++++++++++ 4 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/ru/project/model/SortField.java create mode 100644 app/src/main/java/ru/project/ui/window/NormalSortSettingsWindow.java create mode 100644 app/src/main/java/ru/project/ui/window/ParitySortSettingsWindow.java diff --git a/app/src/main/java/ru/project/model/SortField.java b/app/src/main/java/ru/project/model/SortField.java new file mode 100644 index 0000000..967a3ac --- /dev/null +++ b/app/src/main/java/ru/project/model/SortField.java @@ -0,0 +1,7 @@ +package ru.project.model; + +public enum SortField { + GROUP_NUMBER, + AVERAGE_GRADE, + RECORD_BOOK_NUMBER +} diff --git a/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java index 5c9ea61..577655a 100644 --- a/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java @@ -6,9 +6,12 @@ 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.model.SortField; import ru.project.model.SortMethod; import ru.project.ui.base.BaseModalWindow; import ru.project.ui.window.MessageWindow; +import ru.project.ui.window.NormalSortSettingsWindow; +import ru.project.ui.window.ParitySortSettingsWindow; import ru.project.ui.window.SortMethodSelectionWindow; public class SortCollectionMenu extends BaseModalWindow { @@ -16,9 +19,14 @@ public class SortCollectionMenu extends BaseModalWindow { private final AppState state; private final Label sortMethodLabel; + private final Label sortFieldLabel; + private final SortMethodSelectionWindow sortMethodSelectionWindow; + private final NormalSortSettingsWindow normalSortSettingsWindow; + private final ParitySortSettingsWindow paritySortSettingsWindow; private SortMethod selectedSortMethod; + private SortField selectedSortField; public SortCollectionMenu(WindowBasedTextGUI gui, AppState state) { @@ -26,12 +34,15 @@ public class SortCollectionMenu extends BaseModalWindow { this.state = state; this.selectedSortMethod = SortMethod.NORMAL; + this.selectedSortField = SortField.GROUP_NUMBER; Panel panel = new Panel(); panel.setLayoutManager(new LinearLayout()); this.sortMethodLabel = new Label("Способ сортировки: " + getSortMethodName(selectedSortMethod)); + this.sortFieldLabel = new Label("Сортировать по: " + getSortFieldName(selectedSortField)); + this.sortMethodSelectionWindow = new SortMethodSelectionWindow( gui, @@ -42,7 +53,17 @@ public class SortCollectionMenu extends BaseModalWindow { "Способ сортировки: " + getSortMethodName(selectedSortMethod)); }); + this.normalSortSettingsWindow = + new NormalSortSettingsWindow( + gui, + sortField -> { + selectedSortField = sortField; + }); + + this.paritySortSettingsWindow = new ParitySortSettingsWindow(gui); + panel.addComponent(sortMethodLabel); + panel.addComponent(sortFieldLabel); panel.addComponent(new Button("Выбрать способ сортировки", this::selectSortMethod)); @@ -57,7 +78,11 @@ public class SortCollectionMenu extends BaseModalWindow { } private void setupCurrentSortMethod() { - MessageWindow.showModal(gui, "Настройка способа сортировки пока не реализована."); + switch (selectedSortMethod) { + case NORMAL -> normalSortSettingsWindow.showModal(); + + case PARITY -> paritySortSettingsWindow.showModal(); + } } private void selectSortMethod() { @@ -68,17 +93,14 @@ public class SortCollectionMenu extends BaseModalWindow { if (state.getMainCollection() == null || state.getMainCollection().isEmpty()) { MessageWindow.showModal(gui, "Текущая коллекция пуста."); + return; } switch (selectedSortMethod) { - case NORMAL: - sortNormally(); - break; + case NORMAL -> sortNormally(); - case PARITY: - sortByParity(); - break; + case PARITY -> sortByParity(); } } @@ -98,4 +120,12 @@ public class SortCollectionMenu extends BaseModalWindow { case PARITY -> "Сортировка по чётности"; }; } + + private String getSortFieldName(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/window/NormalSortSettingsWindow.java b/app/src/main/java/ru/project/ui/window/NormalSortSettingsWindow.java new file mode 100644 index 0000000..5b6221b --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/NormalSortSettingsWindow.java @@ -0,0 +1,46 @@ +package ru.project.ui.window; + +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.model.SortField; +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/window/ParitySortSettingsWindow.java b/app/src/main/java/ru/project/ui/window/ParitySortSettingsWindow.java new file mode 100644 index 0000000..1f8db08 --- /dev/null +++ b/app/src/main/java/ru/project/ui/window/ParitySortSettingsWindow.java @@ -0,0 +1,24 @@ +package ru.project.ui.window; + +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); + } +} From 545fae857a3ba4c80b6342e7d019a63465c61b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sat, 29 Aug 2026 23:37:58 +0400 Subject: [PATCH 101/110] refactor(ui): reorganize classes into logical feature packages --- app/src/main/java/ru/project/App.java | 2 +- .../ui/{window => common}/CollectionDisplayWindow.java | 2 +- .../project/ui/{window => common}/InputValueWindow.java | 2 +- .../ru/project/ui/{window => common}/MessageWindow.java | 2 +- .../ui/{menu => creation}/CreateCollectionMenu.java | 8 +++----- .../{window => creation}/CustomDataSettingsWindow.java | 5 ++++- .../ru/project/{model => ui/creation}/FillMethod.java | 2 +- .../ui/{menu => creation}/FillMethodSelectionWindow.java | 3 +-- .../main/java/ru/project/ui/{menu => main}/MainMenu.java | 8 +++++--- .../ui/{window => sorting}/NormalSortSettingsWindow.java | 3 +-- .../ui/{window => sorting}/ParitySortSettingsWindow.java | 2 +- .../project/ui/{menu => sorting}/SortCollectionMenu.java | 9 ++------- .../java/ru/project/{model => ui/sorting}/SortField.java | 2 +- .../ru/project/{model => ui/sorting}/SortMethod.java | 2 +- .../{window => sorting}/SortMethodSelectionWindow.java | 3 +-- 15 files changed, 25 insertions(+), 30 deletions(-) rename app/src/main/java/ru/project/ui/{window => common}/CollectionDisplayWindow.java (98%) rename app/src/main/java/ru/project/ui/{window => common}/InputValueWindow.java (97%) rename app/src/main/java/ru/project/ui/{window => common}/MessageWindow.java (96%) rename app/src/main/java/ru/project/ui/{menu => creation}/CreateCollectionMenu.java (96%) rename app/src/main/java/ru/project/ui/{window => creation}/CustomDataSettingsWindow.java (98%) rename app/src/main/java/ru/project/{model => ui/creation}/FillMethod.java (63%) rename app/src/main/java/ru/project/ui/{menu => creation}/FillMethodSelectionWindow.java (94%) rename app/src/main/java/ru/project/ui/{menu => main}/MainMenu.java (91%) rename app/src/main/java/ru/project/ui/{window => sorting}/NormalSortSettingsWindow.java (95%) rename app/src/main/java/ru/project/ui/{window => sorting}/ParitySortSettingsWindow.java (96%) rename app/src/main/java/ru/project/ui/{menu => sorting}/SortCollectionMenu.java (92%) rename app/src/main/java/ru/project/{model => ui/sorting}/SortField.java (72%) rename app/src/main/java/ru/project/{model => ui/sorting}/SortMethod.java (60%) rename app/src/main/java/ru/project/ui/{window => sorting}/SortMethodSelectionWindow.java (93%) diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java index fa4ee64..73f5efa 100644 --- a/app/src/main/java/ru/project/App.java +++ b/app/src/main/java/ru/project/App.java @@ -4,7 +4,7 @@ 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.menu.MainMenu; +import ru.project.ui.main.MainMenu; public final class App { diff --git a/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java b/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java similarity index 98% rename from app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java rename to app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java index f5da901..1d71808 100644 --- a/app/src/main/java/ru/project/ui/window/CollectionDisplayWindow.java +++ b/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java @@ -1,4 +1,4 @@ -package ru.project.ui.window; +package ru.project.ui.common; import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.gui2.Button; diff --git a/app/src/main/java/ru/project/ui/window/InputValueWindow.java b/app/src/main/java/ru/project/ui/common/InputValueWindow.java similarity index 97% rename from app/src/main/java/ru/project/ui/window/InputValueWindow.java rename to app/src/main/java/ru/project/ui/common/InputValueWindow.java index dc16dbb..f3e3200 100644 --- a/app/src/main/java/ru/project/ui/window/InputValueWindow.java +++ b/app/src/main/java/ru/project/ui/common/InputValueWindow.java @@ -1,4 +1,4 @@ -package ru.project.ui.window; +package ru.project.ui.common; import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.Label; diff --git a/app/src/main/java/ru/project/ui/window/MessageWindow.java b/app/src/main/java/ru/project/ui/common/MessageWindow.java similarity index 96% rename from app/src/main/java/ru/project/ui/window/MessageWindow.java rename to app/src/main/java/ru/project/ui/common/MessageWindow.java index 76ad9a1..3f1905f 100644 --- a/app/src/main/java/ru/project/ui/window/MessageWindow.java +++ b/app/src/main/java/ru/project/ui/common/MessageWindow.java @@ -1,4 +1,4 @@ -package ru.project.ui.window; +package ru.project.ui.common; import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.Label; diff --git a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java similarity index 96% rename from app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java rename to app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java index dd9785a..72820be 100644 --- a/app/src/main/java/ru/project/ui/menu/CreateCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java @@ -1,4 +1,4 @@ -package ru.project.ui.menu; +package ru.project.ui.creation; import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.Label; @@ -8,12 +8,10 @@ import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import ru.project.collection.MyLinkedList; import ru.project.collection.MyList; import ru.project.model.AppState; -import ru.project.model.FillMethod; import ru.project.student.Student; import ru.project.ui.base.BaseModalWindow; -import ru.project.ui.window.CustomDataSettingsWindow; -import ru.project.ui.window.InputValueWindow; -import ru.project.ui.window.MessageWindow; +import ru.project.ui.common.InputValueWindow; +import ru.project.ui.common.MessageWindow; public class CreateCollectionMenu extends BaseModalWindow { diff --git a/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java b/app/src/main/java/ru/project/ui/creation/CustomDataSettingsWindow.java similarity index 98% rename from app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java rename to app/src/main/java/ru/project/ui/creation/CustomDataSettingsWindow.java index 377b417..5f0aa10 100644 --- a/app/src/main/java/ru/project/ui/window/CustomDataSettingsWindow.java +++ b/app/src/main/java/ru/project/ui/creation/CustomDataSettingsWindow.java @@ -1,4 +1,4 @@ -package ru.project.ui.window; +package ru.project.ui.creation; import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.Label; @@ -12,6 +12,9 @@ import ru.project.model.AppState; 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.InputValueWindow; +import ru.project.ui.common.MessageWindow; public class CustomDataSettingsWindow extends BaseModalWindow { diff --git a/app/src/main/java/ru/project/model/FillMethod.java b/app/src/main/java/ru/project/ui/creation/FillMethod.java similarity index 63% rename from app/src/main/java/ru/project/model/FillMethod.java rename to app/src/main/java/ru/project/ui/creation/FillMethod.java index df682af..28567fe 100644 --- a/app/src/main/java/ru/project/model/FillMethod.java +++ b/app/src/main/java/ru/project/ui/creation/FillMethod.java @@ -1,4 +1,4 @@ -package ru.project.model; +package ru.project.ui.creation; public enum FillMethod { CUSTOM, diff --git a/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java similarity index 94% rename from app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java rename to app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java index f408210..9cf0fc7 100644 --- a/app/src/main/java/ru/project/ui/menu/FillMethodSelectionWindow.java +++ b/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java @@ -1,9 +1,8 @@ -package ru.project.ui.menu; +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.model.FillMethod; import ru.project.ui.base.BaseModalWindow; public class FillMethodSelectionWindow extends BaseModalWindow { diff --git a/app/src/main/java/ru/project/ui/menu/MainMenu.java b/app/src/main/java/ru/project/ui/main/MainMenu.java similarity index 91% rename from app/src/main/java/ru/project/ui/menu/MainMenu.java rename to app/src/main/java/ru/project/ui/main/MainMenu.java index 32999d7..ff4899e 100644 --- a/app/src/main/java/ru/project/ui/menu/MainMenu.java +++ b/app/src/main/java/ru/project/ui/main/MainMenu.java @@ -1,4 +1,4 @@ -package ru.project.ui.menu; +package ru.project.ui.main; import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; @@ -6,8 +6,10 @@ import ru.project.collection.MyList; import ru.project.model.AppState; import ru.project.student.Student; import ru.project.ui.base.BaseModalWindow; -import ru.project.ui.window.CollectionDisplayWindow; -import ru.project.ui.window.MessageWindow; +import ru.project.ui.common.CollectionDisplayWindow; +import ru.project.ui.common.MessageWindow; +import ru.project.ui.creation.CreateCollectionMenu; +import ru.project.ui.sorting.SortCollectionMenu; public class MainMenu extends BaseModalWindow { diff --git a/app/src/main/java/ru/project/ui/window/NormalSortSettingsWindow.java b/app/src/main/java/ru/project/ui/sorting/NormalSortSettingsWindow.java similarity index 95% rename from app/src/main/java/ru/project/ui/window/NormalSortSettingsWindow.java rename to app/src/main/java/ru/project/ui/sorting/NormalSortSettingsWindow.java index 5b6221b..0f3bbd2 100644 --- a/app/src/main/java/ru/project/ui/window/NormalSortSettingsWindow.java +++ b/app/src/main/java/ru/project/ui/sorting/NormalSortSettingsWindow.java @@ -1,4 +1,4 @@ -package ru.project.ui.window; +package ru.project.ui.sorting; import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.Button; @@ -6,7 +6,6 @@ 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.model.SortField; import ru.project.ui.base.BaseModalWindow; public class NormalSortSettingsWindow extends BaseModalWindow { diff --git a/app/src/main/java/ru/project/ui/window/ParitySortSettingsWindow.java b/app/src/main/java/ru/project/ui/sorting/ParitySortSettingsWindow.java similarity index 96% rename from app/src/main/java/ru/project/ui/window/ParitySortSettingsWindow.java rename to app/src/main/java/ru/project/ui/sorting/ParitySortSettingsWindow.java index 1f8db08..aa6d090 100644 --- a/app/src/main/java/ru/project/ui/window/ParitySortSettingsWindow.java +++ b/app/src/main/java/ru/project/ui/sorting/ParitySortSettingsWindow.java @@ -1,4 +1,4 @@ -package ru.project.ui.window; +package ru.project.ui.sorting; import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.Label; diff --git a/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/sorting/SortCollectionMenu.java similarity index 92% rename from app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java rename to app/src/main/java/ru/project/ui/sorting/SortCollectionMenu.java index 577655a..ae72b4b 100644 --- a/app/src/main/java/ru/project/ui/menu/SortCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/sorting/SortCollectionMenu.java @@ -1,4 +1,4 @@ -package ru.project.ui.menu; +package ru.project.ui.sorting; import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.Label; @@ -6,13 +6,8 @@ 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.model.SortField; -import ru.project.model.SortMethod; import ru.project.ui.base.BaseModalWindow; -import ru.project.ui.window.MessageWindow; -import ru.project.ui.window.NormalSortSettingsWindow; -import ru.project.ui.window.ParitySortSettingsWindow; -import ru.project.ui.window.SortMethodSelectionWindow; +import ru.project.ui.common.MessageWindow; public class SortCollectionMenu extends BaseModalWindow { diff --git a/app/src/main/java/ru/project/model/SortField.java b/app/src/main/java/ru/project/ui/sorting/SortField.java similarity index 72% rename from app/src/main/java/ru/project/model/SortField.java rename to app/src/main/java/ru/project/ui/sorting/SortField.java index 967a3ac..6219f9b 100644 --- a/app/src/main/java/ru/project/model/SortField.java +++ b/app/src/main/java/ru/project/ui/sorting/SortField.java @@ -1,4 +1,4 @@ -package ru.project.model; +package ru.project.ui.sorting; public enum SortField { GROUP_NUMBER, diff --git a/app/src/main/java/ru/project/model/SortMethod.java b/app/src/main/java/ru/project/ui/sorting/SortMethod.java similarity index 60% rename from app/src/main/java/ru/project/model/SortMethod.java rename to app/src/main/java/ru/project/ui/sorting/SortMethod.java index 8cf4c6e..446f27e 100644 --- a/app/src/main/java/ru/project/model/SortMethod.java +++ b/app/src/main/java/ru/project/ui/sorting/SortMethod.java @@ -1,4 +1,4 @@ -package ru.project.model; +package ru.project.ui.sorting; public enum SortMethod { NORMAL, diff --git a/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/sorting/SortMethodSelectionWindow.java similarity index 93% rename from app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java rename to app/src/main/java/ru/project/ui/sorting/SortMethodSelectionWindow.java index 331f430..121e5e2 100644 --- a/app/src/main/java/ru/project/ui/window/SortMethodSelectionWindow.java +++ b/app/src/main/java/ru/project/ui/sorting/SortMethodSelectionWindow.java @@ -1,9 +1,8 @@ -package ru.project.ui.window; +package ru.project.ui.sorting; import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import java.util.function.Consumer; -import ru.project.model.SortMethod; import ru.project.ui.base.BaseModalWindow; public class SortMethodSelectionWindow extends BaseModalWindow { From 8783308efb3f62be6dc0cf1e629aa4421ec8ba1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D1=8E=D1=80=D1=8F=D0=B3=D0=B8=D0=BD=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB?= Date: Sun, 30 Aug 2026 11:43:27 +0300 Subject: [PATCH 102/110] test: add unit tests for student search --- .../ru/project/search/StudentSearchTest.java | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 app/src/test/java/ru/project/search/StudentSearchTest.java 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..d12bd9c --- /dev/null +++ b/app/src/test/java/ru/project/search/StudentSearchTest.java @@ -0,0 +1,314 @@ +package ru.project.search; + +import org.junit.jupiter.api.Test; +import ru.project.list.MyList; +import ru.project.list.MyLinkedList; +import ru.project.student.Student; + +import static org.junit.jupiter.api.Assertions.*; + +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", 5.1, 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); + assertFalse(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()); + } +} \ No newline at end of file From e20820b78f8009f78f2a8acd69d1237f65b2dee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sun, 30 Aug 2026 13:03:23 +0400 Subject: [PATCH 103/110] refactor: unify code style and fix minor merge issues --- .../project/sorting/BubbleSortStrategy.java | 34 +- .../sorting/EvenOnlyMergeSortStrategy.java | 130 ++-- .../project/sorting/EvenOnlySortStrategy.java | 141 +++-- .../ru/project/sorting/MergeSortStrategy.java | 111 ++-- .../java/ru/project/sorting/SortStrategy.java | 7 +- .../StudentAverageGradeComparator.java | 12 +- .../sorting/StudentGroupComparator.java | 10 +- .../sorting/StudentRecordBookComparator.java | 12 +- .../main/java/ru/project/student/Student.java | 4 + app/src/test/java/ru/project/AppTest.java | 16 +- .../ru/project/Sorting/SortStrategyTest.java | 163 ----- .../sorting/EvenOnlySortStrategyTest.java | 561 +++++++++--------- .../ru/project/sorting/SortStrategyTest.java | 162 +++++ .../java/ru/project/student/StudentTest.java | 222 ++++--- 14 files changed, 779 insertions(+), 806 deletions(-) delete mode 100644 app/src/test/java/ru/project/Sorting/SortStrategyTest.java create mode 100644 app/src/test/java/ru/project/sorting/SortStrategyTest.java diff --git a/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java index 38172db..3c482eb 100644 --- a/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java +++ b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java @@ -1,24 +1,24 @@ package ru.project.sorting; -import ru.project.list.MyList; 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); - } - } - } + @Override + public void sort(MyList list, Comparator comparator) { + if (list == null || list.size() <= 1) { + return; } -} \ No newline at end of file + 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/EvenOnlyMergeSortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java index dea539e..021a70e 100644 --- a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java +++ b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java @@ -1,84 +1,84 @@ package ru.project.sorting; -import ru.project.list.MyList; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.function.ToIntFunction; +import ru.project.collection.MyList; public class EvenOnlyMergeSortStrategy implements SortStrategy { - private final ToIntFunction valueExtractor; + private final ToIntFunction valueExtractor; - public EvenOnlyMergeSortStrategy(ToIntFunction valueExtractor) { - this.valueExtractor = valueExtractor; + public EvenOnlyMergeSortStrategy(ToIntFunction valueExtractor) { + this.valueExtractor = valueExtractor; + } + + @Override + public void sort(MyList list, Comparator comparator) { + if (list == null || list.size() <= 1) { + return; } - @Override - public void sort(MyList list, Comparator comparator) { - if (list == null || list.size() <= 1) { - return; - } + List evenIndices = new ArrayList<>(); + List evenElements = new ArrayList<>(); - List evenIndices = new ArrayList<>(); - List evenElements = new ArrayList<>(); - - for (int i = 0; i < list.size(); i++) { - T element = list.get(i); - int value = valueExtractor.applyAsInt(element); - if (value % 2 == 0) { - evenIndices.add(i); - evenElements.add(element); - } - } - - if (evenElements.size() <= 1) { - return; - } - - List sortedEven = mergeSort(evenElements, comparator); - - for (int j = 0; j < evenIndices.size(); j++) { - list.set(evenIndices.get(j), sortedEven.get(j)); - } + for (int i = 0; i < list.size(); i++) { + T element = list.get(i); + int value = valueExtractor.applyAsInt(element); + if (value % 2 == 0) { + evenIndices.add(i); + evenElements.add(element); + } } - private List mergeSort(List list, Comparator comparator) { - if (list.size() <= 1) { - return list; - } - - int mid = list.size() / 2; - List left = mergeSort(list.subList(0, mid), comparator); - List right = mergeSort(list.subList(mid, list.size()), comparator); - - return merge(left, right, comparator); + if (evenElements.size() <= 1) { + return; } - private List merge(List left, List right, Comparator comparator) { - List result = new ArrayList<>(); - int i = 0, j = 0; + List sortedEven = mergeSort(evenElements, comparator); - while (i < left.size() && j < right.size()) { - if (comparator.compare(left.get(i), right.get(j)) <= 0) { - result.add(left.get(i)); - i++; - } else { - result.add(right.get(j)); - j++; - } - } - - while (i < left.size()) { - result.add(left.get(i)); - i++; - } - - while (j < right.size()) { - result.add(right.get(j)); - j++; - } - - return result; + for (int j = 0; j < evenIndices.size(); j++) { + list.set(evenIndices.get(j), sortedEven.get(j)); } -} \ No newline at end of file + } + + private List mergeSort(List list, Comparator comparator) { + if (list.size() <= 1) { + return list; + } + + int mid = list.size() / 2; + List left = mergeSort(list.subList(0, mid), comparator); + List right = mergeSort(list.subList(mid, list.size()), comparator); + + return merge(left, right, comparator); + } + + private List merge(List left, List right, Comparator comparator) { + List result = new ArrayList<>(); + int i = 0, j = 0; + + while (i < left.size() && j < right.size()) { + if (comparator.compare(left.get(i), right.get(j)) <= 0) { + result.add(left.get(i)); + i++; + } else { + result.add(right.get(j)); + j++; + } + } + + while (i < left.size()) { + result.add(left.get(i)); + i++; + } + + while (j < right.size()) { + result.add(right.get(j)); + j++; + } + + return result; + } +} diff --git a/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java index 32349c1..8d179ec 100644 --- a/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java +++ b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java @@ -1,94 +1,89 @@ package ru.project.sorting; -import ru.project.collection.MyLinkedList; -import ru.project.collection.MyList; - 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; + private SortStrategy sortStrategy; + private ToIntFunction valueExtractor; - public EvenOnlySortStrategy() { + 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"); } - public EvenOnlySortStrategy(SortStrategy sortStrategy) { - setSortStrategy(sortStrategy); + this.sortStrategy = sortStrategy; + } + + public void setValueExtractor(ToIntFunction valueExtractor) { + if (valueExtractor == null) { + throw new IllegalArgumentException("Value extractor must not be null"); } - public EvenOnlySortStrategy( - SortStrategy sortStrategy, - ToIntFunction valueExtractor - ) { - setSortStrategy(sortStrategy); - setValueExtractor(valueExtractor); + this.valueExtractor = valueExtractor; + } + + @Override + public void sort(MyList list, Comparator comparator) { + if (sortStrategy == null) { + throw new IllegalStateException("Sort strategy is not set"); } - public void setSortStrategy(SortStrategy sortStrategy) { - if (sortStrategy == null) { - throw new IllegalArgumentException("Sort strategy must not be null"); - } - - this.sortStrategy = sortStrategy; + if (valueExtractor == null) { + throw new IllegalStateException("Value extractor is not set"); } - public void setValueExtractor(ToIntFunction valueExtractor) { - if (valueExtractor == null) { - throw new IllegalArgumentException("Value extractor must not be null"); - } - - this.valueExtractor = valueExtractor; + if (list == null || list.size() <= 1) { + return; } - @Override - public void sort(MyList list, Comparator comparator) { - if (sortStrategy == null) { - throw new IllegalStateException("Sort strategy is not set"); - } + int evenCount = 0; - if (valueExtractor == null) { - throw new IllegalStateException("Value extractor is not set"); - } + for (int i = 0; i < list.size(); i++) { + T element = list.get(i); - 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)); - } + if (valueExtractor.applyAsInt(element) % 2 == 0) { + evenCount++; + } } -} \ No newline at end of file + + 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 index 598f754..420adee 100644 --- a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java +++ b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java @@ -1,67 +1,66 @@ package ru.project.sorting; -import ru.project.collection.MyList; - 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); + @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); } - 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); + 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++; } - 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++; - } + while (i < leftSize) { + list.set(k, leftArray[i]); + i++; + k++; } -} \ No newline at end of file + 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 index 059d908..b9946e8 100644 --- a/app/src/main/java/ru/project/sorting/SortStrategy.java +++ b/app/src/main/java/ru/project/sorting/SortStrategy.java @@ -1,9 +1,8 @@ package ru.project.sorting; +import java.util.Comparator; import ru.project.collection.MyList; -import java.util.Comparator; - public interface SortStrategy { - void sort(MyList list, Comparator comparator); -} \ No newline at end of file + 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 index fea72f6..3553976 100644 --- a/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java @@ -1,12 +1,12 @@ package ru.project.sorting; -import ru.project.student.Student; 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()); - } -} \ No newline at end of file + @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 index 75cae82..9278e81 100644 --- a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java @@ -1,12 +1,12 @@ package ru.project.sorting; -import ru.project.student.Student; 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()); - } + @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 index 0e4a714..ce65a5b 100644 --- a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java +++ b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java @@ -1,12 +1,12 @@ package ru.project.sorting; -import ru.project.student.Student; 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()); - } -} \ No newline at end of file + @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 index f4cd7fc..111e559 100644 --- a/app/src/main/java/ru/project/student/Student.java +++ b/app/src/main/java/ru/project/student/Student.java @@ -54,6 +54,10 @@ public class Student { 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; } diff --git a/app/src/test/java/ru/project/AppTest.java b/app/src/test/java/ru/project/AppTest.java index c79d156..44a05ef 100644 --- a/app/src/test/java/ru/project/AppTest.java +++ b/app/src/test/java/ru/project/AppTest.java @@ -4,16 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -/** - * Unit test for simple App. - */ +/** Unit test for simple App. */ public class AppTest { - /** - * Rigorous Test :-) - */ - @Test - public void shouldAnswerWithTrue() { - assertTrue(true); - } + /** Rigorous Test :-) */ + @Test + public void shouldAnswerWithTrue() { + assertTrue(true); + } } diff --git a/app/src/test/java/ru/project/Sorting/SortStrategyTest.java b/app/src/test/java/ru/project/Sorting/SortStrategyTest.java deleted file mode 100644 index 6e0472c..0000000 --- a/app/src/test/java/ru/project/Sorting/SortStrategyTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package ru.project.sorting; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.project.list.MyList; -import ru.project.list.MyLinkedList; -import ru.project.student.Student; - -import java.util.Comparator; - -import static org.junit.jupiter.api.Assertions.*; - -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)); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(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)); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(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)); - - EvenOnlyMergeSortStrategy strategy = - new EvenOnlyMergeSortStrategy<>(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()); - } - } -} \ No newline at end of file diff --git a/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java index 8d482bd..daccad5 100644 --- a/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java +++ b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java @@ -1,301 +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; -import static org.junit.jupiter.api.Assertions.assertSame; - -import java.util.Comparator; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - class EvenOnlySortStrategyTest { - @Test - void shouldHandleEmptyList() { - MyList list = new MyLinkedList<>(); + @Test + void shouldHandleEmptyList() { + MyList list = new MyLinkedList<>(); - EvenOnlySortStrategy strategy = - new EvenOnlySortStrategy<>( - new MergeSortStrategy<>(), - value -> value - ); + EvenOnlySortStrategy strategy = + new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value); - strategy.sort(list, Integer::compare); + strategy.sort(list, Integer::compare); - assertEquals(0, list.size()); + 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); + } } - - @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); - } - } - } -} \ No newline at end of file + } +} 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..7a6724c --- /dev/null +++ b/app/src/test/java/ru/project/sorting/SortStrategyTest.java @@ -0,0 +1,162 @@ +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)); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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)); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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)); + + EvenOnlyMergeSortStrategy strategy = + new EvenOnlyMergeSortStrategy<>(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 index 09cf2e7..a51e251 100644 --- a/app/src/test/java/ru/project/student/StudentTest.java +++ b/app/src/test/java/ru/project/student/StudentTest.java @@ -1,145 +1,141 @@ package ru.project.student; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; 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 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 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 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 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 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 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 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"); + @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()); + assertNotSame(original, updated); + assertEquals("B32", updated.getGroupNumber()); + assertEquals(4.5, updated.getAverageGrade()); + assertEquals(12345, updated.getRecordBookNumber()); - assertEquals("A12", original.getGroupNumber()); - } + assertEquals("A12", original.getGroupNumber()); + } - @Test - void testWithAverageGrade() { - Student original = new Student("A12", 4.5, 12345); - Student updated = original.withAverageGrade(4.8); + @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()); + assertNotSame(original, updated); + assertEquals(4.8, updated.getAverageGrade()); + assertEquals("A12", updated.getGroupNumber()); + assertEquals(12345, updated.getRecordBookNumber()); - assertEquals(4.5, original.getAverageGrade()); - } + assertEquals(4.5, original.getAverageGrade()); + } - @Test - void testWithRecordBookNumber() { - Student original = new Student("A12", 4.5, 12345); - Student updated = original.withRecordBookNumber(99999); + @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()); + assertNotSame(original, updated); + assertEquals(99999, updated.getRecordBookNumber()); + assertEquals("A12", updated.getGroupNumber()); + assertEquals(4.5, updated.getAverageGrade()); - assertEquals(12345, original.getRecordBookNumber()); - } + 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 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 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 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); + @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()); + assertEquals(s1, s2); + assertEquals(s1.hashCode(), s2.hashCode()); - assertNotEquals(s1, s3); - assertNotEquals(s1, s4); - assertNotEquals(s1, s5); - assertNotEquals(s1, null); - assertNotEquals(s1, "some string"); - } + assertNotEquals(s1, s3); + assertNotEquals(s1, s4); + assertNotEquals(s1, s5); + assertNotEquals(s1, null); + assertNotEquals(s1, "some string"); + } } From e77b8c5956c0770765f8a8101a3988bd3a943ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Sun, 30 Aug 2026 14:49:34 +0400 Subject: [PATCH 104/110] docs: add Git Flow tutorial and organize Git guides --- .../git/GIT_BRANCH_DEPENDENCIES.ru.md | 0 .../git/GIT_CONFIG.ru.md | 0 tutorials/git/GIT_FLOW.ru.md | 1201 +++++++++++++++++ .../git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md | 0 4 files changed, 1201 insertions(+) rename GIT_BRANCH_DEPENDENCIES.ru.md => tutorials/git/GIT_BRANCH_DEPENDENCIES.ru.md (100%) rename GIT_CONFIG.ru.md => tutorials/git/GIT_CONFIG.ru.md (100%) create mode 100644 tutorials/git/GIT_FLOW.ru.md rename GIT_FORCE_WITH_LEASE_RECOVERY.ru.md => tutorials/git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md (100%) 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 From c5cf12827a2c8395587ec841220cdd995d3839e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 31 Aug 2026 00:35:45 +0300 Subject: [PATCH 105/110] feat: added the get-all method and tests --- .../ru/project/collection/MyLinkedList.java | 8 ++ .../java/ru/project/collection/MyList.java | 1 + .../project/collection/MyLinkedListTest.java | 130 ++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 app/src/test/java/ru/project/collection/MyLinkedListTest.java diff --git a/app/src/main/java/ru/project/collection/MyLinkedList.java b/app/src/main/java/ru/project/collection/MyLinkedList.java index 23bbad0..a919309 100644 --- a/app/src/main/java/ru/project/collection/MyLinkedList.java +++ b/app/src/main/java/ru/project/collection/MyLinkedList.java @@ -79,6 +79,14 @@ public class MyLinkedList implements MyList { public boolean isEmpty() { return size == 0; } + 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() { diff --git a/app/src/main/java/ru/project/collection/MyList.java b/app/src/main/java/ru/project/collection/MyList.java index bd47567..27af25d 100644 --- a/app/src/main/java/ru/project/collection/MyList.java +++ b/app/src/main/java/ru/project/collection/MyList.java @@ -13,4 +13,5 @@ public interface MyList { int size(); boolean isEmpty(); + void addAll(MyList collection); } 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..6c43673 --- /dev/null +++ b/app/src/test/java/ru/project/collection/MyLinkedListTest.java @@ -0,0 +1,130 @@ +package ru.test.maven.spring; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +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)); + } +} From 6c97f036d84039b7a1c84c2ca8f0e66ae1a40d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Mon, 31 Aug 2026 11:22:30 +0400 Subject: [PATCH 106/110] docs: add random student generator task --- docs/tasks/RandomStudentGenerator.md | 305 +++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 docs/tasks/RandomStudentGenerator.md 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` для выполнения данного задания не требуется. From 36baf5449842f01e4e551d42473bde4d68361052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 31 Aug 2026 00:47:49 +0300 Subject: [PATCH 107/110] refactor : changed the combiner --- .../ru/project/collectors/MyCollectors.java | 15 +++++ .../project/collectors/MyListCollector.java | 43 +++++++++++++ .../collectors/MyListCollectorTest.java | 64 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 app/src/main/java/ru/project/collectors/MyCollectors.java create mode 100644 app/src/main/java/ru/project/collectors/MyListCollector.java create mode 100644 app/src/test/java/ru/project/collectors/MyListCollectorTest.java 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..d0d7cf5 --- /dev/null +++ b/app/src/main/java/ru/project/collectors/MyCollectors.java @@ -0,0 +1,15 @@ +package ru.project.collectors; +import java.util.stream.Collector; + +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..bb168e4 --- /dev/null +++ b/app/src/main/java/ru/project/collectors/MyListCollector.java @@ -0,0 +1,43 @@ +package ru.project.collectors; +import java.util.Collections; +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; +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/test/java/ru/project/collectors/MyListCollectorTest.java b/app/src/test/java/ru/project/collectors/MyListCollectorTest.java new file mode 100644 index 0000000..3ca8d21 --- /dev/null +++ b/app/src/test/java/ru/project/collectors/MyListCollectorTest.java @@ -0,0 +1,64 @@ +package ru.test.maven.spring; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.IntStream; +import java.util.Collections; + +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)); + } + +} From ebaadf4fa18a6df33225aefdc36aa8fd66c16c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=B0=D0=B4=D0=B6=D0=B8=D0=BA=D1=83=D1=80=D0=B1?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=A2=D0=B5=D0=BC=D0=B8=D1=80=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 31 Aug 2026 00:57:23 +0300 Subject: [PATCH 108/110] feat: add StudentOccurrenceIndexFinder for occurrence search and test --- .../finder/StudentOccurrenceIndexFinder.java | 125 ++++++++++++++++ .../StudentOccurrenceIndexFinderTest.java | 134 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 app/src/main/java/ru/project/finder/StudentOccurrenceIndexFinder.java create mode 100644 app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java 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..7e7c3d2 --- /dev/null +++ b/app/src/main/java/ru/project/finder/StudentOccurrenceIndexFinder.java @@ -0,0 +1,125 @@ +package ru.project.finder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +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/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java b/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java new file mode 100644 index 0000000..b61ed58 --- /dev/null +++ b/app/src/test/java/ru/project/finder/StudentOccurrenceIndexFinderTest.java @@ -0,0 +1,134 @@ +package ru.project.finder; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +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], "Результат должен быть отсортирован"); + } + } +} From ac8792eda4ed962d3ae9ab674c9a798b1a0b1614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Mon, 31 Aug 2026 21:57:38 +0400 Subject: [PATCH 109/110] docs: update csv student writer contract --- docs/tasks/CsvStudentIO.md | 169 +++++++++++++++++++++++++++++++------ 1 file changed, 143 insertions(+), 26 deletions(-) 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`. Файл создавать запрещено. --- From af233e92624f83bfb26cd031996ad27e0b4a9923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 1 Sep 2026 15:47:45 +0400 Subject: [PATCH 110/110] chore: remove obsolete application and collector classes --- app/src/main/java/ru/project/App.java | 45 ------------- .../main/java/ru/project/MyCollectors.java | 10 --- .../main/java/ru/project/MyListCollector.java | 41 ------------ app/src/test/java/ru/project/AppTest.java | 15 ----- .../java/ru/project/MyListCollectorTest.java | 63 ------------------- 5 files changed, 174 deletions(-) delete mode 100644 app/src/main/java/ru/project/App.java delete mode 100644 app/src/main/java/ru/project/MyCollectors.java delete mode 100644 app/src/main/java/ru/project/MyListCollector.java delete mode 100644 app/src/test/java/ru/project/AppTest.java delete mode 100644 app/src/test/java/ru/project/MyListCollectorTest.java diff --git a/app/src/main/java/ru/project/App.java b/app/src/main/java/ru/project/App.java deleted file mode 100644 index 73f5efa..0000000 --- a/app/src/main/java/ru/project/App.java +++ /dev/null @@ -1,45 +0,0 @@ -package ru.project; - -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.main.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/MyCollectors.java b/app/src/main/java/ru/project/MyCollectors.java deleted file mode 100644 index 20840c3..0000000 --- a/app/src/main/java/ru/project/MyCollectors.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.test.maven.spring; -import java.util.stream.Collector; -public class MyCollectors -{ - private MyCollectors() { - } - public static Collector, MyList> toMyList() { - return new MyListCollector<>(); - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/project/MyListCollector.java b/app/src/main/java/ru/project/MyListCollector.java deleted file mode 100644 index f731379..0000000 --- a/app/src/main/java/ru/project/MyListCollector.java +++ /dev/null @@ -1,41 +0,0 @@ -package ru.test.maven.spring; -import java.util.Collections; -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; -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) -> { - for (int i = 0; i < list2.size(); i++) { - list1.add(list2.get(i)); - } - return list1; - }; - } - @Override - public Function, MyList> finisher() - { - return list -> list; - } - @Override - public Set characteristics() { - return java.util.Set.of( - Collector.Characteristics.IDENTITY_FINISH - ); - } -} \ No newline at end of file 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 44a05ef..0000000 --- a/app/src/test/java/ru/project/AppTest.java +++ /dev/null @@ -1,15 +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/MyListCollectorTest.java b/app/src/test/java/ru/project/MyListCollectorTest.java deleted file mode 100644 index 6155d57..0000000 --- a/app/src/test/java/ru/project/MyListCollectorTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package ru.test.maven.spring; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; -import java.util.Set; -import java.util.stream.Collector; -import java.util.stream.IntStream; -import java.util.Collections; -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)); - } - -}