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 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 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 + implements Collector, MyList> { +} +``` + +Реализовать методы: + +```text +supplier() +accumulator() +combiner() +finisher() +characteristics() +``` + +Назначение: + +- `supplier()` — создать пустой `MyList`; +- `accumulator()` — добавить очередной элемент в `MyList`; +- `combiner()` — объединить две частично заполненные коллекции; +- `finisher()` — вернуть готовый `MyList`; +- `characteristics()` — определить характеристики сборщика. + +Коллектор должен работать с `Student` без специальной логики, связанной с его полями. + +--- + +## `MyCollectors` + +Создать вспомогательный класс: + +```java +public final class MyCollectors { + + public static Collector, MyList> toMyList() { + return new MyListCollector<>(); + } +} +``` + +Класс должен предоставлять удобный способ получения собственного сборщика. + +--- + +## Контракт использования + +Должна поддерживаться конструкция: + +```java +MyList students = Stream.of( + student1, + student2, + student3 +).collect(MyCollectors.toMyList()); +``` + +где `student1`, `student2`, `student3` — валидные объекты `Student`, предварительно созданные через `StudentBuilder`. + +Результатом должен быть именно `MyList`. + +--- + +## Пример `map` + +`map` должен использоваться для преобразования одного `Student` в новый `Student`. + +Исходный объект не изменяется. Для создания нового объекта использовать `with...()`. + +Одно из преобразуемых полей должно быть `String` и формироваться по заданному бизнес-правилу: + +```text +Заглавная латинская буква + двузначное число +``` + +Например: + +```text +A07 +B12 +Z35 +``` + +Пример: + +```java +MyList students = Stream.of( + student1, + student2, + student3 +) +.map(student -> student.withGroupNumber( + generateGroupNumber(student) +)) +.collect(MyCollectors.toMyList()); +``` + +Пример функции формирования значения: + +```java +private static String generateGroupNumber(Student student) { + int number = Math.abs(student.getRecordBookNumber().hashCode() % 100); + char letter = (char) ('A' + + Math.abs(student.getRecordBookNumber().hashCode()) % 26); + + return String.format("%c%02d", letter, number); +} +``` + +Результат `map` должен оставаться `Stream`, после чего он непосредственно собирается в `MyList`. + +Формат создаваемого `groupNumber` должен соответствовать: + +```text +^[A-Z][0-9]{2}$ +``` + +--- + +## Пример `filter` совместно с `map` + +Проверить совместную работу промежуточных операций: + +```java +MyList students = Stream.of( + student1, + student2, + student3 +) +.filter(student -> student.getAverageGrade() >= 4.0) +.map(student -> student.withGroupNumber( + generateGroupNumber(student) +)) +.collect(MyCollectors.toMyList()); +``` + +Проверить, что: + +- в результат попали только студенты со средним баллом не ниже `4.0`; +- каждый результат является новым `Student`; +- `groupNumber` соответствует формату `[A-Z][0-9]{2}`; +- исходные объекты не изменились. + +--- + +## Параллельные потоки + +`combiner()` должен корректно объединять две частично сформированные `MyList`. + +Реалистичный пример: + +```java +MyList students = IntStream.range(0, 10_000) + .parallel() + .mapToObj(i -> student1.withGroupNumber( + String.format("%c%02d", + 'A' + (i % 26), + i % 100) + )) + .collect(MyCollectors.toMyList()); +``` + +Проверить: + +- результат содержит `10_000` объектов; +- ни один объект не потерян; +- количество объектов не превышает ожидаемого; +- каждый `groupNumber` соответствует формату `[A-Z][0-9]{2}`; +- объекты `Student` не изменяются после создания; +- `combiner()` корректно объединяет частичные результаты. + +--- + +## Примеры тестов + +### Заполнение несколькими `Student` + +Создать три валидных объекта `Student` через `StudentBuilder` и собрать их: + +```java +MyList result = Stream.of( + student1, + student2, + student3 +).collect(MyCollectors.toMyList()); +``` + +Проверить: + +```text +result != null +result.size() == 3 +``` + +и соответствие элементов исходному потоку. + +### Проверка порядка + +Для: + +```text +student1 +student2 +student3 +``` + +проверить: + +```text +result.get(0) == student1 +result.get(1) == student2 +result.get(2) == student3 +``` + +### Пустой поток + +```java +MyList result = Stream.empty() + .collect(MyCollectors.toMyList()); +``` + +Проверить: + +```text +result != null +result.isEmpty() +``` + +### Проверка `map` + +Преобразовать несколько `Student` через `withGroupNumber()`. + +Проверить: + +- исходные объекты не изменились; +- созданные объекты отличаются от исходных; +- `groupNumber` соответствует формату `[A-Z][0-9]{2}`. + +### Проверка `filter` и `map` + +Проверить, что после `filter` и `map` в `MyList` попадают только необходимые студенты с корректно сформированным `groupNumber`. + +### Параллельный поток + +Создать поток из `10_000` объектов через `parallel()`. + +Проверить размер итоговой коллекции, отсутствие потери элементов и корректность `groupNumber`. + +--- + +## Ограничения + +Запрещено: + +- использовать `Collectors.toList()`; +- использовать `ArrayList`, `LinkedList` или другие стандартные коллекции для накопления результата; +- сначала собирать `Student` в стандартную коллекцию, а затем переносить их в `MyList`; +- создавать `Student` внутри `MyListCollector`; +- изменять существующий `Student`; +- использовать `StudentBuilder` внутри `map` вместо `with...()`. + +## Результат + +Реализовать: + +```text +MyListCollector.java +MyCollectors.java +``` + +Использовать существующие: + +```text +Student.java +StudentBuilder.java +MyList.java +MyLinkedList.java +``` + +### Рекомендация по архитектуре + +`MyListCollector` должен зависеть только от интерфейса `MyList`, а не от конкретной реализации `MyLinkedList`. + +`MyCollectors` должен содержать только методы создания собственных сборщиков и не должен содержать бизнес-логику. diff --git a/docs/tasks/Student.md b/docs/tasks/Student.md new file mode 100644 index 0000000..2e17172 --- /dev/null +++ b/docs/tasks/Student.md @@ -0,0 +1,46 @@ +# Техническое задание: класс `Student` + +## Задача + +Реализовать класс `Student` как единственную точку проверки корректности данных. Объект класса `Student` не может быть создан с некорректными данными. + +## Требования + +1. Класс `Student` содержит три поля: + + - `groupNumber`; + - `averageGrade`; + - `recordBookNumber`. + +2. Все поля должны быть `private` и неизменяемыми после создания объекта. + +3. Проверка корректности данных должна находиться в классе `Student`. + +4. Реализовать класс `StudentBuilder` для создания объектов `Student`. + +5. В `StudentBuilder` реализовать методы: + + - `setGroupNumber(...)`; + - `setAverageGrade(...)`; + - `setRecordBookNumber(...)`; + - `build()`. + +6. Метод `build()` должен создавать только корректный объект `Student`. При некорректных данных должно выбрасываться исключение. + +7. Реализовать для `Student`: + + - методы получения значений полей; + - `toString()`; + - `equals()`; + - `hashCode()`. + +## Результат + +Должны быть реализованы два независимых класса: + +```text +Student.java +StudentBuilder.java +``` + +Классы не должны зависеть от сортировки, коллекции, меню и ввода данных. diff --git a/docs/tasks/StudentOccurrenceIndexFinder.md b/docs/tasks/StudentOccurrenceIndexFinder.md new file mode 100644 index 0000000..0c6f202 --- /dev/null +++ b/docs/tasks/StudentOccurrenceIndexFinder.md @@ -0,0 +1,688 @@ +# Техническое задание: многопоточный поиск индексов `Student` + +## Задача + +Реализовать многопоточный поиск всех вхождений заданного объекта `Student` в `MyList`. + +Метод **не должен выводить никаких сообщений в консоль**. + +Результатом работы должен быть массив `int[]`, содержащий индексы всех элементов исходной коллекции, которые равны переданному `Student`. + +Сравнение выполнять через существующий `Student.equals()`. + +--- + +## Входная информация + +Основной метод принимает: + +```java +MyList students +``` + +Исходная коллекция студентов. + +```java +Student target +``` + +Студент, вхождения которого необходимо найти. + +--- + +## Выходная информация + +Метод возвращает: + +```java +int[] +``` + +Массив должен содержать индексы всех совпавших элементов. + +Пример: + +```text +Исходная коллекция: + +[Student A, Student B, Student A, Student C, Student A] + +Результат: + +[0, 2, 4] +``` + +Индексы должны быть расположены в порядке возрастания. + +При отсутствии совпадений: + +```text +[] +``` + +При пустой коллекции: + +```text +[] +``` + +--- + +# Название класса + +```text +StudentOccurrenceIndexFinder +``` + +--- + +# Публичные методы и контракты + +## `findOccurrences` + +```java +public int[] findOccurrences( + MyList students, + Student target +); +``` + +Метод должен: + +1. Проверить входные параметры. +2. Разделить коллекцию на диапазоны индексов. +3. Передать каждый диапазон отдельной задаче. +4. Выполнить поиск в нескольких потоках. +5. Получить от каждого потока найденные индексы. +6. Объединить результаты. +7. Вернуть индексы в порядке возрастания. +8. Не изменять исходную коллекцию. +9. Не изменять объекты `Student`. +10. Не выводить ничего в консоль. + +--- + +# Вспомогательные методы + +Количество и вид `private` методов могут быть выбраны разработчиком. + +Рекомендуется выделить: + +```java +private int calculateThreadCount(int size); +``` + +Определяет количество рабочих потоков. + +```java +private int countChunkSize(int size, int threadCount); +``` + +Определяет размер диапазона для одной задачи. + +```java +private MyList findInRange( + MyList students, + Student target, + int fromIndex, + int toIndex +); +``` + +Обрабатывает диапазон: + +```text +[fromIndex, toIndex) +``` + +и возвращает индексы найденных совпадений. + +--- + +# Работа с многопоточностью + +## Рекомендуемый подход + +Не использовать общий контейнер результатов, к которому одновременно обращаются несколько потоков. + +Также **не рекомендуется использовать мьютексы** (`synchronized`, `Lock`, `ReentrantLock`) для хранения результата. + +Лучше использовать модель: + +```text + MyList + │ + ┌────────────┼────────────┐ + ↓ ↓ ↓ + Поток 1 Поток 2 Поток 3 + диапазон диапазон диапазон + │ │ │ + ↓ ↓ ↓ + MyList MyList MyList + └────────────┼────────────┘ + ↓ + основной поток + ↓ + merge + ↓ + int[] +``` + +Каждый поток создаёт **собственный контейнер `MyList`** и записывает найденные индексы только в него. + +Это позволяет избежать состояния гонки без синхронизации общей структуры. + +--- + +# Разбиение коллекции на потоки + +Количество потоков рекомендуется определять на основании: + +```java +Runtime.getRuntime().availableProcessors() +``` + +Количество потоков не должно превышать количество элементов: + +```text +threadCount = min( + availableProcessors(), + students.size() +) +``` + +### Пример + +Для коллекции из `10000` элементов и четырёх рабочих потоков: + +```text +Поток 1 → [0, 2500) +Поток 2 → [2500, 5000) +Поток 3 → [5000, 7500) +Поток 4 → [7500, 10000) +``` + +Каждый индекс должен принадлежать **ровно одному диапазону**. + +Не допускаются: + +- пропущенные индексы; +- обработка одного индекса несколькими потоками. + +--- + +# Поиск внутри потока + +Каждая задача обрабатывает только свой диапазон. + +Для каждого индекса: + +```java +Student student = students.get(index); +``` + +затем: + +```java +target.equals(student) +``` + +Если результат `true`, индекс добавляется в локальный: + +```java +MyList +``` + +Пример: + +```text +Поток 1 → [0, 8, 15] +Поток 2 → [21, 24] +Поток 3 → [30, 42, 48] +Поток 4 → [63] +``` + +--- + +# Объединение результатов + +Основной поток после завершения всех задач должен получить локальные результаты и объединить их. + +Рекомендуется передавать задачи в порядке возрастания диапазонов: + +```text +Результат 1 → диапазон 0 +Результат 2 → диапазон 1 +Результат 3 → диапазон 2 +... +``` + +Если каждая задача добавляет индексы в возрастающем порядке, а результаты объединяются в порядке диапазонов, итоговый массив уже будет отсортирован: + +```text +[0, 8, 15, 21, 24, 30, 42, 48, 63] +``` + +В этом случае дополнительная сортировка результата не требуется. + +**`Arrays.sort()` и другие готовые методы сортировки использовать не следует**, поскольку порядок можно гарантировать самим алгоритмом объединения. + +--- + +# Потокобезопасность + +## Мьютексы + +Использование мьютексов для результата **не требуется**. + +Не рекомендуется использовать: + +```java +synchronized +``` + +```java +Lock +``` + +```java +ReentrantLock +``` + +```java +AtomicInteger +``` + +для общего результата. + +Причина: потоки не должны изменять одну общую структуру. + +Каждый поток работает со своим: + +```text +MyList +``` + +а объединение выполняется после завершения параллельной части одним потоком. + +--- + +# Рекомендуемые классы и импорты + +Для реализации рекомендуется использовать стандартные средства Java: + +```java +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +``` + +Рекомендуемая схема: + +```text +ExecutorService + ↓ +Callable> + ↓ +Future> +``` + +Каждый `Callable` обрабатывает свой диапазон и возвращает собственный `MyList`. + +После получения всех `Future` основной метод объединяет результаты. + +После завершения работы `ExecutorService` должен быть корректно закрыт. + +При `InterruptedException` необходимо восстановить статус прерывания текущего потока: + +```java +Thread.currentThread().interrupt(); +``` + +--- + +# Использование `MyList` + +Сторонние контейнеры **не допускаются**. + +Запрещено использовать: + +```text +ArrayList +LinkedList +Vector +CopyOnWriteArrayList +ConcurrentLinkedQueue +и другие стандартные контейнеры +``` + +как хранилище промежуточных или итоговых результатов. + +Для результатов каждого потока использовать: + +```java +MyList +``` + +Итог преобразовать в: + +```java +int[] +``` + +--- + +# Требуется ли изменять `MyList` + +Текущего интерфейса **достаточно для реализации задачи**: + +```java +public interface MyList { + + void add(T element); + + T get(int index); + + T set(int index, T element); + + T remove(int index); + + int size(); + + boolean isEmpty(); +} +``` + +Дополнительные методы добавлять **не требуется**. + +Для задачи достаточно: + +```java +size() +get(index) +add(index) +``` + +Поскольку поиск выполняется только на чтение, а результаты каждого потока добавляются в его собственный `MyList`, существующего интерфейса достаточно. + +### Важно + +Метод: + +```java +void add(T element); +``` + +должен добавлять элемент в конец коллекции. + +Это позволяет каждому потоку формировать свои индексы в порядке их обнаружения. + +--- + +# Использование `Student` + +Изменения в `Student` **не требуются**. + +Использовать существующий метод: + +```java +equals(Object obj) +``` + +Сравнение выполнять: + +```java +target.equals(students.get(index)) +``` + +Не использовать: + +```text +== +``` + +Не сравнивать объекты вручную по отдельным полям. + +Не использовать `toString()` для определения совпадения. + +--- + +# `StudentBuilder` + +Изменения в `StudentBuilder` **не требуются**. + +Он не участвует в алгоритме поиска. + +--- + +# Обработка входных данных + +Если: + +```java +students == null +``` + +выбросить: + +```java +IllegalArgumentException +``` + +Если: + +```java +target == null +``` + +выбросить: + +```java +IllegalArgumentException +``` + +Для пустой коллекции вернуть: + +```java +new int[0] +``` + +Без создания рабочих потоков. + +--- + +# Обобщённый алгоритм реализации + +```text +1. Проверить students и target. +2. Получить размер коллекции. +3. Если размер равен 0 → вернуть []. +4. Вычислить количество потоков. +5. Разделить индексы коллекции на непересекающиеся диапазоны. +6. Создать Callable для каждого диапазона. +7. Запустить задачи через ExecutorService. +8. Каждый поток: + - получает fromIndex и toIndex; + - проходит свой диапазон; + - сравнивает Student через equals(); + - записывает найденные индексы в собственный MyList. +9. Основной поток получает результаты через Future. +10. Объединяет MyList в порядке диапазонов. +11. Преобразует результат в int[]. +12. Возвращает массив. +``` + +--- + +# Примеры тестов + +## Одно совпадение + +```text +[A, B, C, D] +``` + +Искомый: + +```text +C +``` + +Ожидается: + +```text +[2] +``` + +## Несколько совпадений + +```text +[A, B, A, C, A] +``` + +Ожидается: + +```text +[0, 2, 4] +``` + +## Нет совпадений + +```text +[A, B, C] +``` + +Искомый: + +```text +D +``` + +Ожидается: + +```text +[] +``` + +## Все элементы совпадают + +```text +[A, A, A, A] +``` + +Ожидается: + +```text +[0, 1, 2, 3] +``` + +## Пустая коллекция + +Ожидается: + +```text +[] +``` + +## Один элемент + +Проверить: + +```text +[A] + A → [0] +[A] + B → [] +``` + +## Проверка границ диапазонов потоков + +Проверить коллекции размеров: + +```text +1 +2 +3 +4 +10 +100 +1000 +``` + +Особое внимание уделить совпадениям: + +```text +на последнем элементе одного диапазона; +на первом элементе следующего диапазона. +``` + +Каждый индекс должен быть найден ровно один раз. + +## Проверка порядка результата + +Создать совпадения в разных диапазонах. + +Проверить, что результат имеет вид: + +```text +[0, 3, 7, 12, 25, 49, 100] +``` + +и находится в порядке возрастания без использования дополнительной сортировки. + +## Проверка `Student.equals()` + +Создать два разных экземпляра `Student` с одинаковыми значениями полей. + +Один поместить в коллекцию, второй передать как `target`. + +Проверить, что индекс считается совпадением согласно `equals()`. + +## Проверка неизменности коллекции + +После выполнения метода проверить: + +- размер `MyList`; +- порядок элементов; +- значения всех элементов. + +## Проверка отсутствия вывода + +Перехватить стандартный вывод и убедиться, что выполнение `findOccurrences()` не выводит сообщений в консоль. + +## Большая коллекция + +Создать коллекцию, например из `100000` студентов, с заранее известными индексами совпадений. + +Проверить: + +- все ожидаемые индексы найдены; +- лишних индексов нет; +- индексы идут по возрастанию; +- итоговый массив имеет правильный размер. + +## Git + +Название ветки: + +```text +feature/student-occurrence-index-finder +``` + +## Результат + +Необходимо реализовать: + +```text +StudentOccurrenceIndexFinder.java +``` + +Существующие интерфейсы и классы: + +```text +MyList.java +Student.java +StudentBuilder.java +``` + +изменять не требуется. diff --git a/docs/tasks/StudentUpdate.md b/docs/tasks/StudentUpdate.md new file mode 100644 index 0000000..e3db385 --- /dev/null +++ b/docs/tasks/StudentUpdate.md @@ -0,0 +1,326 @@ +# Техническое задание: обновление `Student` + +## Задача + +Обновить класс `Student` и связанный с ним код в соответствии с новой моделью данных и требованиями к неизменяемости объекта. + +Текущая реализация содержит: + +```java +private final String groupNumber; +private final double averageGrade; +private final String recordBookNumber; +``` + +Необходимо изменить тип `recordBookNumber` и добавить методы создания изменённых копий `Student`. + +## Изменения в `Student` + +### 1. Изменить тип `recordBookNumber` + +Поле: + +```java +private final String recordBookNumber; +``` + +заменить на: + +```java +private final int recordBookNumber; +``` + +Метод: + +```java +getRecordBookNumber() +``` + +должен возвращать `int`. + +Все проверки, `equals()`, `hashCode()` и `toString()` должны быть адаптированы под новый тип. + +### 2. Добавить бизнес-правило для `groupNumber` + +`groupNumber` должен соответствовать строго следующему формату: + +```text +<Заглавная буква латинского алфавита><2 цифры> +``` + +Регулярное выражение: + +```text +^[A-Z][0-9]{2}$ +``` + +Допустимые значения: + +```text +A12 +B32 +Z00 +``` + +Недопустимые значения: + +```text +а12 +a12 +A1 +A123 +AA1 +а1А +аа!!а123 +23фи!12 +№№###*() +``` + +Проверка должна выполняться при создании `Student`. + +`Student` не может существовать с некорректным `groupNumber`. + +### 3. Проверить `averageGrade` + +Сохранить правило: + +```text +0.0 <= averageGrade <= 5.0 +``` + +Значения `0.0` и `5.0` допустимы. + +### 4. Проверить `recordBookNumber` + +После преобразования в `int` установить бизнес-правило: + +```text +recordBookNumber > 0 +``` + +Значение `0` и отрицательные значения считаются недопустимыми. + +--- + +# Методы `with...()` + +Добавить методы, возвращающие **новый объект `Student`**, не изменяя текущий объект. + +Необходимо реализовать: + +```java +withGroupNumber(String groupNumber) +withAverageGrade(double averageGrade) +withRecordBookNumber(int recordBookNumber) +``` + +Каждый метод должен: + +1. Сохранять исходный объект без изменений. +2. Изменять только указанное поле в новом объекте. +3. Сохранять остальные поля исходного объекта. +4. Повторно применять все правила валидации. +5. Возвращать новый валидный объект `Student`. + +Пример: + +```java +Student updated = student.withAverageGrade(4.8); +``` + +После выполнения: + +```text +student.getAverageGrade() == прежнее значение +updated.getAverageGrade() == 4.8 +``` + +Если новое значение невалидно, метод должен выбросить исключение, а исходный объект не должен измениться. + +--- + +# `StudentBuilder` + +Обновить существующий `StudentBuilder` в соответствии с новой моделью `Student`. + +Методы должны соответствовать типам: + +```java +setGroupNumber(String groupNumber) +setAverageGrade(double averageGrade) +setRecordBookNumber(int recordBookNumber) +``` + +`build()` должен создавать только валидный `Student`. + +Все новые правила валидации должны применяться при создании объекта через `StudentBuilder`. + +--- + +# Обновление зависимого кода + +Найти и обновить весь код, использующий `Student` и изменяемые методы или типы. + +В частности, обновить: + +- `Comparator`; +- сортировки; +- источники данных; +- тестовые классы; +- создание `Student`; +- `with...()`-операции; +- чтение и запись данных, если они используют `recordBookNumber`. + +Например, существующий: + +```java +implements Comparator +``` + +должен использовать актуальный метод: + +```java +getAverageGrade() +``` + +вместо ошибочного `getAvarageGrade()`. + +Компаратор `recordBookNumber` должен сравнивать значения как числа. + +Например: + +```java +Integer.compare( + s1.getRecordBookNumber(), + s2.getRecordBookNumber() +) +``` + +Все места, где `recordBookNumber` рассматривался как `String`, необходимо адаптировать к `int`. + +--- + +# Тесты + +Обновить существующий `StudentTest` с учётом новой модели. + +## Создание корректного `Student` + +Проверить создание объекта со значениями: + +```text +groupNumber = A12 +averageGrade = 4.5 +recordBookNumber = 12345 +``` + +Проверить корректность всех геттеров. + +## Проверка `groupNumber` + +Проверить успешное создание для: + +```text +A12 +B32 +Z00 +``` + +Проверить исключение для: + +```text +a12 +A1 +A123 +AA1 +а1А +аа!!а123 +23фи!12 +№№###*() +``` + +## Проверка `averageGrade` + +Проверить: + +```text +0.0 — допустимо +5.0 — допустимо +-1.0 — недопустимо +6.0 — недопустимо +``` + +## Проверка `recordBookNumber` + +Проверить: + +```text +1 — допустимо +12345 — допустимо +0 — недопустимо +-1 — недопустимо +``` + +## Тесты `with...()` + +### `withGroupNumber()` + +Проверить, что: + +- возвращается новый объект; +- `groupNumber` изменён; +- `averageGrade` не изменился; +- `recordBookNumber` не изменился; +- исходный `Student` остался прежним. + +### `withAverageGrade()` + +Проверить те же свойства для `averageGrade`. + +### `withRecordBookNumber()` + +Проверить те же свойства для `recordBookNumber`. + +### `with...()` с невалидным значением + +Для каждого метода передать некорректное значение. + +Проверить: + +- выбрасывается `IllegalArgumentException`; +- исходный объект не изменяется; +- не создаётся невалидный `Student`. + +## `equals()` и `hashCode()` + +Обновить существующие тесты с учётом `int recordBookNumber`. + +Проверить: + +- одинаковые студенты равны; +- одинаковые студенты имеют одинаковый `hashCode`; +- изменение любого поля создаёт отличающийся объект. + +## Результат + +Необходимо обновить: + +```text +Student.java +StudentBuilder.java +StudentTest.java +``` + +а также **все классы проекта, зависящие от изменённого интерфейса `Student`**, включая компараторы и другие реализации, использующие `recordBookNumber`, `groupNumber`, `averageGrade` или их геттеры. + +### Основной принцип + +`Student` должен оставаться единственной точкой истины для валидности данных: + +```text +StudentBuilder ──┐ + ├──> Student +with...() ───────┘ +``` + +Ни `Builder`, ни `with...()`, ни внешние классы не должны позволять создать невалидный объект. diff --git a/docs/tasks/Testing.md b/docs/tasks/Testing.md new file mode 100644 index 0000000..26f898c --- /dev/null +++ b/docs/tasks/Testing.md @@ -0,0 +1,67 @@ +# Общее техническое задание: тестирование + +## Задача + +Для всех реализованных классов написать автоматические тесты с использованием **JUnit**. + +## Требования + +1. Для каждого основного класса проекта должны быть написаны тесты. + +2. Проверить: + + - корректное выполнение основных методов; + - корректную обработку некорректных данных; + - граничные случаи; + - ожидаемые исключения. + +3. Для `Student` проверить: + + - создание корректного объекта; + - невозможность создания объекта с некорректными данными; + - работу `Builder`; + - `equals()` и `hashCode()`. + +4. Для `MyLinkedList` проверить: + + - `add()`; + - `get()`; + - `set()`; + - `remove()`; + - `size()`; + - `isEmpty()`; + - некорректные индексы. + +5. Для `MergeSortStrategy` проверить: + + - сортировку по каждому из трёх полей; + - уже отсортированную коллекцию; + - обратный порядок; + - коллекцию из одного элемента; + - пустую коллекцию. + +6. Для каждого компаратора проверить правильное сравнение объектов `Student`. + +7. Тесты должны быть независимыми друг от друга и воспроизводимыми. + +8. Необходимо использовать JUnit и стандартные средства проверки результатов. + +## Результат + +Тесты размещать отдельно от основного кода, например: + +```text +src/ +├── main/ +│ └── java/ +└── test/ + └── java/ + ├── StudentTest.java + ├── MyLinkedListTest.java + ├── MergeSortStrategyTest.java + ├── StudentGroupComparatorTest.java + ├── StudentAverageGradeComparatorTest.java + └── StudentRecordBookComparatorTest.java +``` + +Все тесты должны успешно проходить перед слиянием ветки в `main`. diff --git a/tutorials/git/GIT_BRANCH_DEPENDENCIES.ru.md b/tutorials/git/GIT_BRANCH_DEPENDENCIES.ru.md new file mode 100644 index 0000000..f427e43 --- /dev/null +++ b/tutorials/git/GIT_BRANCH_DEPENDENCIES.ru.md @@ -0,0 +1,122 @@ +# Работа с зависимостями между Git-ветками + +## Сценарий + +Предположим, мы находимся в ветке: + +```bash +feature/student +``` + +В этой ветке реализуется функциональность, связанная со `Student`. + +При этом у нас появляется новая, независимая по смыслу фича: + +```bash +feature/sorting +``` + +Для реализации сортировки нам необходим класс или интерфейс `Student`, который уже был создан в `feature/student`. + +## Как создать новую ветку + +Если нам необходимо продолжить разработку `feature/sorting`, используя текущее состояние `feature/student`, создаём новую ветку **от `feature/student`**. + +Сначала переключаемся на `feature/student`: + +```bash +git checkout feature/student +``` + +Затем создаём новую ветку: + +```bash +git branch feature/sorting +``` + +После этого история будет выглядеть примерно так: + +```text + feature/sorting + / +---------------● + \ + feature/student +``` + +Обе ветки на момент создания указывают на один и тот же коммит. + +Теперь в `feature/sorting` доступен весь код, который был добавлен в `feature/student`, в том числе `Student`. + +Чтобы перейти в новую ветку: + +```bash +git checkout feature/sorting +``` + +## Почему важно создавать ветку именно таким образом + +В данном случае `feature/sorting` зависит от кода, находящегося в `feature/student`. + +При этом сами фичи логически разные: + +- `feature/student` — разработка `Student`; +- `feature/sorting` — реализация сортировки; +- `Student` нужен в `feature/sorting` как зависимость для реализации сортировки. + +Поэтому не стоит продолжать разработку сортировки непосредственно в `feature/student`. Лучше создать отдельную ветку от того состояния, где необходимый код уже существует. + +--- + +## Если ветки уже разошлись + +Иногда невозможно создать `feature/sorting` непосредственно от нужного места в истории `feature/student`. + +Например, `feature/sorting` уже была создана раньше: + +```text +A---B---C---D feature/student + \ + E---F---G feature/sorting +``` + +В таком случае можно получить изменения из `feature/student` посредством слияния (merge). + +Находясь в `feature/sorting`, выполняем: + +```bash +git checkout feature/sorting +git merge feature/student +``` + +История станет примерно такой: + +```text +A---B---C---D feature/student + \ \ + E---F---G---M feature/sorting +``` + +Коммит `M` — результат слияния двух веток. + +После этого в `feature/sorting` будут доступны изменения из `feature/student`, в том числе необходимый `Student`. + +## Главное правило + +**Если новая фича зависит от кода из другой ветки, но при этом является самостоятельной по смыслу — создаём для неё отдельную ветку, используя нужную ветку как основу.** + +Если новую ветку можно создать непосредственно от нужного состояния исходной ветки: + +```bash +git checkout feature/student +git branch feature/sorting +``` + +Если ветки уже разошлись и создать её от нужного состояния невозможно: + +```bash +git checkout feature/sorting +git merge feature/student +``` + +Таким образом, мы сохраняем логическое разделение фич и одновременно можем использовать необходимый код из другой ветки. diff --git a/GIT_CONFIG.ru.md b/tutorials/git/GIT_CONFIG.ru.md similarity index 100% rename from GIT_CONFIG.ru.md rename to tutorials/git/GIT_CONFIG.ru.md diff --git a/tutorials/git/GIT_FLOW.ru.md b/tutorials/git/GIT_FLOW.ru.md new file mode 100644 index 0000000..02f6063 --- /dev/null +++ b/tutorials/git/GIT_FLOW.ru.md @@ -0,0 +1,1201 @@ +# Git Flow: правила разработки в команде + +## Назначение + +Этот документ описывает принятый в команде порядок работы с Git Flow: + +- какие ветки используются; +- от каких веток создавать новые; +- куда вливать изменения; +- как работать с `feature`, `release`, `hotfix`; +- как работать с зависимыми feature; +- как синхронизировать локальные ветки; +- как безопасно публиковать изменения; +- что делать через Pull Request. + +Цель — сделать историю изменений предсказуемой и избежать ситуаций, когда изменения случайно попадают не в ту ветку. + +--- + +## 1. Основная модель веток + +В классическом Git Flow используются две основные долгоживущие ветки: + +- `main` — код, находящийся в production; +- `develop` — основная ветка текущей разработки. + +Временные ветки: + +- `feature/*` — разработка новой функциональности; +- `release/*` — подготовка релиза; +- `hotfix/*` — срочное исправление production; +- `bugfix/*` — исправление ошибок в процессе разработки; +- `support/*` — поддержка старых версий, если это необходимо проекту. + +Базовая схема: + +```text + feature/* + / \ + / \ + / \ +main ─────────────────●──────────────●───────────── + ↑ ↑ + │ │ + release hotfix + │ │ + └──────┐ ┌─────┘ + ↓ ↓ +develop ────────────────●────●─●────────────────── + ↑ + │ + feature +``` + +Главное правило: + +```text +feature/* → develop +release/* → main +release/* → develop +hotfix/* → main +hotfix/* → develop +``` + +--- + +# 2. Что является источником для каждой ветки + +## Feature + +Feature создаётся от: + +```text +develop → feature/* +``` + +Пример: + +```bash +git flow feature start user-profile +``` + +Получается: + +```text +develop + \ + feature/user-profile +``` + +Новые feature по умолчанию **не создаются от `main`**. + +--- + +## Release + +Release создаётся от: + +```text +develop → release/* +``` + +Пример: + +```bash +git flow release start 2.4.0 +``` + +Получается: + +```text +develop + \ + release/2.4.0 +``` + +Release используется для подготовки уже готового набора изменений к production. + +В release обычно не добавляют новые большие функциональные возможности. + +--- + +## Hotfix + +Hotfix создаётся от: + +```text +main → hotfix/* +``` + +Пример: + +```bash +git flow hotfix start 2.4.1 +``` + +Используйте `hotfix`, когда проблема уже находится в production и исправление нельзя ждать до следующего обычного релиза. + +--- + +## Bugfix + +`bugfix` используется для исправления ошибок в процессе разработки. + +В зависимости от конфигурации `git-flow` такая ветка обычно является веткой разработки и должна в конечном итоге попасть в `develop`. + +Пример: + +```bash +git flow bugfix start fix-registration +``` + +--- + +# 3. Основной рабочий цикл Feature + +Обычная разработка выглядит так: + +```text +develop + │ + ├── feature/login + │ + ├── feature/payment + │ + └── feature/profile + │ + └────────→ develop +``` + +## Шаг 1. Обновить develop + +Перед началом работы необходимо синхронизировать локальный `develop`: + +```bash +git checkout develop +git pull origin develop +``` + +## Шаг 2. Создать feature + +```bash +git flow feature start user-profile +``` + +## Шаг 3. Работать и делать коммиты + +```bash +git add . +git commit -m "feat: add user profile" +``` + +Коммиты должны быть небольшими и логически связанными. + +## Шаг 4. Опубликовать feature + +Если работа ведётся через remote: + +```bash +git flow feature publish user-profile +``` + +или: + +```bash +git push -u origin feature/user-profile +``` + +## Шаг 5. Создать Pull Request + +Из: + +```text +feature/user-profile +``` + +в: + +```text +develop +``` + +После ревью и успешного прохождения CI изменения можно вливать в `develop`. + +--- + +# 4. Как вливать Feature в develop + +Есть два допустимых варианта. + +## Вариант A — через Pull Request + +Для командной разработки это предпочтительный способ: + +```text +feature/user-profile + │ + │ Pull Request + ↓ + develop +``` + +Плюсы: + +- code review; +- автоматические проверки; +- прозрачная история; +- возможность обсуждать изменения до merge. + +--- + +## Вариант B — через git-flow локально + +Можно использовать: + +```bash +git flow feature finish user-profile +``` + +Эта команда завершает feature и вливает её в `develop`. + +После этого изменения нужно отправить в remote: + +```bash +git push origin develop +``` + +Если проект использует обязательный Pull Request, локальный `feature finish` обычно не используется для финального merge. + +--- + +# 5. Важно: не все Feature должны зависеть друг от друга + +Предпочтительная структура: + +```text +develop + ├── feature/A + ├── feature/B + └── feature/C +``` + +Нежелательная структура: + +```text +develop + └── feature/A + └── feature/B + └── feature/C +``` + +Каждая feature по возможности должна начинаться от `develop`. + +--- + +# 6. Что делать, если Feature зависит от другой Feature + +Иногда `feature/B` действительно требует незавершённых изменений из `feature/A`. + +Например: + +```text +develop + \ + feature/A + \ + feature/B +``` + +В этом случае временно допустимо создать `feature/B` от `feature/A`. + +Пример: + +```bash +git checkout feature/A +git checkout -b feature/B +``` + +Но нужно понимать, что теперь B зависит от A. + +После завершения A желательно вернуть B на обычную базу: + +```text +develop + ├── feature/A + └── feature/B +``` + +Например, после merge A в `develop`: + +```bash +git checkout feature/B +git rebase develop +``` + +или выполнить merge: + +```bash +git checkout feature/B +git merge develop +``` + +Выбор между `rebase` и `merge` зависит от правил проекта. + +Главное правило: + +> Зависимая feature — временное исключение, а не стандартная структура проекта. + +Подробнее о зависимостях см. `GIT_BRANCH_DEPENDENCIES.ru.md`. + +--- + +# 7. Как обновлять Feature от develop + +Пока feature разрабатывается, в `develop` могут появляться новые изменения. + +Периодически feature необходимо синхронизировать. + +### Вариант с rebase + +```bash +git checkout feature/user-profile +git fetch origin +git rebase origin/develop +``` + +### Вариант с merge + +```bash +git checkout feature/user-profile +git fetch origin +git merge origin/develop +``` + +Необходимо использовать тот вариант, который принят в правилах конкретного репозитория. + +Если используется `rebase` для уже опубликованной ветки, после него может понадобиться: + +```bash +git push --force-with-lease +``` + +Использовать: + +```bash +git push --force +``` + +не рекомендуется. + +Подробнее о безопасном использовании `--force-with-lease` см. в: + +`GIT_FORCE_WITH_LEASE_RECOVERY.ru.md` + +--- + +# 8. Release Flow + +Когда набор изменений готов к релизу: + +```text +develop + │ + └──→ release/2.4.0 +``` + +Создание: + +```bash +git flow release start 2.4.0 +``` + +Далее выполняется: + +- финальное тестирование; +- исправление найденных ошибок; +- обновление версии; +- подготовка release notes; +- подготовка production-конфигурации. + +Когда release готов: + +```text +release/2.4.0 → main +release/2.4.0 → develop +``` + +То есть результат релиза должен оказаться одновременно: + +```text +main +``` + +и + +```text +develop +``` + +Пример: + +```bash +git flow release finish 2.4.0 +``` + +После чего изменения необходимо отправить в remote: + +```bash +git push origin main +git push origin develop +git push origin --tags +``` + +Конкретная последовательность зависит от используемого процесса CI/CD. + +--- + +# 9. Почему release вливается и в main, и в develop + +Предположим: + +```text +develop + \ + release/2.4.0 +``` + +Во время подготовки релиза нашли bug и исправили его в release. + +Если отправить изменения только в `main`: + +```text +release → main +``` + +то исправление может не попасть обратно в будущую разработку. + +Поэтому используется: + +```text +release → main +release → develop +``` + +Таким образом, `develop` не теряет исправления, сделанные во время подготовки релиза. + +--- + +# 10. Hotfix Flow + +Если production сломан и исправление требуется срочно: + +```text +main + \ + hotfix/2.4.1 +``` + +Создание: + +```bash +git flow hotfix start 2.4.1 +``` + +После исправления hotfix должен попасть: + +```text +hotfix/2.4.1 → main +hotfix/2.4.1 → develop +``` + +Пример: + +```bash +git flow hotfix finish 2.4.1 +``` + +После этого изменения публикуются: + +```bash +git push origin main +git push origin develop +git push origin --tags +``` + +--- + +# 11. Bugfix Flow + +Если ошибка обнаружена во время разработки и не требует отдельного production hotfix: + +```text +develop + \ + bugfix/registration +``` + +Создание: + +```bash +git flow bugfix start registration +``` + +После завершения: + +```text +bugfix/registration → develop +``` + +Использование `bugfix` или обычного `feature` для исправления ошибки определяется соглашениями проекта. + +Главное — не создавать production hotfix от `develop`. + +--- + +# 12. Support Flow + +`support/*` нужен только тогда, когда проект действительно поддерживает старые версии. + +Например: + +```text +main + ├── 3.0 + └── support/2.x +``` + +Такие ветки позволяют поддерживать старую версию отдельно от основной разработки. + +Использование `support/*` должно быть явно согласовано командой. + +Если проект не требует поддержки нескольких версий одновременно, `support` лучше не использовать без необходимости. + +--- + +# 13. Публикация веток + +Для feature: + +```bash +git flow feature publish +``` + +Для остальных типов веток используются соответствующие `publish`/`push` команды согласно конфигурации `git-flow`. + +Перед публикацией необходимо убедиться, что ветка имеет правильное имя и базовую ветку. + +Проверить ветки можно: + +```bash +git branch +git branch -r +``` + +Полезно также посмотреть историю: + +```bash +git log --all --graph --decorate --oneline +``` + +Это один из самых удобных способов визуально проверить, куда действительно идут изменения. + +--- + +# 14. От какого типа ветки что создаём + +| Ветка | Создаётся от | Основное назначение | Куда вливается | +|---|---|---|---| +| `feature/*` | `develop` | Новая функциональность | `develop` | +| `bugfix/*` | `develop` | Исправление ошибки в разработке | `develop` | +| `release/*` | `develop` | Подготовка релиза | `main` и `develop` | +| `hotfix/*` | `main` | Срочное production-исправление | `main` и `develop` | +| `support/*` | зависит от процесса | Поддержка старой версии | зависит от процесса | + +--- + +# 15. Что не следует делать + +## Не создавать Feature от main + +Плохо: + +```text +main + \ + feature/new-payment +``` + +Правильно: + +```text +develop + \ + feature/new-payment +``` + +--- + +## Не вливать Feature напрямую в main + +Обычно: + +```text +feature → develop → release → main +``` + +а не: + +```text +feature → main +``` + +--- + +## Не создавать Hotfix от develop + +Плохо: + +```text +develop → hotfix +``` + +Правильно: + +```text +main → hotfix +``` + +--- + +## Не держать Feature зависимой от другой Feature без необходимости + +Плохо: + +```text +feature/A + \ + feature/B + \ + feature/C +``` + +Лучше: + +```text +develop + ├── feature/A + ├── feature/B + └── feature/C +``` + +--- + +## Не выполнять force push без необходимости + +Опасно: + +```bash +git push --force +``` + +Предпочтительно: + +```bash +git push --force-with-lease +``` + +при условии, что force push разрешён правилами репозитория. + +--- + +# 16. Рекомендуемый командный процесс + +Для обычной задачи: + +```text +1. develop + ↓ +2. feature/* + ↓ +3. commits + ↓ +4. push + ↓ +5. Pull Request + ↓ +6. review + CI + ↓ +7. develop + ↓ +8. release/* + ↓ +9. main +``` + +В виде схемы: + +```text + Pull Request +feature/login ─────────────────────────→ develop + │ + │ release + ↓ + release/2.4.0 + │ + ┌──────────┴──────────┐ + ↓ ↓ + main develop +``` + +--- + +# 17. Правила Pull Request + +Для командной работы рекомендуется: + +1. Один PR — одна логическая задача. +2. Целевая ветка PR должна быть заранее определена. +3. PR должен проходить CI до merge. +4. Автор PR не должен самостоятельно обходить обязательные проверки. +5. Конфликты необходимо разрешать в своей рабочей ветке, а не в `develop`/`main`. +6. Не следует смешивать в одном PR feature, рефакторинг и несвязанные исправления. +7. После изменения истории feature-ветки необходимо убедиться, что PR всё ещё содержит только нужные изменения. + +Рекомендуемая схема: + +```text +feature/* → Pull Request → develop +release/* → Pull Request → main +hotfix/* → Pull Request → main +``` + +Если политика проекта допускает отдельный merge release/hotfix в `develop`, это также должно быть явно зафиксировано в правилах репозитория. + +--- + +# 18. Конвенция именования веток + +Название ветки должно быть коротким и отражать задачу. + +Примеры: + +```text +feature/user-profile +feature/payment-api +feature/checkout-flow + +bugfix/registration-error +bugfix/incorrect-total + +release/2.4.0 + +hotfix/2.4.1 + +support/2.x +``` + +Не рекомендуется: + +```text +feature/test +feature/new +feature/fix +feature/ivan +feature/123 +``` + +Если в проекте используется трекер задач, предпочтительно включать идентификатор задачи: + +```text +feature/PROJ-123-user-profile +bugfix/PROJ-456-invalid-total +``` + +Точная схема именования должна быть одинаковой для всей команды. + +--- + +# 19. Коммиты + +Коммиты должны быть: + +- небольшими; +- законченными; +- логически связанными; +- понятными без чтения всего diff. + +Плохо: + +```bash +git commit -m "fix" +git commit -m "changes" +git commit -m "test" +``` + +Лучше: + +```bash +git commit -m "feat: add user profile endpoint" +git commit -m "fix: validate empty email" +git commit -m "test: cover invalid payment status" +``` + +Если проект использует Conventional Commits, придерживаемся его правил: + +```text +feat: +fix: +refactor: +test: +docs: +chore: +build: +ci: +``` + +--- + +# 20. Синхронизация перед началом работы + +Перед созданием новой feature: + +```bash +git fetch origin +git checkout develop +git pull --ff-only origin develop +``` + +После этого: + +```bash +git flow feature start my-feature +``` + +Для `hotfix`: + +```bash +git fetch origin +git checkout main +git pull --ff-only origin main +git flow hotfix start 2.4.1 +``` + +Использование `--ff-only` помогает не создавать случайные локальные merge-коммиты во время обычного обновления базовой ветки. + +--- + +# 21. Что делать при конфликтах + +Если конфликт возник при `rebase`: + +```bash +git status +``` + +Исправить конфликтующие файлы, затем: + +```bash +git add +git rebase --continue +``` + +Чтобы полностью отменить rebase: + +```bash +git rebase --abort +``` + +Если конфликт возник при merge: + +```bash +git status +``` + +После исправления: + +```bash +git add +git commit +``` + +Если merge необходимо отменить: + +```bash +git merge --abort +``` + +Не следует разрешать конфликты вслепую. После разрешения обязательно проверить diff и тесты. + +--- + +# 22. Удаление завершённых веток + +После merge завершённая feature обычно больше не нужна. + +Локальную ветку можно удалить: + +```bash +git branch -d feature/my-feature +``` + +Удалённую ветку: + +```bash +git push origin --delete feature/my-feature +``` + +Если репозиторий автоматически удаляет ветку после merge, вручную удалять её не нужно. + +Не удаляйте ветку до тех пор, пока не убедились, что нужные изменения действительно попали в целевую ветку. + +--- + +# 23. Как проверить поток коммитов + +Для просмотра всех локальных и remote веток: + +```bash +git log --all --graph --decorate --oneline --date-order +``` + +Полезно также: + +```bash +git branch -a +``` + +и: + +```bash +git show-branch --all +``` + +Пример графа: + +```text +* abc1234 (main) Merge release/2.4.0 +|\ +| * def5678 (release/2.4.0) Prepare release 2.4.0 +| * 123abcd Fix release configuration +|/ +* 456efgh (develop) Merge feature/payment +|\ +| * 789ijkl (feature/payment) Add payment service +| * 012mnop Add payment validation +|/ +* 345qrst Previous release +``` + +Такой граф позволяет быстро проверить: + +- от какой ветки была создана ветка; +- какие изменения в неё попали; +- где находится merge; +- не ушла ли feature в неправильную ветку. + +--- + +# 24. Частые ошибки + +## Feature создана от старого develop + +Симптом: + +```text +feature + \ + старый develop +``` + +Решение — сначала синхронизировать feature с актуальным `develop`. + +--- + +## Feature случайно создана от main + +Не продолжайте разработку, не проверив ситуацию. + +Сначала нужно убедиться, какие изменения уже появились в feature, и только после этого выбрать способ переноса на актуальный `develop`. + +В зависимости от ситуации это может быть: + +```bash +git rebase develop +``` + +или пересоздание ветки от правильной базы. + +--- + +## В hotfix попали незавершённые изменения из develop + +Это обычно означает, что hotfix создавался не от актуального `main`. + +Hotfix должен базироваться на том состоянии, которое реально находится в production. + +--- + +## Исправление сделано только в release + +Если release содержит важный bugfix, он должен попасть и в `main`, и обратно в `develop`. + +Нельзя оставлять важные исправления только внутри временной release-ветки. + +--- + +## Два разработчика одновременно переписали историю одной feature + +Не следует выполнять `rebase`/force push общей ветки без согласования. + +Для опубликованных веток безопаснее: + +```bash +git push --force-with-lease +``` + +Но даже `--force-with-lease` не отменяет необходимость договориться с остальными участниками. + +Подробнее: `GIT_FORCE_WITH_LEASE_RECOVERY.ru.md`. + +--- + +# 25. Полная схема Git Flow + +```text + feature/A + / + / + ● + / + / +develop ────────────────●────────●───────────────●──────── + \ / + \ / + \ / + release/2.4.0 + │ + ┌────────┴────────┐ + ↓ ↓ + main develop + │ + │ + └── hotfix/2.4.1 + │ + ┌────────┴────────┐ + ↓ ↓ + main develop +``` + +Для обычной разработки поток выглядит так: + +```text +develop + │ + ├── feature/A ──────────────┐ + │ ↓ + ├── feature/B ─────────→ develop + │ + └── feature/C ──────────────┘ + │ + ↓ + release/2.4.0 + │ + ┌─────┴─────┐ + ↓ ↓ + main develop +``` + +--- + +# 26. Краткая памятка + +Запоминаем: + +```text +feature ← develop +feature → develop + +bugfix ← develop +bugfix → develop + +release ← develop +release → main +release → develop + +hotfix ← main +hotfix → main +hotfix → develop +``` + +Основные команды: + +```bash +# Feature +git flow feature start +git flow feature publish +git flow feature finish + +# Bugfix +git flow bugfix start +git flow bugfix finish + +# Release +git flow release start +git flow release finish + +# Hotfix +git flow hotfix start +git flow hotfix finish + +# Support +git flow support start +``` + +Для просмотра графа: + +```bash +git log --all --graph --decorate --oneline --date-order +``` + +--- + +# 27. Наше командное правило + +Перед каждой операцией с ветками нужно ответить на три вопроса: + +**1. От какой ветки я создаю текущую ветку?** + +**2. В какую ветку должны попасть мои изменения?** + +**3. Не содержит ли моя ветка изменений, которые не должны попасть в целевую ветку?** + +Если на эти три вопроса есть однозначный ответ, операция с Git почти всегда становится предсказуемой. + +--- + +# 28. Итог + +Нормальный Git Flow в команде выглядит так: + +```text + feature + ↗ +develop ───────────────●─────────────── + │ + │ release + ↓ + main + │ + │ hotfix + ↓ + develop +``` + +Ключевая идея проста: + +> `develop` — источник обычной разработки, `main` — источник production-правок. Feature идут в `develop`, release — в `main` и обратно в `develop`, hotfix — из `main` в `main` и обратно в `develop`. + +Git Flow — это не набор обязательных магических команд. Это договорённость команды о том, **где рождаются изменения, через какие этапы проходят и в какую ветку попадают**. + +Перед merge всегда проверяем целевую ветку, актуальность базы, CI и содержимое diff. diff --git a/tutorials/git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md b/tutorials/git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md new file mode 100644 index 0000000..8631d4c --- /dev/null +++ b/tutorials/git/GIT_FORCE_WITH_LEASE_RECOVERY.ru.md @@ -0,0 +1,175 @@ +# Памятка: как синхронизировать локальную ветку после `git push --force-with-lease` + +Эта инструкция подходит для ситуации, когда удалённая ветка была переписана через `git push --force-with-lease`, и теперь локальная ветка расходится с `origin`. + +## Если локальных изменений нет + +Если у тебя **нет никаких локальных изменений, которые нужно сохранить**, это хорошо: можно безопасно заменить указатель локальной ветки на удалённую. + +В таком случае ничего нужного не пропадёт. + +### 1. Сделай `git fetch` + +```bash +git fetch +``` + +### 2. Посмотри, где расходятся ветки + +```bash +git log --oneline --decorate --color --graph --all +``` + +С помощью графа можно увидеть, где локальная и удалённая ветки расходятся в истории. + +Например: + +```text +feature/student +origin/feature/student +``` + +`feature/student` — локальная ветка, `origin/feature/student` — состояние этой ветки в удалённом репозитории. + +### 3. Перемести указатель локальной ветки на удалённую + +Сначала перейди на нужную локальную ветку: + +```bash +git checkout feature/student +``` + +Затем: + +```bash +git reset --hard origin/feature/student +``` + +После этого локальная `feature/student` будет указывать на тот же коммит, что и `origin/feature/student`. + +### Весь сценарий целиком + +```bash +git fetch +git log --oneline --decorate --color --graph --all +git checkout feature/student +git reset --hard origin/feature/student +``` + +--- + +# Если локальные изменения нужно сохранить + +В этом случае **не нужно сразу делать `git reset --hard`**. + +`reset --hard` изменит состояние рабочей директории и может удалить незакоммиченные изменения. + +Сначала нужно сохранить свою работу. + +## Вариант 1. Изменения уже закоммичены + +Если твои изменения находятся в локальных коммитах, самый простой и надёжный вариант — сначала создать дополнительную ветку, которая будет указывать на текущее состояние: + +```bash +git checkout feature/student +git branch backup/feature-student +``` + +Теперь у тебя есть резервная ветка: + +```text +backup/feature-student + ↓ + твои старые коммиты + +feature/student + ↓ + твои старые коммиты +``` + +После этого можно безопаснее двигать `feature/student`: + +```bash +git fetch +git reset --hard origin/feature/student +``` + +Резервная ветка `backup/feature-student` при этом никуда не исчезнет. + +Если позже выяснится, что какие-то коммиты из старой истории нужны, их можно перенести в новую историю с помощью `git cherry-pick`. + +Например: + +```bash +git cherry-pick +``` + +## Вариант 2. Есть незакоммиченные изменения + +Если ты что-то изменил, но ещё не сделал `git commit`, сначала сохрани изменения. + +Один из удобных вариантов — временно убрать их в `stash`: + +```bash +git stash push -u -m "backup before force-push recovery" +``` + +После этого рабочая директория станет чистой, и можно выполнять: + +```bash +git fetch +git checkout feature/student +git reset --hard origin/feature/student +``` + +Затем можно вернуть сохранённые изменения: + +```bash +git stash pop +``` + +Git попытается применить их поверх новой истории. + +Если возникнут конфликты, их нужно будет разрешить вручную. + +--- + +# Самое важное правило + +Перед: + +```bash +git reset --hard origin/feature/student +``` + +нужно ответить на вопрос: + +> **Есть ли у меня локальные изменения, которые я не хочу потерять?** + +### Если нет + +Можно: + +```bash +git reset --hard origin/feature/student +``` + +### Если да, но изменения закоммичены + +Сначала создай резервную ветку: + +```bash +git branch backup/feature-student +``` + +### Если да, но изменения ещё не закоммичены + +Сначала используй `stash`: + +```bash +git stash push -u -m "backup before force-push recovery" +``` + +И только после этого выполняй `reset --hard`. + +**Принцип простой: сначала сохранить то, что может понадобиться, потом двигать указатель ветки.**