diff --git a/app/pom.xml b/app/pom.xml
index 9caf260..3555da9 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -41,11 +41,19 @@
junit-jupiter-params
test
+
org.apache.commons
commons-csv
1.14.1
+
+
+ com.googlecode.lanterna
+ lanterna
+ 3.1.5
+
+
@@ -96,5 +104,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
deleted file mode 100644
index 8fd4c39..0000000
--- a/app/src/main/java/ru/project/App.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package ru.project;
-
-/**
- * Hello world!
- */
-public class App {
- public static void main(String[] args) {
- System.out.println("Hello World!");
- }
-}
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..a919309
--- /dev/null
+++ b/app/src/main/java/ru/project/collection/MyLinkedList.java
@@ -0,0 +1,128 @@
+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++;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+
+ public void remove(int index) {
+ if (index < 0 || index >= size) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (index == 0) {
+ head = head.getNext();
+ size--;
+ return;
+ }
+ int currentIndex = 0;
+ Node temp = head;
+ while (temp != null) {
+ if (currentIndex == index - 1) {
+ temp.setNext(temp.getNext().getNext());
+ size--;
+ return;
+ } else {
+ temp = temp.getNext();
+ currentIndex++;
+ }
+ }
+ }
+
+ public T set(int index, T value) {
+ if (index < 0 || index >= size) {
+ throw new IllegalArgumentException();
+ }
+
+ Node temp = head;
+ for (int i = 0; i < index; i++) {
+ temp = temp.getNext();
+ }
+
+ T oldValue = temp.getValue();
+ temp.setValue(value);
+ return oldValue;
+ }
+
+ public int size() {
+ return size;
+ }
+
+ public boolean isEmpty() {
+ return size == 0;
+ }
+ public void addAll(MyList extends T> collection) {
+ if (collection == null || collection.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < collection.size(); i++) {
+ add(collection.get(i));
+ }
+ }
+
+ @Override
+ public String toString() {
+ Object[] res = new Object[size];
+ int index = 0;
+ Node temp = head;
+ while (temp != null) {
+ res[index] = temp.getValue();
+ index++;
+ temp = temp.getNext();
+ }
+ return Arrays.toString(res);
+ }
+
+ private static class Node {
+ private T value;
+ private Node next;
+
+ public Node(T value) {
+ this.value = value;
+ }
+
+ public void setValue(T value) {
+ this.value = value;
+ }
+
+ public void setNext(Node next) {
+ this.next = next;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public Node getNext() {
+ return next;
+ }
+ }
+}
diff --git a/app/src/main/java/ru/project/collection/MyList.java b/app/src/main/java/ru/project/collection/MyList.java
new file mode 100644
index 0000000..27af25d
--- /dev/null
+++ b/app/src/main/java/ru/project/collection/MyList.java
@@ -0,0 +1,17 @@
+package ru.project.collection;
+
+public interface MyList {
+
+ void add(T element);
+
+ T get(int index);
+
+ T set(int index, T element);
+
+ void remove(int index);
+
+ int size();
+
+ boolean isEmpty();
+ void addAll(MyList extends T> collection);
+}
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/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/main/java/ru/project/model/AppState.java b/app/src/main/java/ru/project/model/AppState.java
new file mode 100644
index 0000000..375877e
--- /dev/null
+++ b/app/src/main/java/ru/project/model/AppState.java
@@ -0,0 +1,43 @@
+package ru.project.model;
+
+import ru.project.collection.MyLinkedList;
+import ru.project.collection.MyList;
+import ru.project.student.Student;
+
+public class AppState {
+
+ private MyList mainCollection;
+ private MyList customDataCollection;
+ private String currentFilePath;
+
+ public AppState() {
+ this.mainCollection = new MyLinkedList<>();
+ this.customDataCollection = new MyLinkedList<>();
+ this.currentFilePath = null;
+ }
+
+ public MyList getMainCollection() {
+ return mainCollection;
+ }
+
+ public void setMainCollection(MyList currentCollection) {
+ this.mainCollection = currentCollection;
+ }
+
+ public MyList getCustomDataCollection() {
+ return customDataCollection;
+ }
+
+ public void setCustomDataCollection(MyList customDataCollection) {
+
+ this.customDataCollection = customDataCollection;
+ }
+
+ public String getCurrentFilePath() {
+ return currentFilePath;
+ }
+
+ public void setCurrentFilePath(String currentFilePath) {
+ this.currentFilePath = currentFilePath;
+ }
+}
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
diff --git a/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java
new file mode 100644
index 0000000..3c482eb
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/BubbleSortStrategy.java
@@ -0,0 +1,24 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import ru.project.collection.MyList;
+
+public class BubbleSortStrategy implements SortStrategy {
+
+ @Override
+ public void sort(MyList list, Comparator comparator) {
+ if (list == null || list.size() <= 1) {
+ return;
+ }
+ int n = list.size();
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = 0; j < n - i - 1; j++) {
+ if (comparator.compare(list.get(j), list.get(j + 1)) > 0) {
+ T temp = list.get(j);
+ list.set(j, list.get(j + 1));
+ list.set(j + 1, temp);
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java
new file mode 100644
index 0000000..021a70e
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/EvenOnlyMergeSortStrategy.java
@@ -0,0 +1,84 @@
+package ru.project.sorting;
+
+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;
+
+ 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;
+ }
+}
diff --git a/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java
new file mode 100644
index 0000000..8d179ec
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/EvenOnlySortStrategy.java
@@ -0,0 +1,89 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import java.util.function.ToIntFunction;
+import ru.project.collection.MyLinkedList;
+import ru.project.collection.MyList;
+
+public class EvenOnlySortStrategy implements SortStrategy {
+
+ private SortStrategy sortStrategy;
+ private ToIntFunction valueExtractor;
+
+ public EvenOnlySortStrategy() {}
+
+ public EvenOnlySortStrategy(SortStrategy sortStrategy) {
+ setSortStrategy(sortStrategy);
+ }
+
+ public EvenOnlySortStrategy(SortStrategy sortStrategy, ToIntFunction valueExtractor) {
+ setSortStrategy(sortStrategy);
+ setValueExtractor(valueExtractor);
+ }
+
+ public void setSortStrategy(SortStrategy sortStrategy) {
+ if (sortStrategy == null) {
+ throw new IllegalArgumentException("Sort strategy must not be null");
+ }
+
+ this.sortStrategy = sortStrategy;
+ }
+
+ public void setValueExtractor(ToIntFunction valueExtractor) {
+ if (valueExtractor == null) {
+ throw new IllegalArgumentException("Value extractor must not be null");
+ }
+
+ this.valueExtractor = valueExtractor;
+ }
+
+ @Override
+ public void sort(MyList list, Comparator comparator) {
+ if (sortStrategy == null) {
+ throw new IllegalStateException("Sort strategy is not set");
+ }
+
+ if (valueExtractor == null) {
+ throw new IllegalStateException("Value extractor is not set");
+ }
+
+ if (list == null || list.size() <= 1) {
+ return;
+ }
+
+ int evenCount = 0;
+
+ for (int i = 0; i < list.size(); i++) {
+ T element = list.get(i);
+
+ if (valueExtractor.applyAsInt(element) % 2 == 0) {
+ evenCount++;
+ }
+ }
+
+ if (evenCount <= 1) {
+ return;
+ }
+
+ MyList evenElements = new MyLinkedList<>();
+ int[] evenIndexes = new int[evenCount];
+
+ int evenIndex = 0;
+
+ for (int i = 0; i < list.size(); i++) {
+ T element = list.get(i);
+
+ if (valueExtractor.applyAsInt(element) % 2 == 0) {
+ evenElements.add(element);
+ evenIndexes[evenIndex] = i;
+ evenIndex++;
+ }
+ }
+
+ sortStrategy.sort(evenElements, comparator);
+
+ for (int i = 0; i < evenIndexes.length; i++) {
+ list.set(evenIndexes[i], evenElements.get(i));
+ }
+ }
+}
diff --git a/app/src/main/java/ru/project/sorting/MergeSortStrategy.java b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java
new file mode 100644
index 0000000..420adee
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/MergeSortStrategy.java
@@ -0,0 +1,66 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import ru.project.collection.MyList;
+
+public class MergeSortStrategy implements SortStrategy {
+
+ @Override
+ public void sort(MyList list, Comparator comparator) {
+ if (list == null || list.size() <= 1) {
+ return;
+ }
+ mergeSort(list, 0, list.size() - 1, comparator);
+ }
+
+ private void mergeSort(MyList list, int left, int right, Comparator comparator) {
+ if (left >= right) {
+ return;
+ }
+ int mid = (left + right) / 2;
+ mergeSort(list, left, mid, comparator);
+ mergeSort(list, mid + 1, right, comparator);
+ merge(list, left, mid, right, comparator);
+ }
+
+ private void merge(MyList list, int left, int mid, int right, Comparator comparator) {
+ int leftSize = mid - left + 1;
+ int rightSize = right - mid;
+
+ @SuppressWarnings("unchecked")
+ T[] leftArray = (T[]) new Object[leftSize];
+ @SuppressWarnings("unchecked")
+ T[] rightArray = (T[]) new Object[rightSize];
+
+ for (int i = 0; i < leftSize; i++) {
+ leftArray[i] = list.get(left + i);
+ }
+ for (int j = 0; j < rightSize; j++) {
+ rightArray[j] = list.get(mid + 1 + j);
+ }
+
+ int i = 0, j = 0, k = left;
+
+ while (i < leftSize && j < rightSize) {
+ if (comparator.compare(leftArray[i], rightArray[j]) <= 0) {
+ list.set(k, leftArray[i]);
+ i++;
+ } else {
+ list.set(k, rightArray[j]);
+ j++;
+ }
+ k++;
+ }
+
+ while (i < leftSize) {
+ list.set(k, leftArray[i]);
+ i++;
+ k++;
+ }
+ while (j < rightSize) {
+ list.set(k, rightArray[j]);
+ j++;
+ k++;
+ }
+ }
+}
diff --git a/app/src/main/java/ru/project/sorting/SortStrategy.java b/app/src/main/java/ru/project/sorting/SortStrategy.java
new file mode 100644
index 0000000..b9946e8
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/SortStrategy.java
@@ -0,0 +1,8 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import ru.project.collection.MyList;
+
+public interface SortStrategy {
+ void sort(MyList list, Comparator comparator);
+}
diff --git a/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java
new file mode 100644
index 0000000..3553976
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/StudentAverageGradeComparator.java
@@ -0,0 +1,12 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import ru.project.student.Student;
+
+public class StudentAverageGradeComparator implements Comparator {
+
+ @Override
+ public int compare(Student s1, Student s2) {
+ return Double.compare(s1.getAverageGrade(), s2.getAverageGrade());
+ }
+}
diff --git a/app/src/main/java/ru/project/sorting/StudentGroupComparator.java b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java
new file mode 100644
index 0000000..9278e81
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/StudentGroupComparator.java
@@ -0,0 +1,12 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import ru.project.student.Student;
+
+public class StudentGroupComparator implements Comparator {
+
+ @Override
+ public int compare(Student s1, Student s2) {
+ return s1.getGroupNumber().compareTo(s2.getGroupNumber());
+ }
+}
diff --git a/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java
new file mode 100644
index 0000000..ce65a5b
--- /dev/null
+++ b/app/src/main/java/ru/project/sorting/StudentRecordBookComparator.java
@@ -0,0 +1,12 @@
+package ru.project.sorting;
+
+import java.util.Comparator;
+import ru.project.student.Student;
+
+public class StudentRecordBookComparator implements Comparator {
+
+ @Override
+ public int compare(Student s1, Student s2) {
+ return Integer.compare(s1.getRecordBookNumber(), s2.getRecordBookNumber());
+ }
+}
diff --git a/app/src/main/java/ru/project/student/Student.java b/app/src/main/java/ru/project/student/Student.java
new file mode 100644
index 0000000..111e559
--- /dev/null
+++ b/app/src/main/java/ru/project/student/Student.java
@@ -0,0 +1,111 @@
+package ru.project.student;
+
+import java.util.Objects;
+
+public class Student {
+
+ public static final double MIN_AVERAGE_GRADE = 0.0;
+ public static final double MAX_AVERAGE_GRADE = 5.0;
+ public static final int MIN_RECORD_BOOK_NUMBER = 1;
+ public static final String GROUP_NUMBER_PATTERN = "^[A-Z][0-9]{2}$";
+
+ private final String groupNumber;
+ private final double averageGrade;
+ private final int recordBookNumber;
+
+ public Student(String groupNumber, double averageGrade, int recordBookNumber) {
+ if (groupNumber == null || !groupNumber.matches(GROUP_NUMBER_PATTERN)) {
+ throw new IllegalArgumentException(
+ "Group number must match pattern: " + GROUP_NUMBER_PATTERN);
+ }
+ if (averageGrade < MIN_AVERAGE_GRADE || averageGrade > MAX_AVERAGE_GRADE) {
+ throw new IllegalArgumentException(
+ "Average grade must be between " + MIN_AVERAGE_GRADE + " and " + MAX_AVERAGE_GRADE);
+ }
+ if (recordBookNumber <= 0) {
+ throw new IllegalArgumentException("Record book number must be positive");
+ }
+ if (recordBookNumber < MIN_RECORD_BOOK_NUMBER) {
+ throw new IllegalArgumentException(
+ "Record book number must be at least " + MIN_RECORD_BOOK_NUMBER);
+ }
+ this.groupNumber = groupNumber;
+ this.averageGrade = averageGrade;
+ this.recordBookNumber = recordBookNumber;
+ }
+
+ public String getGroupNumber() {
+ return groupNumber;
+ }
+
+ public double getAverageGrade() {
+ return averageGrade;
+ }
+
+ public int getRecordBookNumber() {
+ return recordBookNumber;
+ }
+
+ public Student withGroupNumber(String groupNumber) {
+ return new Student(groupNumber, this.averageGrade, this.recordBookNumber);
+ }
+
+ public Student withAverageGrade(double averageGrade) {
+ return new Student(this.groupNumber, averageGrade, this.recordBookNumber);
+ }
+
+ public Student withRecordBookNumber(int recordBookNumber) {
+ return new Student(this.groupNumber, this.averageGrade, recordBookNumber);
+ }
+
+ public static double getMinAverageGrade() {
+ return MIN_AVERAGE_GRADE;
+ }
+
+ public static double getMaxAverageGrade() {
+ return MAX_AVERAGE_GRADE;
+ }
+
+ public static int getMinRecordBookNumber() {
+ return MIN_RECORD_BOOK_NUMBER;
+ }
+
+ public static String getGroupNumberPattern() {
+ return GROUP_NUMBER_PATTERN;
+ }
+
+ public String toString() {
+ return "Student{"
+ + "groupNumber='"
+ + groupNumber
+ + '\''
+ + ", averageGrade="
+ + averageGrade
+ + ", recordBookNumber="
+ + recordBookNumber
+ + '}';
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(groupNumber, averageGrade, recordBookNumber);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ if (this.hashCode() != obj.hashCode()) {
+ return false;
+ }
+
+ Student student = (Student) obj;
+ return Double.compare(averageGrade, student.averageGrade) == 0
+ && recordBookNumber == student.recordBookNumber
+ && Objects.equals(groupNumber, student.groupNumber);
+ }
+}
diff --git a/app/src/main/java/ru/project/student/StudentBuilder.java b/app/src/main/java/ru/project/student/StudentBuilder.java
new file mode 100644
index 0000000..4a103d0
--- /dev/null
+++ b/app/src/main/java/ru/project/student/StudentBuilder.java
@@ -0,0 +1,26 @@
+package ru.project.student;
+
+public class StudentBuilder {
+ private String groupNumber;
+ private double averageGrade;
+ private int recordBookNumber;
+
+ public StudentBuilder setGroupNumber(String groupNumber) {
+ this.groupNumber = groupNumber;
+ return this;
+ }
+
+ public StudentBuilder setAverageGrade(double averageGrade) {
+ this.averageGrade = averageGrade;
+ return this;
+ }
+
+ public StudentBuilder setRecordBookNumber(int recordBookNumber) {
+ this.recordBookNumber = recordBookNumber;
+ return this;
+ }
+
+ public Student build() {
+ return new Student(groupNumber, averageGrade, recordBookNumber);
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/base/BaseModalWindow.java b/app/src/main/java/ru/project/ui/base/BaseModalWindow.java
new file mode 100644
index 0000000..0f0abde
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/base/BaseModalWindow.java
@@ -0,0 +1,19 @@
+package ru.project.ui.base;
+
+import com.googlecode.lanterna.gui2.BasicWindow;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+
+public abstract class BaseModalWindow extends BasicWindow {
+
+ protected final WindowBasedTextGUI gui;
+
+ protected BaseModalWindow(String title, WindowBasedTextGUI gui) {
+ super(title);
+
+ this.gui = gui;
+ }
+
+ public void showModal() {
+ gui.addWindowAndWait(this);
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java b/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java
new file mode 100644
index 0000000..1d71808
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/common/CollectionDisplayWindow.java
@@ -0,0 +1,102 @@
+package ru.project.ui.common;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.EmptySpace;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.TextBox;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import com.googlecode.lanterna.input.KeyStroke;
+import com.googlecode.lanterna.input.KeyType;
+import ru.project.collection.MyList;
+import ru.project.student.Student;
+import ru.project.ui.base.BaseModalWindow;
+
+public class CollectionDisplayWindow extends BaseModalWindow {
+
+ private final MyList collection;
+
+ private TextBox collectionTextBox;
+ private Button exitButton;
+
+ public CollectionDisplayWindow(WindowBasedTextGUI gui, MyList collection) {
+
+ super("Просмотр коллекции", gui);
+
+ this.collection = collection;
+
+ Panel panel = createPanel();
+
+ setComponent(panel);
+
+ exitButton.takeFocus();
+ }
+
+ @Override
+ public boolean handleInput(KeyStroke key) {
+ if (key.getKeyType() == KeyType.Escape && collectionTextBox.isFocused()) {
+
+ exitButton.takeFocus();
+ return true;
+ }
+
+ return super.handleInput(key);
+ }
+
+ private Panel createPanel() {
+ Panel panel = new Panel();
+
+ panel.setLayoutManager(new LinearLayout(Direction.VERTICAL));
+
+ addCollectionDisplay(panel);
+ addExitButton(panel);
+
+ return panel;
+ }
+
+ private void addCollectionDisplay(Panel panel) {
+ collectionTextBox = createCollectionScrollBox();
+
+ panel.addComponent(collectionTextBox);
+ }
+
+ private TextBox createCollectionScrollBox() {
+ TextBox textBox = new TextBox(new TerminalSize(80, 20), TextBox.Style.MULTI_LINE);
+
+ textBox.setReadOnly(true);
+
+ if (collection == null || collection.isEmpty()) {
+ textBox.setText("Коллекция пуста.");
+ } else {
+ textBox.setText(buildCollectionText());
+ }
+
+ return textBox;
+ }
+
+ private void addExitButton(Panel panel) {
+ panel.addComponent(new EmptySpace(new TerminalSize(0, 1)));
+
+ exitButton = new Button("Выйти", this::close);
+
+ panel.addComponent(exitButton);
+ }
+
+ private String buildCollectionText() {
+ StringBuilder text = new StringBuilder();
+
+ for (int i = 0; i < collection.size(); i++) {
+ Student student = collection.get(i);
+
+ text.append("[")
+ .append(i)
+ .append("] ")
+ .append(student == null ? "null" : student)
+ .append("\n");
+ }
+
+ return text.toString();
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/common/InputValueWindow.java b/app/src/main/java/ru/project/ui/common/InputValueWindow.java
new file mode 100644
index 0000000..f3e3200
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/common/InputValueWindow.java
@@ -0,0 +1,56 @@
+package ru.project.ui.common;
+
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.TextBox;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import java.util.function.Predicate;
+import ru.project.ui.base.BaseModalWindow;
+
+public class InputValueWindow extends BaseModalWindow {
+
+ private final TextBox textBox;
+ private final Predicate onSave;
+
+ public InputValueWindow(
+ WindowBasedTextGUI gui,
+ String title,
+ String prompt,
+ String initialValue,
+ Predicate onSave) {
+
+ super(title, gui);
+
+ this.onSave = onSave;
+
+ Panel panel = new Panel();
+ panel.setLayoutManager(new LinearLayout());
+
+ panel.addComponent(new Label(prompt));
+
+ textBox = new TextBox(initialValue == null ? "" : initialValue);
+
+ panel.addComponent(textBox);
+
+ panel.addComponent(new Button("Сохранить", this::save));
+
+ panel.addComponent(new Button("Отмена", this::close));
+
+ setComponent(panel);
+ }
+
+ private void save() {
+ String value = textBox.getText().trim();
+
+ if (value.isEmpty()) {
+ MessageWindow.showModal(gui, "Значение не может быть пустым.");
+ return;
+ }
+
+ if (onSave.test(value)) {
+ close();
+ }
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/common/MessageWindow.java b/app/src/main/java/ru/project/ui/common/MessageWindow.java
new file mode 100644
index 0000000..3f1905f
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/common/MessageWindow.java
@@ -0,0 +1,28 @@
+package ru.project.ui.common;
+
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import ru.project.ui.base.BaseModalWindow;
+
+public class MessageWindow extends BaseModalWindow {
+
+ public MessageWindow(WindowBasedTextGUI gui, String message) {
+ super("Информация", gui);
+
+ Panel panel = new Panel();
+ panel.setLayoutManager(new LinearLayout());
+
+ panel.addComponent(new Label(message));
+
+ panel.addComponent(new Button("OK", this::close));
+
+ setComponent(panel);
+ }
+
+ public static void showModal(WindowBasedTextGUI gui, String message) {
+ new MessageWindow(gui, message).showModal();
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java b/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java
new file mode 100644
index 0000000..72820be
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/creation/CreateCollectionMenu.java
@@ -0,0 +1,169 @@
+package ru.project.ui.creation;
+
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import ru.project.collection.MyLinkedList;
+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.common.InputValueWindow;
+import ru.project.ui.common.MessageWindow;
+
+public class CreateCollectionMenu extends BaseModalWindow {
+
+ private int collectionSize;
+ private FillMethod selectedFillMethod;
+
+ private final Label sizeLabel;
+ private final Label fillMethodLabel;
+
+ private final CustomDataSettingsWindow customDataSettingsWindow;
+
+ public CreateCollectionMenu(WindowBasedTextGUI gui, AppState state) {
+ super("Создание новой коллекции", gui);
+
+ collectionSize = 0;
+ selectedFillMethod = FillMethod.CUSTOM;
+
+ customDataSettingsWindow = new CustomDataSettingsWindow(gui, state, () -> collectionSize);
+
+ 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::showCollectionSizeInputValueWindow));
+
+ panel.addComponent(new Button("Выбрать способ заполнения", this::selectFillMethod));
+
+ panel.addComponent(
+ new Button("Настроить текущий способ заполнения", this::setupCurrentFillMethod));
+
+ panel.addComponent(new Button("Создать новую коллекцию", () -> updateMainCollection(state)));
+
+ panel.addComponent(new Button("Выйти", this::close));
+
+ setComponent(panel);
+ }
+
+ private void showCollectionSizeInputValueWindow() {
+ InputValueWindow window =
+ new InputValueWindow(
+ gui,
+ "Размер коллекции",
+ "Введите размер коллекции:",
+ collectionSize > 0 ? String.valueOf(collectionSize) : null,
+ this::handleSizeEntered);
+
+ window.showModal();
+ }
+
+ private boolean handleSizeEntered(String value) {
+ try {
+ int enteredSize = Integer.parseInt(value);
+ if (enteredSize <= 0) {
+ MessageWindow.showModal(gui, "Размер должен быть больше 0.");
+ return false;
+ }
+ collectionSize = enteredSize;
+ sizeLabel.setText("Размер: " + collectionSize);
+ return true;
+ } catch (NumberFormatException e) {
+ MessageWindow.showModal(gui, "Введите целое число.");
+ return false;
+ }
+ }
+
+ private void setupCurrentFillMethod() {
+ switch (selectedFillMethod) {
+ case CUSTOM -> {
+ customDataSettingsWindow.showModal();
+ }
+
+ case FILE ->
+ // TODO
+ MessageWindow.showModal(gui, "Не реализовано.");
+
+ case RANDOM ->
+ MessageWindow.showModal(
+ gui, "Для случайных данных дополнительные настройки не требуются.");
+ }
+ }
+
+ private void selectFillMethod() {
+ BaseModalWindow menu =
+ new FillMethodSelectionWindow(
+ gui,
+ fillMethod -> {
+ selectedFillMethod = fillMethod;
+ fillMethodLabel.setText(
+ "Способ заполнения: " + getFillMethodName(selectedFillMethod));
+ });
+
+ menu.showModal();
+ }
+
+ private void updateMainCollection(AppState state) {
+ if (collectionSize <= 0) {
+ MessageWindow.showModal(gui, "Сначала задайте размер коллекции.");
+ return;
+ }
+
+ switch (selectedFillMethod) {
+ case CUSTOM:
+ createCollectionFromCustomData(state);
+ break;
+
+ case RANDOM:
+ createCollectionFromRandomData(state);
+ break;
+
+ case FILE:
+ createCollectionFromFileData(state);
+ break;
+ }
+ }
+
+ 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(AppState state) {
+ // TODO
+ MessageWindow.showModal(gui, "Не реализовано.");
+ }
+
+ private void createCollectionFromFileData(AppState state) {
+ // TODO
+ MessageWindow.showModal(gui, "Не реализовано.");
+ }
+
+ private String getFillMethodName(FillMethod fillMethod) {
+ return switch (fillMethod) {
+ case CUSTOM -> "Пользовательские данные";
+
+ case RANDOM -> "Случайные данные";
+
+ case FILE -> "Данные из файла";
+ };
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/creation/CustomDataSettingsWindow.java b/app/src/main/java/ru/project/ui/creation/CustomDataSettingsWindow.java
new file mode 100644
index 0000000..5f0aa10
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/creation/CustomDataSettingsWindow.java
@@ -0,0 +1,289 @@
+package ru.project.ui.creation;
+
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import 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.BaseModalWindow;
+import ru.project.ui.common.CollectionDisplayWindow;
+import ru.project.ui.common.InputValueWindow;
+import ru.project.ui.common.MessageWindow;
+
+public class CustomDataSettingsWindow extends BaseModalWindow {
+
+ private final AppState state;
+ private final Supplier requestedCollectionSize;
+ 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 MyList preparedCustomDataCollection;
+
+ public CustomDataSettingsWindow(
+ WindowBasedTextGUI gui, AppState state, Supplier collectionSize) {
+
+ super("Настройка пользовательских данных", gui);
+
+ this.state = state;
+ this.requestedCollectionSize = collectionSize;
+
+ this.studentBuilder = new StudentBuilder();
+
+ 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);
+ }
+
+ @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(
+ gui,
+ "Номер группы",
+ "Введите номер группы (например, A12):",
+ groupNumber,
+ value -> {
+ if (!value.matches(Student.getGroupNumberPattern())) {
+ MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12.");
+ return false;
+ }
+
+ groupNumber = value;
+ studentBuilder.setGroupNumber(value);
+
+ groupNumberLabel.setText("Номер группы: " + groupNumber);
+
+ return true;
+ });
+
+ window.showModal();
+ }
+
+ private void setAverageGrade() {
+ InputValueWindow window =
+ new InputValueWindow(
+ gui,
+ "Средний балл",
+ "Введите средний балл от 0.0 до 5.0:",
+ averageGrade == null ? null : String.valueOf(averageGrade),
+ value -> {
+ try {
+ double parsedValue = Double.parseDouble(value);
+
+ if (parsedValue < Student.getMinAverageGrade()
+ || parsedValue > Student.getMaxAverageGrade()) {
+
+ MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0.");
+
+ return false;
+ }
+
+ averageGrade = parsedValue;
+
+ studentBuilder.setAverageGrade(parsedValue);
+
+ averageGradeLabel.setText("Средний балл: " + averageGrade);
+
+ return true;
+
+ } catch (NumberFormatException e) {
+ MessageWindow.showModal(gui, "Введите корректное число.");
+
+ return false;
+ }
+ });
+
+ window.showModal();
+ }
+
+ private void setRecordBookNumber() {
+ InputValueWindow window =
+ new InputValueWindow(
+ gui,
+ "Номер зачётной книжки",
+ "Введите номер зачётной книжки:",
+ recordBookNumber == null ? null : String.valueOf(recordBookNumber),
+ value -> {
+ try {
+ int parsedValue = Integer.parseInt(value);
+
+ if (parsedValue < Student.getMinRecordBookNumber()) {
+
+ MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным.");
+
+ return false;
+ }
+
+ recordBookNumber = parsedValue;
+
+ studentBuilder.setRecordBookNumber(parsedValue);
+
+ recordBookNumberLabel.setText("Номер зачётной книжки: " + recordBookNumber);
+
+ return true;
+
+ } catch (NumberFormatException e) {
+ MessageWindow.showModal(gui, "Введите целое число.");
+
+ return false;
+ }
+ });
+
+ window.showModal();
+ }
+
+ private void setObjectIndex() {
+ InputValueWindow window =
+ new InputValueWindow(
+ gui,
+ "Индекс объекта",
+ "Введите индекс объекта:",
+ selectedObjectIndex == null ? null : String.valueOf(selectedObjectIndex),
+ value -> {
+ try {
+ int index = Integer.parseInt(value);
+
+ if (index < 0) {
+ MessageWindow.showModal(gui, "Индекс объекта не может быть отрицательным.");
+
+ return false;
+ }
+
+ int size = requestedCollectionSize.get();
+
+ if (index >= size) {
+ MessageWindow.showModal(gui, "Индекс должен быть меньше размера коллекции.");
+
+ return false;
+ }
+
+ selectedObjectIndex = index;
+
+ indexLabel.setText("Индекс объекта: " + selectedObjectIndex);
+
+ return true;
+
+ } catch (NumberFormatException e) {
+ MessageWindow.showModal(gui, "Введите целое число.");
+
+ return false;
+ }
+ });
+
+ window.showModal();
+ }
+
+ private void editObject() {
+ if (!hasStudentData()) {
+ MessageWindow.showModal(gui, "Сначала задайте все поля студента.");
+ return;
+ }
+
+ if (selectedObjectIndex == null) {
+ MessageWindow.showModal(gui, "Сначала задайте индекс объекта.");
+ return;
+ }
+
+ if (selectedObjectIndex >= preparedCustomDataCollection.size()) {
+ MessageWindow.showModal(gui, "Индекс объекта выходит за границы подготовленной коллекции.");
+ return;
+ }
+
+ Student student = studentBuilder.build();
+
+ preparedCustomDataCollection.set(selectedObjectIndex, student);
+
+ MessageWindow.showModal(gui, "Данные объекта успешно заданы.");
+ }
+
+ private void apply() {
+ state.setCustomDataCollection(preparedCustomDataCollection);
+ prepareCustomDataCollection();
+
+ MessageWindow.showModal(gui, "Пользовательские данные успешно применены.");
+ }
+
+ private void preview() {
+ if (preparedCustomDataCollection == null || preparedCustomDataCollection.isEmpty()) {
+
+ MessageWindow.showModal(gui, "Подготовленная коллекция пуста.");
+
+ return;
+ }
+
+ CollectionDisplayWindow window = new CollectionDisplayWindow(gui, preparedCustomDataCollection);
+
+ window.showModal();
+ }
+
+ private boolean hasStudentData() {
+ return groupNumber != null && averageGrade != null && recordBookNumber != null;
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/creation/FillMethod.java b/app/src/main/java/ru/project/ui/creation/FillMethod.java
new file mode 100644
index 0000000..28567fe
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/creation/FillMethod.java
@@ -0,0 +1,7 @@
+package ru.project.ui.creation;
+
+public enum FillMethod {
+ CUSTOM,
+ RANDOM,
+ FILE
+}
diff --git a/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java
new file mode 100644
index 0000000..9cf0fc7
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/creation/FillMethodSelectionWindow.java
@@ -0,0 +1,36 @@
+package ru.project.ui.creation;
+
+import com.googlecode.lanterna.gui2.ActionListBox;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import java.util.function.Consumer;
+import ru.project.ui.base.BaseModalWindow;
+
+public class FillMethodSelectionWindow extends BaseModalWindow {
+
+ private final Consumer onFillMethodSelected;
+
+ public FillMethodSelectionWindow(
+ WindowBasedTextGUI gui, Consumer onFillMethodSelected) {
+
+ super("Выбор способа заполнения", gui);
+
+ this.onFillMethodSelected = onFillMethodSelected;
+
+ ActionListBox menu = new ActionListBox();
+
+ menu.addItem("Пользовательские данные", () -> selectFillMethod(FillMethod.CUSTOM));
+
+ menu.addItem("Случайные данные", () -> selectFillMethod(FillMethod.RANDOM));
+
+ menu.addItem("Данные из файла", () -> selectFillMethod(FillMethod.FILE));
+
+ menu.addItem("Выйти", this::close);
+
+ setComponent(menu);
+ }
+
+ private void selectFillMethod(FillMethod fillMethod) {
+ onFillMethodSelected.accept(fillMethod);
+ close();
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/main/MainMenu.java b/app/src/main/java/ru/project/ui/main/MainMenu.java
new file mode 100644
index 0000000..ff4899e
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/main/MainMenu.java
@@ -0,0 +1,80 @@
+package ru.project.ui.main;
+
+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.BaseModalWindow;
+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 {
+
+ 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();
+
+ menu.addItem("Показать текущую коллекцию", () -> showMainCollection(state));
+
+ menu.addItem("Меню создания новой коллекции", this::showCreateCollectionMenu);
+
+ menu.addItem("Меню сохранения текущей коллекции в файл", this::saveCollection);
+
+ menu.addItem("Меню сортировки текущей коллекции", this::sortCollection);
+
+ menu.addItem("Меню поиска элемента в текущей коллекции", this::searchCollection);
+
+ menu.addItem("Меню подсчёта количества вхождений элемента", this::countOccurrences);
+
+ menu.addItem("Завершить выполнение программы", this::exit);
+
+ setComponent(menu);
+ }
+
+ private void showMainCollection(AppState state) {
+ MyList mainCollection = state.getMainCollection();
+
+ BaseModalWindow window = new CollectionDisplayWindow(gui, mainCollection);
+
+ window.showModal();
+ }
+
+ private void showCreateCollectionMenu() {
+ createCollectionMenu.showModal();
+ }
+
+ private void saveCollection() {
+ // TODO
+ MessageWindow.showModal(gui, "Не реализовано.");
+ }
+
+ private void sortCollection() {
+ sortCollectionMenu.showModal();
+ }
+
+ private void searchCollection() {
+ // TODO
+ MessageWindow.showModal(gui, "Не реализовано.");
+ }
+
+ private void countOccurrences() {
+ // TODO
+ MessageWindow.showModal(gui, "Не реализовано.");
+ }
+
+ private void exit() {
+ close();
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/sorting/NormalSortSettingsWindow.java b/app/src/main/java/ru/project/ui/sorting/NormalSortSettingsWindow.java
new file mode 100644
index 0000000..0f3bbd2
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/sorting/NormalSortSettingsWindow.java
@@ -0,0 +1,45 @@
+package ru.project.ui.sorting;
+
+import com.googlecode.lanterna.gui2.ActionListBox;
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import java.util.function.Consumer;
+import ru.project.ui.base.BaseModalWindow;
+
+public class NormalSortSettingsWindow extends BaseModalWindow {
+
+ private final Consumer onSortFieldSelected;
+
+ public NormalSortSettingsWindow(WindowBasedTextGUI gui, Consumer onSortFieldSelected) {
+
+ super("Обычная сортировка", gui);
+
+ this.onSortFieldSelected = onSortFieldSelected;
+
+ Panel panel = new Panel();
+ panel.setLayoutManager(new LinearLayout());
+
+ ActionListBox menu = new ActionListBox();
+
+ menu.addItem("Сортировать по номеру группы", () -> selectSortField(SortField.GROUP_NUMBER));
+
+ menu.addItem("Сортировать по среднему баллу", () -> selectSortField(SortField.AVERAGE_GRADE));
+
+ menu.addItem(
+ "Сортировать по номеру зачётной книжки",
+ () -> selectSortField(SortField.RECORD_BOOK_NUMBER));
+
+ panel.addComponent(menu);
+
+ panel.addComponent(new Button("Выйти", this::close));
+
+ setComponent(panel);
+ }
+
+ private void selectSortField(SortField sortField) {
+ onSortFieldSelected.accept(sortField);
+ close();
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/sorting/ParitySortSettingsWindow.java b/app/src/main/java/ru/project/ui/sorting/ParitySortSettingsWindow.java
new file mode 100644
index 0000000..aa6d090
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/sorting/ParitySortSettingsWindow.java
@@ -0,0 +1,24 @@
+package ru.project.ui.sorting;
+
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import ru.project.ui.base.BaseModalWindow;
+
+public class ParitySortSettingsWindow extends BaseModalWindow {
+
+ public ParitySortSettingsWindow(WindowBasedTextGUI gui) {
+ super("Настройка сортировки по чётности", gui);
+
+ Panel panel = new Panel();
+ panel.setLayoutManager(new LinearLayout());
+
+ panel.addComponent(new Label("Сортировка выполняется по номеру зачётной книжки."));
+
+ panel.addComponent(new Button("Назад", this::close));
+
+ setComponent(panel);
+ }
+}
diff --git a/app/src/main/java/ru/project/ui/sorting/SortCollectionMenu.java b/app/src/main/java/ru/project/ui/sorting/SortCollectionMenu.java
new file mode 100644
index 0000000..ae72b4b
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/sorting/SortCollectionMenu.java
@@ -0,0 +1,126 @@
+package ru.project.ui.sorting;
+
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.Label;
+import com.googlecode.lanterna.gui2.LinearLayout;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
+import ru.project.model.AppState;
+import ru.project.ui.base.BaseModalWindow;
+import ru.project.ui.common.MessageWindow;
+
+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) {
+
+ super("Меню сортировки текущей коллекции", gui);
+
+ 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,
+ sortMethod -> {
+ selectedSortMethod = sortMethod;
+
+ sortMethodLabel.setText(
+ "Способ сортировки: " + 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));
+
+ panel.addComponent(
+ new Button("Настроить текущий способ сортировки", this::setupCurrentSortMethod));
+
+ panel.addComponent(new Button("Сортировать текущую коллекцию", this::sortCurrentCollection));
+
+ panel.addComponent(new Button("Выйти", this::close));
+
+ setComponent(panel);
+ }
+
+ private void setupCurrentSortMethod() {
+ switch (selectedSortMethod) {
+ case NORMAL -> normalSortSettingsWindow.showModal();
+
+ case PARITY -> paritySortSettingsWindow.showModal();
+ }
+ }
+
+ private void selectSortMethod() {
+ sortMethodSelectionWindow.showModal();
+ }
+
+ private void sortCurrentCollection() {
+ if (state.getMainCollection() == null || state.getMainCollection().isEmpty()) {
+
+ MessageWindow.showModal(gui, "Текущая коллекция пуста.");
+
+ return;
+ }
+
+ switch (selectedSortMethod) {
+ case NORMAL -> sortNormally();
+
+ case PARITY -> sortByParity();
+ }
+ }
+
+ 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 -> "Сортировка по чётности";
+ };
+ }
+
+ 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/sorting/SortField.java b/app/src/main/java/ru/project/ui/sorting/SortField.java
new file mode 100644
index 0000000..6219f9b
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/sorting/SortField.java
@@ -0,0 +1,7 @@
+package ru.project.ui.sorting;
+
+public enum SortField {
+ GROUP_NUMBER,
+ AVERAGE_GRADE,
+ RECORD_BOOK_NUMBER
+}
diff --git a/app/src/main/java/ru/project/ui/sorting/SortMethod.java b/app/src/main/java/ru/project/ui/sorting/SortMethod.java
new file mode 100644
index 0000000..446f27e
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/sorting/SortMethod.java
@@ -0,0 +1,6 @@
+package ru.project.ui.sorting;
+
+public enum SortMethod {
+ NORMAL,
+ PARITY
+}
diff --git a/app/src/main/java/ru/project/ui/sorting/SortMethodSelectionWindow.java b/app/src/main/java/ru/project/ui/sorting/SortMethodSelectionWindow.java
new file mode 100644
index 0000000..121e5e2
--- /dev/null
+++ b/app/src/main/java/ru/project/ui/sorting/SortMethodSelectionWindow.java
@@ -0,0 +1,34 @@
+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.ui.base.BaseModalWindow;
+
+public class SortMethodSelectionWindow extends BaseModalWindow {
+
+ private final Consumer onSortMethodSelected;
+
+ public SortMethodSelectionWindow(
+ WindowBasedTextGUI gui, Consumer onSortMethodSelected) {
+
+ super("Выбрать способ сортировки", gui);
+
+ this.onSortMethodSelected = onSortMethodSelected;
+
+ ActionListBox menu = new ActionListBox();
+
+ menu.addItem("Обычная сортировка", () -> selectSortMethod(SortMethod.NORMAL));
+
+ menu.addItem("Сортировка по чётности", () -> selectSortMethod(SortMethod.PARITY));
+
+ menu.addItem("Выйти", this::close);
+
+ setComponent(menu);
+ }
+
+ private void selectSortMethod(SortMethod sortMethod) {
+ onSortMethodSelected.accept(sortMethod);
+ close();
+ }
+}
diff --git a/app/src/test/java/ru/project/AppTest.java b/app/src/test/java/ru/project/AppTest.java
deleted file mode 100644
index c79d156..0000000
--- a/app/src/test/java/ru/project/AppTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package ru.project;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * Unit test for simple App.
- */
-public class AppTest {
-
- /**
- * Rigorous Test :-)
- */
- @Test
- public void shouldAnswerWithTrue() {
- assertTrue(true);
- }
-}
diff --git a/app/src/test/java/ru/project/collection/MyLinkedListTest.java b/app/src/test/java/ru/project/collection/MyLinkedListTest.java
new file mode 100644
index 0000000..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));
+ }
+}
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));
+ }
+
+}
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], "Результат должен быть отсортирован");
+ }
+ }
+}
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
diff --git a/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java
new file mode 100644
index 0000000..daccad5
--- /dev/null
+++ b/app/src/test/java/ru/project/sorting/EvenOnlySortStrategyTest.java
@@ -0,0 +1,286 @@
+package ru.project.sorting;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Comparator;
+import org.junit.jupiter.api.Test;
+import ru.project.collection.MyLinkedList;
+import ru.project.collection.MyList;
+import ru.project.student.Student;
+
+class EvenOnlySortStrategyTest {
+
+ @Test
+ void shouldHandleEmptyList() {
+ MyList list = new MyLinkedList<>();
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(0, list.size());
+ }
+
+ @Test
+ void shouldKeepSingleEvenElementUnchanged() {
+ MyList list = new MyLinkedList<>();
+ list.add(4);
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(4, list.get(0));
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ void shouldKeepSingleOddElementUnchanged() {
+ MyList list = new MyLinkedList<>();
+ list.add(7);
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(7, list.get(0));
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ void shouldHandleDuplicateEvenValues() {
+ MyList list = new MyLinkedList<>();
+
+ list.add(8);
+ list.add(4);
+ list.add(8);
+ list.add(2);
+ list.add(4);
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(2, list.get(0));
+ assertEquals(4, list.get(1));
+ assertEquals(4, list.get(2));
+ assertEquals(8, list.get(3));
+ assertEquals(8, list.get(4));
+ }
+
+ @Test
+ void shouldKeepOddStudentObjectsAtSameIndexes() {
+ Student oddFirst = new Student("A01", 4.0, 7);
+ Student evenFirst = new Student("A02", 4.0, 8);
+ Student oddSecond = new Student("A03", 4.0, 3);
+ Student evenSecond = new Student("A04", 4.0, 2);
+ Student oddThird = new Student("A05", 4.0, 5);
+ Student evenThird = new Student("A06", 4.0, 4);
+
+ MyList students = new MyLinkedList<>();
+
+ students.add(oddFirst);
+ students.add(evenFirst);
+ students.add(oddSecond);
+ students.add(evenSecond);
+ students.add(oddThird);
+ students.add(evenThird);
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), Student::getRecordBookNumber);
+
+ strategy.sort(students, new StudentRecordBookComparator());
+
+ assertSame(oddFirst, students.get(0));
+ assertSame(oddSecond, students.get(2));
+ assertSame(oddThird, students.get(4));
+
+ assertEquals(2, students.get(1).getRecordBookNumber());
+ assertEquals(4, students.get(3).getRecordBookNumber());
+ assertEquals(8, students.get(5).getRecordBookNumber());
+ }
+
+ @Test
+ void shouldSortWhenStrategyPassedThroughConstructor() {
+ MyList list = new MyLinkedList<>();
+
+ list.add(8);
+ list.add(2);
+ list.add(4);
+
+ EvenOnlySortStrategy strategy = new EvenOnlySortStrategy<>(new MergeSortStrategy<>());
+
+ strategy.setValueExtractor(value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(2, list.get(0));
+ assertEquals(4, list.get(1));
+ assertEquals(8, list.get(2));
+ }
+
+ @Test
+ void shouldSortOnlyEvenElements() {
+ MyList students = new MyLinkedList<>();
+
+ students.add(new Student("A01", 4.0, 7));
+ students.add(new Student("A02", 4.0, 8));
+ students.add(new Student("A03", 4.0, 3));
+ students.add(new Student("A04", 4.0, 2));
+ students.add(new Student("A05", 4.0, 5));
+ students.add(new Student("A06", 4.0, 4));
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), Student::getRecordBookNumber);
+
+ strategy.sort(students, new StudentRecordBookComparator());
+
+ assertEquals(7, students.get(0).getRecordBookNumber());
+ assertEquals(2, students.get(1).getRecordBookNumber());
+ assertEquals(3, students.get(2).getRecordBookNumber());
+ assertEquals(4, students.get(3).getRecordBookNumber());
+ assertEquals(5, students.get(4).getRecordBookNumber());
+ assertEquals(8, students.get(5).getRecordBookNumber());
+
+ assertEquals(6, students.size());
+ }
+
+ @Test
+ void shouldSortAllEvenElements() {
+ MyList list = new MyLinkedList<>();
+
+ list.add(8);
+ list.add(2);
+ list.add(6);
+ list.add(4);
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(2, list.get(0));
+ assertEquals(4, list.get(1));
+ assertEquals(6, list.get(2));
+ assertEquals(8, list.get(3));
+ }
+
+ @Test
+ void shouldKeepOddElementsUnchanged() {
+ MyList list = new MyLinkedList<>();
+
+ list.add(7);
+ list.add(3);
+ list.add(5);
+ list.add(1);
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertEquals(7, list.get(0));
+ assertEquals(3, list.get(1));
+ assertEquals(5, list.get(2));
+ assertEquals(1, list.get(3));
+ }
+
+ @Test
+ void shouldThrowWhenSortStrategyIsNotSet() {
+ EvenOnlySortStrategy strategy = new EvenOnlySortStrategy<>();
+
+ strategy.setValueExtractor(value -> value);
+
+ MyList list = new MyLinkedList<>();
+ list.add(2);
+ list.add(4);
+
+ assertThrows(IllegalStateException.class, () -> strategy.sort(list, Integer::compare));
+ }
+
+ @Test
+ void shouldRejectNullSortStrategy() {
+ EvenOnlySortStrategy strategy = new EvenOnlySortStrategy<>();
+
+ assertThrows(IllegalArgumentException.class, () -> strategy.setSortStrategy(null));
+ }
+
+ @Test
+ void shouldDelegateSortingToProvidedStrategy() {
+ MyList list = new MyLinkedList<>();
+
+ list.add(7);
+ list.add(8);
+ list.add(3);
+ list.add(2);
+
+ RecordingSortStrategy recordingStrategy = new RecordingSortStrategy();
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(recordingStrategy, value -> value);
+
+ strategy.sort(list, Integer::compare);
+
+ assertTrue(recordingStrategy.wasCalled);
+ assertEquals(2, recordingStrategy.receivedSize);
+ assertEquals(8, recordingStrategy.firstReceivedElement);
+ assertEquals(2, recordingStrategy.secondReceivedElement);
+ }
+
+ @Test
+ void shouldUseNewStrategyAfterReplacement() {
+ RecordingSortStrategy firstStrategy = new RecordingSortStrategy();
+
+ RecordingSortStrategy secondStrategy = new RecordingSortStrategy();
+
+ EvenOnlySortStrategy strategy =
+ new EvenOnlySortStrategy<>(firstStrategy, value -> value);
+
+ MyList firstList = new MyLinkedList<>();
+ firstList.add(4);
+ firstList.add(2);
+
+ strategy.sort(firstList, Integer::compare);
+
+ assertTrue(firstStrategy.wasCalled);
+
+ strategy.setSortStrategy(secondStrategy);
+
+ MyList secondList = new MyLinkedList<>();
+ secondList.add(8);
+ secondList.add(6);
+
+ strategy.sort(secondList, Integer::compare);
+
+ assertTrue(secondStrategy.wasCalled);
+ }
+
+ private static class RecordingSortStrategy implements SortStrategy {
+
+ private boolean wasCalled;
+ private int receivedSize;
+ private Integer firstReceivedElement;
+ private Integer secondReceivedElement;
+
+ @Override
+ public void sort(MyList list, Comparator comparator) {
+ wasCalled = true;
+ receivedSize = list.size();
+
+ if (list.size() > 0) {
+ firstReceivedElement = list.get(0);
+ }
+
+ if (list.size() > 1) {
+ secondReceivedElement = list.get(1);
+ }
+ }
+ }
+}
diff --git a/app/src/test/java/ru/project/sorting/SortStrategyTest.java b/app/src/test/java/ru/project/sorting/SortStrategyTest.java
new file mode 100644
index 0000000..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
new file mode 100644
index 0000000..a51e251
--- /dev/null
+++ b/app/src/test/java/ru/project/student/StudentTest.java
@@ -0,0 +1,141 @@
+package ru.project.student;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class StudentTest {
+
+ @Test
+ void testValidStudentCreation() {
+ Student student = new Student("A12", 4.5, 12345);
+ assertEquals("A12", student.getGroupNumber());
+ assertEquals(4.5, student.getAverageGrade());
+ assertEquals(12345, student.getRecordBookNumber());
+ }
+
+ @Test
+ void testValidGroupNumbers() {
+ assertDoesNotThrow(() -> new Student("A12", 4.5, 1));
+ assertDoesNotThrow(() -> new Student("B32", 4.5, 1));
+ assertDoesNotThrow(() -> new Student("Z00", 4.5, 1));
+ }
+
+ @Test
+ void testInvalidGroupNumbers() {
+ assertThrows(IllegalArgumentException.class, () -> new Student("a12", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("A1", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("A123", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("AA1", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("а1А", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("аа!!а123", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("23фи!12", 4.5, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("№№###*()", 4.5, 1));
+ }
+
+ @Test
+ void testValidAverageGrades() {
+ assertDoesNotThrow(() -> new Student("A12", 0.0, 1));
+ assertDoesNotThrow(() -> new Student("A12", 5.0, 1));
+ }
+
+ @Test
+ void testInvalidAverageGrades() {
+ assertThrows(IllegalArgumentException.class, () -> new Student("A12", -1.0, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Student("A12", 6.0, 1));
+ }
+
+ @Test
+ void testValidRecordBookNumbers() {
+ assertDoesNotThrow(() -> new Student("A12", 4.5, 1));
+ assertDoesNotThrow(() -> new Student("A12", 4.5, 12345));
+ }
+
+ @Test
+ void testInvalidRecordBookNumbers() {
+ assertThrows(IllegalArgumentException.class, () -> new Student("A12", 4.5, 0));
+ assertThrows(IllegalArgumentException.class, () -> new Student("A12", 4.5, -1));
+ }
+
+ @Test
+ void testWithGroupNumber() {
+ Student original = new Student("A12", 4.5, 12345);
+ Student updated = original.withGroupNumber("B32");
+
+ assertNotSame(original, updated);
+ assertEquals("B32", updated.getGroupNumber());
+ assertEquals(4.5, updated.getAverageGrade());
+ assertEquals(12345, updated.getRecordBookNumber());
+
+ assertEquals("A12", original.getGroupNumber());
+ }
+
+ @Test
+ void testWithAverageGrade() {
+ Student original = new Student("A12", 4.5, 12345);
+ Student updated = original.withAverageGrade(4.8);
+
+ assertNotSame(original, updated);
+ assertEquals(4.8, updated.getAverageGrade());
+ assertEquals("A12", updated.getGroupNumber());
+ assertEquals(12345, updated.getRecordBookNumber());
+
+ assertEquals(4.5, original.getAverageGrade());
+ }
+
+ @Test
+ void testWithRecordBookNumber() {
+ Student original = new Student("A12", 4.5, 12345);
+ Student updated = original.withRecordBookNumber(99999);
+
+ assertNotSame(original, updated);
+ assertEquals(99999, updated.getRecordBookNumber());
+ assertEquals("A12", updated.getGroupNumber());
+ assertEquals(4.5, updated.getAverageGrade());
+
+ assertEquals(12345, original.getRecordBookNumber());
+ }
+
+ @Test
+ void testWithInvalidGroupNumber() {
+ Student original = new Student("A12", 4.5, 12345);
+ assertThrows(IllegalArgumentException.class, () -> original.withGroupNumber("a12"));
+ assertEquals("A12", original.getGroupNumber()); // не изменился
+ }
+
+ @Test
+ void testWithInvalidAverageGrade() {
+ Student original = new Student("A12", 4.5, 12345);
+ assertThrows(IllegalArgumentException.class, () -> original.withAverageGrade(6.0));
+ assertEquals(4.5, original.getAverageGrade());
+ }
+
+ @Test
+ void testWithInvalidRecordBookNumber() {
+ Student original = new Student("A12", 4.5, 12345);
+ assertThrows(IllegalArgumentException.class, () -> original.withRecordBookNumber(0));
+ assertEquals(12345, original.getRecordBookNumber());
+ }
+
+ @Test
+ void testEqualsAndHashCode() {
+ Student s1 = new Student("A12", 4.5, 12345);
+ Student s2 = new Student("A12", 4.5, 12345);
+ Student s3 = new Student("B32", 4.5, 12345);
+ Student s4 = new Student("A12", 4.8, 12345);
+ Student s5 = new Student("A12", 4.5, 99999);
+
+ assertEquals(s1, s2);
+ assertEquals(s1.hashCode(), s2.hashCode());
+
+ assertNotEquals(s1, s3);
+ assertNotEquals(s1, s4);
+ assertNotEquals(s1, s5);
+ assertNotEquals(s1, null);
+ assertNotEquals(s1, "some string");
+ }
+}
diff --git a/docs/MENU.ru.txt b/docs/MENU.ru.txt
new file mode 100644
index 0000000..0be1048
--- /dev/null
+++ b/docs/MENU.ru.txt
@@ -0,0 +1,130 @@
+Главное меню
+{
+ Показать текущую коллекцию
+
+ Меню создания новой коллекции
+ {
+ Вернуться
+
+ Задать размер
+
+ Настроить способ заполнения
+ {
+ Пользовательские данные
+ {
+ Задать поле 'Название A'
+ Задать поле 'Название B'
+ Задать поле 'Название C'
+ Задать индекс изменяемого объекта
+ Изменить данные объекта
+ Предпросмотр
+ }
+
+ Случайные данные
+ {
+ Настройка диапазона генерации
+ {
+ Поле 'Название A'
+ {
+ Задать минимальное значение
+ Задать максимальное значение
+ }
+
+ Поле 'Название B'
+ {
+ Задать минимальное значение
+ Задать максимальное значение
+ }
+
+ Поле 'Название C'
+ {
+ Задать минимальное значение
+ Задать максимальное значение
+ }
+ }
+ }
+
+ Данные из файла
+ {
+ Задать путь до файла
+ {
+ Абсолютный
+ Относительный
+ }
+ }
+ }
+
+ Выбрать способ заполнения
+ {
+ Пользовательские данные *
+ Случайные данные
+ Данные из файла
+ }
+
+ Создать новую коллекцию
+ }
+
+ Меню сохранения текущей коллекции в файл
+ {
+ Вывести текущий абсолютный путь до файла
+
+ Задать путь до файла
+ {
+ Абсолютный
+ Относительный
+ }
+
+ Дописать коллекцию в текущий файл
+ }
+
+ Меню сортировки текущей коллекции
+ {
+ Вернуться
+
+ Настроить способ сортировки
+ {
+ Обычная сортировка
+ {
+ Сортировать по полю 'Название A'
+ Сортировать по полю 'Название B'
+ Сортировать по полю 'Название C'
+ }
+ }
+
+ Настроить способ сортировки
+ {
+ Сортировка по чётности
+ {
+ Выбрать числовое поле 'Название A'
+ Выбрать числовое поле 'Название B'
+ }
+ }
+
+ Выбрать способ сортировки
+ {
+ Обычная сортировка *
+ Сортировка по чётности
+ }
+
+ Сортировать текущую коллекцию
+ }
+
+ Меню поиска элемента в текущей коллекции
+ {
+ Вернуться
+ Задать значение поля 'Название A'
+ Задать значение поля 'Название B'
+ Задать значение поля 'Название C'
+ Найти первое вхождение в текущей коллекции
+ }
+
+ Меню подсчёта количества вхождений элемента
+ {
+ Задать значение поля 'Название A'
+ Задать значение поля 'Название B'
+ Задать значение поля 'Название C'
+ Выполнить многопоточный подсчёт
+ }
+
+ Завершить выполнение программы
+}
diff --git a/docs/tasks/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
+```
diff --git a/docs/tasks/CsvStudentIO.md b/docs/tasks/CsvStudentIO.md
new file mode 100644
index 0000000..76ea466
--- /dev/null
+++ b/docs/tasks/CsvStudentIO.md
@@ -0,0 +1,441 @@
+# Техническое задание: чтение и дозапись `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. Заголовок CSV не должен записываться повторно.
+6. Если файл не существует, новый файл **не создавать**.
+7. Если файл не существует, метод должен выбросить `IOException`.
+8. При успешной записи метод завершается без исключения.
+9. Метод не возвращает значение.
+10. Исключения, связанные с записью файла, не обрабатывать внутри метода.
+11. Исключения `Student` и `StudentBuilder` также не обрабатывать.
+
+## Невозможность записи
+
+Если указанный файл не существует, метод должен выбросить `IOException`.
+
+Также `IOException` должен возникать и передаваться вызывающему коду, если:
+
+- файл недоступен для записи;
+- отсутствуют права на запись;
+- произошла ошибка файловой системы;
+- запись в файл невозможна по другой причине.
+
+Создание файла с именем вида:
+
+```text
+students-yyyy-MM-dd-HH-mm.csv
+```
+
+`StudentCsvWriter` **не выполняет**.
+
+## Пример теста: несуществующий файл
+
+Передать путь к несуществующему файлу:
+
+```java
+writer.write(student);
+```
+
+Проверить:
+
+```text
+IOException
+```
+
+Проверить также, что новый файл не был создан.
+
+## Изменение теста существующего файла
+
+Для существующего файла:
+
+```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`. Файл создавать запрещено.
+
+---
+
+# `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/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/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
+```
diff --git a/docs/tasks/RandomStudentGenerator.md b/docs/tasks/RandomStudentGenerator.md
new file mode 100644
index 0000000..3e902f9
--- /dev/null
+++ b/docs/tasks/RandomStudentGenerator.md
@@ -0,0 +1,305 @@
+# Техническое задание: генератор случайных студентов
+
+## Задача
+
+Реализовать класс для формирования `MyList` со случайными значениями.
+
+По умолчанию ограничения диапазона для генерации не задаются. В таком случае значения должны генерироваться во всём допустимом диапазоне, установленном правилами класса `Student`.
+
+Генерация объектов должна выполняться через существующий `StudentBuilder`.
+
+## Название класса
+
+```text id="m9s0q1"
+RandomStudentGenerator
+```
+
+## Контракт класса
+
+Класс должен хранить текущие ограничения генерации для каждого поля `Student`.
+
+Для каждого поля необходимо предоставить по два метода: левое и правое ограничение.
+
+### `groupNumber`
+
+Тип поля: `String`.
+
+```java id="t2z8k4"
+RandomStudentGenerator setMinGroupNumber(String minGroupNumber);
+```
+
+```java id="q7x5n1"
+RandomStudentGenerator setMaxGroupNumber(String maxGroupNumber);
+```
+
+Ограничения применяются при генерации `groupNumber`.
+
+Сравнение границ выполнять в соответствии с правилами, используемыми для `groupNumber` в `Student`.
+
+### `averageGrade`
+
+Тип поля: `double`.
+
+```java id="v4n2j8"
+RandomStudentGenerator setMinAverageGrade(double minAverageGrade);
+```
+
+```java id="r5c1p9"
+RandomStudentGenerator setMaxAverageGrade(double maxAverageGrade);
+```
+
+Диапазон должен быть включительным:
+
+```text id="u3k7w2"
+minAverageGrade <= averageGrade <= maxAverageGrade
+```
+
+### `recordBookNumber`
+
+Тип поля: `int`.
+
+```java id="d8m4x6"
+RandomStudentGenerator setMinRecordBookNumber(int minRecordBookNumber);
+```
+
+```java id="e1q9s5"
+RandomStudentGenerator setMaxRecordBookNumber(int maxRecordBookNumber);
+```
+
+Диапазон должен быть включительным:
+
+```text id="y6p2v8"
+minRecordBookNumber <= recordBookNumber <= maxRecordBookNumber
+```
+
+## Метод генерации
+
+Основной метод:
+
+```java id="k3w7m1"
+MyList generate(int size);
+```
+
+Контракт:
+
+- создаёт ровно `size` объектов `Student`;
+- все объекты должны быть валидными;
+- каждый объект создаётся через `StudentBuilder`;
+- результат возвращается как `MyList`;
+- стандартные `List`, `ArrayList`, `LinkedList` и другие контейнеры использовать нельзя.
+
+## Поведение диапазонов по умолчанию
+
+Если для поля ограничение не задано, используется весь допустимый диапазон этого поля.
+
+Необходимо учитывать существующие ограничения `Student`, в частности:
+
+```text id="z4r8t2"
+averageGrade:
+getMinAverageGrade()
+getMaxAverageGrade()
+
+recordBookNumber:
+getMinRecordBookNumber()
+```
+
+Для `groupNumber` использовать существующий шаблон:
+
+```text id="n5c7y3"
+getGroupNumberPattern()
+```
+
+Генерируемое значение `groupNumber` должно соответствовать требованиям `Student`.
+
+## Проверка ограничений
+
+При установке границ необходимо проверять их корректность.
+
+Недопустимые случаи:
+
+```text id="h2k6p4"
+min > max
+```
+
+Для них выбрасывать:
+
+```java id="c7v3n9"
+IllegalArgumentException
+```
+
+Ограничение также не должно выходить за допустимые значения `Student`.
+
+## Рекомендуемая реализация
+
+Класс должен хранить шесть ограничений:
+
+```text id="b5q9x2"
+minGroupNumber
+maxGroupNumber
+
+minAverageGrade
+maxAverageGrade
+
+minRecordBookNumber
+maxRecordBookNumber
+```
+
+Для каждого поля значения генерируются независимо.
+
+Рекомендуется использовать стандартный генератор случайных чисел Java, например:
+
+```java id="a8f4k1"
+Random
+```
+
+или:
+
+```java id="p3m7d5"
+ThreadLocalRandom
+```
+
+При создании `Student`:
+
+```text id="w6j2r9"
+сгенерировать поля
+ ↓
+StudentBuilder
+ ↓
+build()
+ ↓
+Student
+ ↓
+MyList
+```
+
+Дополнительную валидацию, дублирующую правила `Student`, в генераторе не реализовывать.
+
+## Примеры использования
+
+Без настройки диапазонов:
+
+```java id="s2k8m4"
+RandomStudentGenerator generator =
+ new RandomStudentGenerator();
+
+MyList students =
+ generator.generate(100);
+```
+
+С настройкой диапазонов:
+
+```java id="f7q3n1"
+RandomStudentGenerator generator =
+ new RandomStudentGenerator()
+ .setMinGroupNumber("A00")
+ .setMaxGroupNumber("Z99")
+ .setMinAverageGrade(3.0)
+ .setMaxAverageGrade(5.0)
+ .setMinRecordBookNumber(10000)
+ .setMaxRecordBookNumber(99999);
+
+MyList students =
+ generator.generate(100);
+```
+
+## Тесты
+
+### Генерация без ограничений
+
+```java id="r1w5p8"
+MyList result = generator.generate(100);
+```
+
+Проверить:
+
+- размер равен `100`;
+- все элементы не равны `null`;
+- каждый `Student` валиден.
+
+### Ограничение `averageGrade`
+
+Задать:
+
+```text id="x4n7k2"
+3.0 — 4.0
+```
+
+Проверить, что все значения находятся в указанном диапазоне.
+
+### Ограничение `recordBookNumber`
+
+Задать:
+
+```text id="q9m3v6"
+10000 — 20000
+```
+
+Проверить диапазон и включение границ.
+
+### Ограничение `groupNumber`
+
+Задать левую и правую границы.
+
+Проверить, что все сгенерированные значения находятся между ними согласно правилам сравнения `groupNumber`.
+
+### Граничные значения
+
+Установить:
+
+```text id="p8c4s1"
+min == max
+```
+
+Проверить, что все сгенерированные значения равны указанной границе.
+
+### Некорректный диапазон
+
+Проверить выброс `IllegalArgumentException`, если:
+
+```text id="z6t2h5"
+min > max
+```
+
+### Некорректное ограничение
+
+Передать диапазон, выходящий за допустимые значения `Student`.
+
+Проверить выброс исключения.
+
+### Генерация пустой коллекции
+
+Для:
+
+```java id="j5r8w3"
+generate(0)
+```
+
+ожидается пустой `MyList`.
+
+### Некорректный размер
+
+Для отрицательного размера:
+
+```java id="n2k7p4"
+generate(-1)
+```
+
+ожидается `IllegalArgumentException`.
+
+## Результат
+
+Необходимо реализовать:
+
+```text id="v8m3q6"
+RandomStudentGenerator.java
+```
+
+Класс должен использовать существующие:
+
+```text id="f1j9x4"
+Student.java
+StudentBuilder.java
+MyList.java
+```
+
+Изменять интерфейсы `Student`, `StudentBuilder` и `MyList` для выполнения данного задания не требуется.
diff --git a/docs/tasks/Sorting.md b/docs/tasks/Sorting.md
new file mode 100644
index 0000000..3fc39b0
--- /dev/null
+++ b/docs/tasks/Sorting.md
@@ -0,0 +1,48 @@
+# Техническое задание: сортировка слиянием
+
+## Задача
+
+Реализовать паттерн Strategy и собственный алгоритм сортировки слиянием объектов `Student`.
+
+## Требования
+
+1. Создать интерфейс `SortStrategy` с методом:
+
+```text
+sort(MyList list, Comparator comparator)
+```
+
+1. Создать класс `MergeSortStrategy`, реализующий `SortStrategy`.
+
+2. Реализовать алгоритм сортировки слиянием самостоятельно.
+
+3. Готовые методы сортировки Java не использовать.
+
+4. Сортировка должна работать с `MyList`.
+
+5. Сортировка должна использовать переданный `Comparator`.
+
+6. Реализовать сортировку `Student` по каждому из трёх полей:
+
+ - `groupNumber`;
+ - `averageGrade`;
+ - `recordBookNumber`.
+
+7. Создать отдельные компараторы:
+
+ - `StudentGroupComparator`;
+ - `StudentAverageGradeComparator`;
+ - `StudentRecordBookComparator`.
+
+8. `MergeSortStrategy` не должен зависеть от класса `Student`.
+
+## Результат
+
+```text
+SortStrategy.java
+MergeSortStrategy.java
+
+StudentGroupComparator.java
+StudentAverageGradeComparator.java
+StudentRecordBookComparator.java
+```
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