Merge branch 'develop' into feature/datasource
This commit is contained in:
+21
@@ -41,11 +41,19 @@
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-csv</artifactId>
|
||||
<version>1.14.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.googlecode.lanterna</groupId>
|
||||
<artifactId>lanterna</artifactId>
|
||||
<version>3.1.5</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -96,5 +104,18 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
|
||||
<configuration>
|
||||
<mainClass>ru.project.App</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package ru.project;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*/
|
||||
public class App {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello World!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package ru.project.collection;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MyLinkedList<T> implements MyList<T> {
|
||||
private Node<T> head;
|
||||
private int size;
|
||||
|
||||
public void add(T value) {
|
||||
if (head == null) {
|
||||
this.head = new Node<>(value);
|
||||
} else {
|
||||
Node<T> temp = head;
|
||||
while (temp.getNext() != null) {
|
||||
temp = temp.getNext();
|
||||
}
|
||||
temp.setNext(new Node<>(value));
|
||||
}
|
||||
size++;
|
||||
}
|
||||
|
||||
public T get(int index) {
|
||||
int currentIndex = 0;
|
||||
Node<T> 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<T> 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<T> temp = head;
|
||||
for (int i = 0; i < index; i++) {
|
||||
temp = temp.getNext();
|
||||
}
|
||||
|
||||
T oldValue = temp.getValue();
|
||||
temp.setValue(value);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return size == 0;
|
||||
}
|
||||
public void addAll(MyList<? extends T> collection) {
|
||||
if (collection == null || collection.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < collection.size(); i++) {
|
||||
add(collection.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
Object[] res = new Object[size];
|
||||
int index = 0;
|
||||
Node<T> temp = head;
|
||||
while (temp != null) {
|
||||
res[index] = temp.getValue();
|
||||
index++;
|
||||
temp = temp.getNext();
|
||||
}
|
||||
return Arrays.toString(res);
|
||||
}
|
||||
|
||||
private static class Node<T> {
|
||||
private T value;
|
||||
private Node<T> next;
|
||||
|
||||
public Node(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void setValue(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void setNext(Node<T> next) {
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Node<T> getNext() {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package ru.project.collection;
|
||||
|
||||
public interface MyList<T> {
|
||||
|
||||
void add(T element);
|
||||
|
||||
T get(int index);
|
||||
|
||||
T set(int index, T element);
|
||||
|
||||
void remove(int index);
|
||||
|
||||
int size();
|
||||
|
||||
boolean isEmpty();
|
||||
void addAll(MyList<? extends T> collection);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.project.collectors;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
public class MyCollectors
|
||||
{
|
||||
private MyCollectors() {
|
||||
|
||||
}
|
||||
|
||||
public static <T> Collector<T, MyList<T>, MyList<T>> toMyList()
|
||||
{
|
||||
return new MyListCollector<>();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 <T> implements Collector<T, MyList<T>, MyList<T>>
|
||||
{
|
||||
@Override
|
||||
public Supplier<MyList<T>> supplier()
|
||||
{
|
||||
return () -> new MyLinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiConsumer<MyList<T>, T> accumulator() {
|
||||
|
||||
return (list, element) -> list.add(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryOperator<MyList<T>> combiner() {
|
||||
return (list1, list2) -> {
|
||||
list1.addAll(list2);
|
||||
return list1;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<MyList<T>, MyList<T>> finisher()
|
||||
{
|
||||
return list -> list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Collector.Characteristics> characteristics() {
|
||||
return java.util.Set.of(
|
||||
Collector.Characteristics.IDENTITY_FINISH
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Student> 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<Future<MyList<Integer>>> 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<MyList<Integer>> future = executor.submit(() -> {
|
||||
return findInRange(students, target, start, end);
|
||||
});
|
||||
futures.add(future);
|
||||
}
|
||||
MyList<Integer> allIndices = new MyLinkedList<>();
|
||||
try {
|
||||
for (int i = 0; i < futures.size(); i++) {
|
||||
Future<MyList<Integer>> future = futures.get(i);
|
||||
MyList<Integer> 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<Integer> list) {
|
||||
if (list.size() <= 1) return;
|
||||
|
||||
MyList<Integer> 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<Integer> 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<Integer> findInRange(MyList<Student> students, Student target, int fromIndex, int toIndex) {
|
||||
MyList<Integer> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Student> mainCollection;
|
||||
private MyList<Student> customDataCollection;
|
||||
private String currentFilePath;
|
||||
|
||||
public AppState() {
|
||||
this.mainCollection = new MyLinkedList<>();
|
||||
this.customDataCollection = new MyLinkedList<>();
|
||||
this.currentFilePath = null;
|
||||
}
|
||||
|
||||
public MyList<Student> getMainCollection() {
|
||||
return mainCollection;
|
||||
}
|
||||
|
||||
public void setMainCollection(MyList<Student> currentCollection) {
|
||||
this.mainCollection = currentCollection;
|
||||
}
|
||||
|
||||
public MyList<Student> getCustomDataCollection() {
|
||||
return customDataCollection;
|
||||
}
|
||||
|
||||
public void setCustomDataCollection(MyList<Student> customDataCollection) {
|
||||
|
||||
this.customDataCollection = customDataCollection;
|
||||
}
|
||||
|
||||
public String getCurrentFilePath() {
|
||||
return currentFilePath;
|
||||
}
|
||||
|
||||
public void setCurrentFilePath(String currentFilePath) {
|
||||
this.currentFilePath = currentFilePath;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<StudentSearchCriteria> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.project.search;
|
||||
|
||||
public enum SearchOperation {
|
||||
UNION,
|
||||
INTERSECTION
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.project.search;
|
||||
|
||||
import ru.project.student.Student;
|
||||
|
||||
public interface StudentSearchCriteria {
|
||||
boolean matches(Student student);
|
||||
}
|
||||
@@ -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<Student> find(MyList<Student> students) {
|
||||
if (students == null) {
|
||||
return new MyLinkedList<>();
|
||||
}
|
||||
MyList<Student> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.project.sorting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import ru.project.collection.MyList;
|
||||
|
||||
public class BubbleSortStrategy<T> implements SortStrategy<T> {
|
||||
|
||||
@Override
|
||||
public void sort(MyList<T> list, Comparator<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T> implements SortStrategy<T> {
|
||||
|
||||
private final ToIntFunction<T> valueExtractor;
|
||||
|
||||
public EvenOnlyMergeSortStrategy(ToIntFunction<T> valueExtractor) {
|
||||
this.valueExtractor = valueExtractor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sort(MyList<T> list, Comparator<T> comparator) {
|
||||
if (list == null || list.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> evenIndices = new ArrayList<>();
|
||||
List<T> 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<T> sortedEven = mergeSort(evenElements, comparator);
|
||||
|
||||
for (int j = 0; j < evenIndices.size(); j++) {
|
||||
list.set(evenIndices.get(j), sortedEven.get(j));
|
||||
}
|
||||
}
|
||||
|
||||
private List<T> mergeSort(List<T> list, Comparator<T> comparator) {
|
||||
if (list.size() <= 1) {
|
||||
return list;
|
||||
}
|
||||
|
||||
int mid = list.size() / 2;
|
||||
List<T> left = mergeSort(list.subList(0, mid), comparator);
|
||||
List<T> right = mergeSort(list.subList(mid, list.size()), comparator);
|
||||
|
||||
return merge(left, right, comparator);
|
||||
}
|
||||
|
||||
private List<T> merge(List<T> left, List<T> right, Comparator<T> comparator) {
|
||||
List<T> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<T> implements SortStrategy<T> {
|
||||
|
||||
private SortStrategy<T> sortStrategy;
|
||||
private ToIntFunction<T> valueExtractor;
|
||||
|
||||
public EvenOnlySortStrategy() {}
|
||||
|
||||
public EvenOnlySortStrategy(SortStrategy<T> sortStrategy) {
|
||||
setSortStrategy(sortStrategy);
|
||||
}
|
||||
|
||||
public EvenOnlySortStrategy(SortStrategy<T> sortStrategy, ToIntFunction<T> valueExtractor) {
|
||||
setSortStrategy(sortStrategy);
|
||||
setValueExtractor(valueExtractor);
|
||||
}
|
||||
|
||||
public void setSortStrategy(SortStrategy<T> sortStrategy) {
|
||||
if (sortStrategy == null) {
|
||||
throw new IllegalArgumentException("Sort strategy must not be null");
|
||||
}
|
||||
|
||||
this.sortStrategy = sortStrategy;
|
||||
}
|
||||
|
||||
public void setValueExtractor(ToIntFunction<T> valueExtractor) {
|
||||
if (valueExtractor == null) {
|
||||
throw new IllegalArgumentException("Value extractor must not be null");
|
||||
}
|
||||
|
||||
this.valueExtractor = valueExtractor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sort(MyList<T> list, Comparator<T> 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<T> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package ru.project.sorting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import ru.project.collection.MyList;
|
||||
|
||||
public class MergeSortStrategy<T> implements SortStrategy<T> {
|
||||
|
||||
@Override
|
||||
public void sort(MyList<T> list, Comparator<T> comparator) {
|
||||
if (list == null || list.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
mergeSort(list, 0, list.size() - 1, comparator);
|
||||
}
|
||||
|
||||
private void mergeSort(MyList<T> list, int left, int right, Comparator<T> 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<T> list, int left, int mid, int right, Comparator<T> 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.project.sorting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import ru.project.collection.MyList;
|
||||
|
||||
public interface SortStrategy<T> {
|
||||
void sort(MyList<T> list, Comparator<T> comparator);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.project.sorting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import ru.project.student.Student;
|
||||
|
||||
public class StudentAverageGradeComparator implements Comparator<Student> {
|
||||
|
||||
@Override
|
||||
public int compare(Student s1, Student s2) {
|
||||
return Double.compare(s1.getAverageGrade(), s2.getAverageGrade());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.project.sorting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import ru.project.student.Student;
|
||||
|
||||
public class StudentGroupComparator implements Comparator<Student> {
|
||||
|
||||
@Override
|
||||
public int compare(Student s1, Student s2) {
|
||||
return s1.getGroupNumber().compareTo(s2.getGroupNumber());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.project.sorting;
|
||||
|
||||
import java.util.Comparator;
|
||||
import ru.project.student.Student;
|
||||
|
||||
public class StudentRecordBookComparator implements Comparator<Student> {
|
||||
|
||||
@Override
|
||||
public int compare(Student s1, Student s2) {
|
||||
return Integer.compare(s1.getRecordBookNumber(), s2.getRecordBookNumber());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Student> collection;
|
||||
|
||||
private TextBox collectionTextBox;
|
||||
private Button exitButton;
|
||||
|
||||
public CollectionDisplayWindow(WindowBasedTextGUI gui, MyList<Student> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> onSave;
|
||||
|
||||
public InputValueWindow(
|
||||
WindowBasedTextGUI gui,
|
||||
String title,
|
||||
String prompt,
|
||||
String initialValue,
|
||||
Predicate<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<Student> customDataCollection = state.getCustomDataCollection();
|
||||
|
||||
MyList<Student> 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 -> "Данные из файла";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> 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<Student> preparedCustomDataCollection;
|
||||
|
||||
public CustomDataSettingsWindow(
|
||||
WindowBasedTextGUI gui, AppState state, Supplier<Integer> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.project.ui.creation;
|
||||
|
||||
public enum FillMethod {
|
||||
CUSTOM,
|
||||
RANDOM,
|
||||
FILE
|
||||
}
|
||||
@@ -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<FillMethod> onFillMethodSelected;
|
||||
|
||||
public FillMethodSelectionWindow(
|
||||
WindowBasedTextGUI gui, Consumer<FillMethod> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<Student> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<SortField> onSortFieldSelected;
|
||||
|
||||
public NormalSortSettingsWindow(WindowBasedTextGUI gui, Consumer<SortField> 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 -> "номеру зачётной книжки";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.project.ui.sorting;
|
||||
|
||||
public enum SortField {
|
||||
GROUP_NUMBER,
|
||||
AVERAGE_GRADE,
|
||||
RECORD_BOOK_NUMBER
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.project.ui.sorting;
|
||||
|
||||
public enum SortMethod {
|
||||
NORMAL,
|
||||
PARITY
|
||||
}
|
||||
@@ -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<SortMethod> onSortMethodSelected;
|
||||
|
||||
public SortMethodSelectionWindow(
|
||||
WindowBasedTextGUI gui, Consumer<SortMethod> 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> 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<Integer> list = new MyLinkedList<>();
|
||||
list.add(10);
|
||||
assertEquals(10, list.get(0));
|
||||
}
|
||||
@Test
|
||||
public void shouldTestRemove() {
|
||||
MyLinkedList<Integer> 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<String> 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<Integer> list = new MyLinkedList<>();
|
||||
list.add(1);
|
||||
list.add(2);
|
||||
list.add(3);
|
||||
assertEquals(3, list.size());
|
||||
}
|
||||
@Test
|
||||
void shouldTestisEmpty()
|
||||
{
|
||||
MyLinkedList<Integer> list = new MyLinkedList<>();
|
||||
assertTrue(list.isEmpty());
|
||||
}
|
||||
@Test
|
||||
void shouldTestGet_OutOfBounds_ThrowsException() {
|
||||
MyLinkedList<Integer> 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<Integer> 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<String> list = new MyLinkedList<>();
|
||||
list.add("man");
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> list.remove(5));
|
||||
}
|
||||
@Test
|
||||
void shouldTestRemove_NegativeIndex_ThrowsException() {
|
||||
MyLinkedList<String> list = new MyLinkedList<>();
|
||||
list.add("1");
|
||||
list.add("2");
|
||||
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> list.remove(-1));
|
||||
}
|
||||
@Test
|
||||
void shouldTestaddAll_nonEmptySource_toEmptyList_addsAllElementsInOrder() {
|
||||
MyList<String> list = new MyLinkedList<>();
|
||||
MyList<String> 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<String> list = new MyLinkedList<>();
|
||||
list.add("X");
|
||||
list.add("Y");
|
||||
|
||||
MyList<String> 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));
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> collector = new MyListCollector<>();
|
||||
|
||||
@Test
|
||||
void shouldTestCollectInOrderSequential() {
|
||||
MyList<Integer> 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<Integer> 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<Integer> result = IntStream.empty()
|
||||
.boxed()
|
||||
.collect(MyCollectors.toMyList());
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
|
||||
}
|
||||
@Test
|
||||
void shouldTestNotUseStandardList() {
|
||||
MyList<String> 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<Collector.Characteristics> characteristics = collector.characteristics();
|
||||
assertTrue(characteristics.contains(java.util.stream.Collector.Characteristics.IDENTITY_FINISH));
|
||||
assertFalse(characteristics.contains(java.util.stream.Collector.Characteristics.UNORDERED));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Student> 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], "Результат должен быть отсортирован");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Student> 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<Student> result = service.find(students);
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("A10", result.get(0).getGroupNumber());
|
||||
assertEquals("C30", result.get(1).getGroupNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchServiceEmptyResult() {
|
||||
MyList<Student> 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<Student> result = service.find(students);
|
||||
|
||||
assertEquals(0, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchServiceEmptyCollection() {
|
||||
MyList<Student> students = new MyLinkedList<>();
|
||||
StudentSearchCriteria criteria = new AverageGradeRangeCriteria(4.0, 5.0);
|
||||
StudentSearchService service = new StudentSearchService(criteria);
|
||||
MyList<Student> result = service.find(students);
|
||||
assertEquals(0, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchServicePreservesOrder() {
|
||||
MyList<Student> 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<Student> 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<Student> original = new MyLinkedList<>();
|
||||
original.add(new Student("A10", 4.5, 1));
|
||||
original.add(new Student("B20", 3.8, 2));
|
||||
|
||||
MyList<Student> 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<Student> 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<Student> 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<Student> result = service.find(null);
|
||||
assertNotNull(result);
|
||||
assertEquals(0, result.size());
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> list = new MyLinkedList<>();
|
||||
|
||||
EvenOnlySortStrategy<Integer> strategy =
|
||||
new EvenOnlySortStrategy<>(new MergeSortStrategy<>(), value -> value);
|
||||
|
||||
strategy.sort(list, Integer::compare);
|
||||
|
||||
assertEquals(0, list.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepSingleEvenElementUnchanged() {
|
||||
MyList<Integer> list = new MyLinkedList<>();
|
||||
list.add(4);
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Integer> list = new MyLinkedList<>();
|
||||
list.add(7);
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Integer> list = new MyLinkedList<>();
|
||||
|
||||
list.add(8);
|
||||
list.add(4);
|
||||
list.add(8);
|
||||
list.add(2);
|
||||
list.add(4);
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Student> students = new MyLinkedList<>();
|
||||
|
||||
students.add(oddFirst);
|
||||
students.add(evenFirst);
|
||||
students.add(oddSecond);
|
||||
students.add(evenSecond);
|
||||
students.add(oddThird);
|
||||
students.add(evenThird);
|
||||
|
||||
EvenOnlySortStrategy<Student> 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<Integer> list = new MyLinkedList<>();
|
||||
|
||||
list.add(8);
|
||||
list.add(2);
|
||||
list.add(4);
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Student> 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<Student> 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<Integer> list = new MyLinkedList<>();
|
||||
|
||||
list.add(8);
|
||||
list.add(2);
|
||||
list.add(6);
|
||||
list.add(4);
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Integer> list = new MyLinkedList<>();
|
||||
|
||||
list.add(7);
|
||||
list.add(3);
|
||||
list.add(5);
|
||||
list.add(1);
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Integer> strategy = new EvenOnlySortStrategy<>();
|
||||
|
||||
strategy.setValueExtractor(value -> value);
|
||||
|
||||
MyList<Integer> list = new MyLinkedList<>();
|
||||
list.add(2);
|
||||
list.add(4);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> strategy.sort(list, Integer::compare));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectNullSortStrategy() {
|
||||
EvenOnlySortStrategy<Integer> strategy = new EvenOnlySortStrategy<>();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> strategy.setSortStrategy(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateSortingToProvidedStrategy() {
|
||||
MyList<Integer> list = new MyLinkedList<>();
|
||||
|
||||
list.add(7);
|
||||
list.add(8);
|
||||
list.add(3);
|
||||
list.add(2);
|
||||
|
||||
RecordingSortStrategy recordingStrategy = new RecordingSortStrategy();
|
||||
|
||||
EvenOnlySortStrategy<Integer> 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<Integer> strategy =
|
||||
new EvenOnlySortStrategy<>(firstStrategy, value -> value);
|
||||
|
||||
MyList<Integer> firstList = new MyLinkedList<>();
|
||||
firstList.add(4);
|
||||
firstList.add(2);
|
||||
|
||||
strategy.sort(firstList, Integer::compare);
|
||||
|
||||
assertTrue(firstStrategy.wasCalled);
|
||||
|
||||
strategy.setSortStrategy(secondStrategy);
|
||||
|
||||
MyList<Integer> secondList = new MyLinkedList<>();
|
||||
secondList.add(8);
|
||||
secondList.add(6);
|
||||
|
||||
strategy.sort(secondList, Integer::compare);
|
||||
|
||||
assertTrue(secondStrategy.wasCalled);
|
||||
}
|
||||
|
||||
private static class RecordingSortStrategy implements SortStrategy<Integer> {
|
||||
|
||||
private boolean wasCalled;
|
||||
private int receivedSize;
|
||||
private Integer firstReceivedElement;
|
||||
private Integer secondReceivedElement;
|
||||
|
||||
@Override
|
||||
public void sort(MyList<Integer> list, Comparator<Integer> comparator) {
|
||||
wasCalled = true;
|
||||
receivedSize = list.size();
|
||||
|
||||
if (list.size() > 0) {
|
||||
firstReceivedElement = list.get(0);
|
||||
}
|
||||
|
||||
if (list.size() > 1) {
|
||||
secondReceivedElement = list.get(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Student> students;
|
||||
private final Comparator<Student> groupComparator = new StudentGroupComparator();
|
||||
private final Comparator<Student> gradeComparator = new StudentAverageGradeComparator();
|
||||
private final Comparator<Student> 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<Student> strategy = new MergeSortStrategy<>();
|
||||
strategy.sort(students, groupComparator);
|
||||
assertOrder(students, "A12", "A12", "B32", "B32", "Z00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMergeSortByAverageGrade() {
|
||||
SortStrategy<Student> strategy = new MergeSortStrategy<>();
|
||||
strategy.sort(students, gradeComparator);
|
||||
assertOrder(students, 3.8, 4.2, 4.5, 4.9, 5.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMergeSortByRecordBookNumber() {
|
||||
SortStrategy<Student> strategy = new MergeSortStrategy<>();
|
||||
strategy.sort(students, recordComparator);
|
||||
assertOrder(students, 12345, 12346, 12347, 12348, 12349);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBubbleSortByGroupNumber() {
|
||||
SortStrategy<Student> strategy = new BubbleSortStrategy<>();
|
||||
strategy.sort(students, groupComparator);
|
||||
assertOrder(students, "A12", "A12", "B32", "B32", "Z00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBubbleSortByAverageGrade() {
|
||||
SortStrategy<Student> strategy = new BubbleSortStrategy<>();
|
||||
strategy.sort(students, gradeComparator);
|
||||
assertOrder(students, 3.8, 4.2, 4.5, 4.9, 5.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBubbleSortByRecordBookNumber() {
|
||||
SortStrategy<Student> strategy = new BubbleSortStrategy<>();
|
||||
strategy.sort(students, recordComparator);
|
||||
assertOrder(students, 12345, 12346, 12347, 12348, 12349);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEvenOnlySortByRecordBookNumber() {
|
||||
MyList<Student> 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<Student> 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<Student> 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<Student> 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<Student> 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<Student> 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<Student> empty = new MyLinkedList<>();
|
||||
SortStrategy<Student> strategy = new MergeSortStrategy<>();
|
||||
strategy.sort(empty, groupComparator);
|
||||
assertEquals(0, empty.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortSingleElement() {
|
||||
MyList<Student> single = new MyLinkedList<>();
|
||||
single.add(new Student("A12", 4.5, 12345));
|
||||
SortStrategy<Student> strategy = new MergeSortStrategy<>();
|
||||
strategy.sort(single, groupComparator);
|
||||
assertEquals(1, single.size());
|
||||
}
|
||||
|
||||
private void assertOrder(MyList<Student> 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<Student> 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<Student> list, int... records) {
|
||||
assertEquals(records.length, list.size());
|
||||
for (int i = 0; i < records.length; i++) {
|
||||
assertEquals(records[i], list.get(i).getRecordBookNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
Главное меню
|
||||
{
|
||||
Показать текущую коллекцию
|
||||
|
||||
Меню создания новой коллекции
|
||||
{
|
||||
Вернуться
|
||||
|
||||
Задать размер
|
||||
|
||||
Настроить способ заполнения
|
||||
{
|
||||
Пользовательские данные
|
||||
{
|
||||
Задать поле 'Название A'
|
||||
Задать поле 'Название B'
|
||||
Задать поле 'Название C'
|
||||
Задать индекс изменяемого объекта
|
||||
Изменить данные объекта
|
||||
Предпросмотр
|
||||
}
|
||||
|
||||
Случайные данные
|
||||
{
|
||||
Настройка диапазона генерации
|
||||
{
|
||||
Поле 'Название A'
|
||||
{
|
||||
Задать минимальное значение
|
||||
Задать максимальное значение
|
||||
}
|
||||
|
||||
Поле 'Название B'
|
||||
{
|
||||
Задать минимальное значение
|
||||
Задать максимальное значение
|
||||
}
|
||||
|
||||
Поле 'Название C'
|
||||
{
|
||||
Задать минимальное значение
|
||||
Задать максимальное значение
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Данные из файла
|
||||
{
|
||||
Задать путь до файла
|
||||
{
|
||||
Абсолютный
|
||||
Относительный
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Выбрать способ заполнения
|
||||
{
|
||||
Пользовательские данные *
|
||||
Случайные данные
|
||||
Данные из файла
|
||||
}
|
||||
|
||||
Создать новую коллекцию
|
||||
}
|
||||
|
||||
Меню сохранения текущей коллекции в файл
|
||||
{
|
||||
Вывести текущий абсолютный путь до файла
|
||||
|
||||
Задать путь до файла
|
||||
{
|
||||
Абсолютный
|
||||
Относительный
|
||||
}
|
||||
|
||||
Дописать коллекцию в текущий файл
|
||||
}
|
||||
|
||||
Меню сортировки текущей коллекции
|
||||
{
|
||||
Вернуться
|
||||
|
||||
Настроить способ сортировки
|
||||
{
|
||||
Обычная сортировка
|
||||
{
|
||||
Сортировать по полю 'Название A'
|
||||
Сортировать по полю 'Название B'
|
||||
Сортировать по полю 'Название C'
|
||||
}
|
||||
}
|
||||
|
||||
Настроить способ сортировки
|
||||
{
|
||||
Сортировка по чётности
|
||||
{
|
||||
Выбрать числовое поле 'Название A'
|
||||
Выбрать числовое поле 'Название B'
|
||||
}
|
||||
}
|
||||
|
||||
Выбрать способ сортировки
|
||||
{
|
||||
Обычная сортировка *
|
||||
Сортировка по чётности
|
||||
}
|
||||
|
||||
Сортировать текущую коллекцию
|
||||
}
|
||||
|
||||
Меню поиска элемента в текущей коллекции
|
||||
{
|
||||
Вернуться
|
||||
Задать значение поля 'Название A'
|
||||
Задать значение поля 'Название B'
|
||||
Задать значение поля 'Название C'
|
||||
Найти первое вхождение в текущей коллекции
|
||||
}
|
||||
|
||||
Меню подсчёта количества вхождений элемента
|
||||
{
|
||||
Задать значение поля 'Название A'
|
||||
Задать значение поля 'Название B'
|
||||
Задать значение поля 'Название C'
|
||||
Выполнить многопоточный подсчёт
|
||||
}
|
||||
|
||||
Завершить выполнение программы
|
||||
}
|
||||
@@ -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<Student> find(MyList<Student> students);
|
||||
```
|
||||
|
||||
### Поведение
|
||||
|
||||
Метод `find()` должен:
|
||||
|
||||
1. Последовательно проверить студентов исходной коллекции.
|
||||
2. Передать каждого студента в:
|
||||
|
||||
```java
|
||||
criteria.matches(student)
|
||||
```
|
||||
|
||||
1. Добавить подходящих студентов в новую `MyList<Student>`.
|
||||
2. Вернуть новую коллекцию.
|
||||
|
||||
`StudentSearchService` не должен содержать логику конкретных диапазонов или объединения критериев.
|
||||
|
||||
### Обработка отсутствующего критерия
|
||||
|
||||
Если `StudentSearchService` создан без критерия:
|
||||
|
||||
```java
|
||||
new StudentSearchService(null);
|
||||
```
|
||||
|
||||
необходимо выбросить:
|
||||
|
||||
```java
|
||||
IllegalArgumentException
|
||||
```
|
||||
|
||||
Класс не должен выполнять поиск без заданного критерия.
|
||||
|
||||
---
|
||||
|
||||
# Пример использования
|
||||
|
||||
### Один критерий
|
||||
|
||||
```java
|
||||
StudentSearchCriteria criteria =
|
||||
new AverageGradeRangeCriteria(4.0, 5.0);
|
||||
|
||||
StudentSearchService service =
|
||||
new StudentSearchService(criteria);
|
||||
|
||||
MyList<Student> 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<Student>` не изменять.
|
||||
2. Результат возвращать в виде новой `MyList<Student>`.
|
||||
3. Порядок найденных студентов должен сохраняться.
|
||||
4. Стандартные `List`, `ArrayList`, `LinkedList` не использовать.
|
||||
5. Готовые методы поиска не использовать.
|
||||
6. При отсутствии совпадений возвращать пустую `MyList<Student>`.
|
||||
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
|
||||
```
|
||||
@@ -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<Student>[] students) throws IOException;
|
||||
```
|
||||
|
||||
### Контракт
|
||||
|
||||
Метод должен:
|
||||
|
||||
1. Принимать массив `MyList<Student>`.
|
||||
2. Последовательно обрабатывать все переданные коллекции.
|
||||
3. Записывать каждого студента в указанный CSV-файл.
|
||||
4. Сохранять порядок:
|
||||
- коллекций в массиве;
|
||||
- студентов внутри каждой `MyList`.
|
||||
5. Использовать тот же формат CSV и те же правила записи, что и `write(Student)`.
|
||||
6. Работать только с уже существующим файлом.
|
||||
7. Не создавать файл, если он отсутствует.
|
||||
8. При отсутствии файла выбрасывать `IOException`.
|
||||
9. Не изменять переданные `MyList<Student>`.
|
||||
10. Не обрабатывать исключения, возникающие при создании или валидации `Student`.
|
||||
11. При ошибке записи прекращать операцию и передавать исключение вызывающему коду.
|
||||
|
||||
### Пример
|
||||
|
||||
```java
|
||||
MyList<Student>[] 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<Student>` внутри массива не должны приводить к ошибке и не должны добавлять записей в файл.
|
||||
|
||||
### Проверка пустого массива
|
||||
|
||||
Для:
|
||||
|
||||
```java
|
||||
MyList<Student>[] studentLists = ...; // размер 0
|
||||
```
|
||||
|
||||
метод не должен добавлять никаких записей и не должен изменять файл.
|
||||
|
||||
### Проверка отсутствующего файла
|
||||
|
||||
Для несуществующего файла:
|
||||
|
||||
```java
|
||||
writer.write(studentLists);
|
||||
```
|
||||
|
||||
должен выбрасываться `IOException`. Файл создавать запрещено.
|
||||
|
||||
---
|
||||
|
||||
# `StudentCsvReader`
|
||||
|
||||
## Назначение
|
||||
|
||||
Последовательно читать студентов из CSV-файла.
|
||||
|
||||
## Контракт
|
||||
|
||||
Класс должен реализовывать:
|
||||
|
||||
```java
|
||||
Iterator<Student>
|
||||
```
|
||||
|
||||
Конструктор:
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# Техническое задание: кастомный контейнер List
|
||||
|
||||
## Задача
|
||||
|
||||
Реализовать собственный контейнер для хранения объектов, не используя готовые реализации `List`, `ArrayList` и `LinkedList`.
|
||||
|
||||
## Требования
|
||||
|
||||
1. Создать интерфейс `MyList<T>`.
|
||||
|
||||
2. В интерфейсе определить основные операции:
|
||||
|
||||
- `add(...)`;
|
||||
- `get(...)`;
|
||||
- `set(...)`;
|
||||
- `remove(...)`;
|
||||
- `size()`;
|
||||
- `isEmpty()`.
|
||||
|
||||
3. Создать класс `MyLinkedList<T>`, реализующий `MyList<T>`.
|
||||
|
||||
4. Реализовать контейнер на основе собственных узлов `Node<T>`.
|
||||
|
||||
5. Контейнер должен поддерживать:
|
||||
|
||||
- добавление элемента;
|
||||
- получение элемента по индексу;
|
||||
- замену элемента по индексу;
|
||||
- удаление элемента по индексу;
|
||||
- получение количества элементов;
|
||||
- проверку на пустоту.
|
||||
|
||||
6. Обработать некорректные индексы с помощью исключений.
|
||||
|
||||
7. Контейнер не должен зависеть от класса `Student`, сортировки или пользовательского интерфейса.
|
||||
|
||||
## Результат
|
||||
|
||||
Должны быть реализованы:
|
||||
|
||||
```text
|
||||
MyList.java
|
||||
MyLinkedList.java
|
||||
```
|
||||
@@ -0,0 +1,368 @@
|
||||
# Техническое задание: `EvenOnlySortStrategy`
|
||||
|
||||
## Задача
|
||||
|
||||
Создать класс `EvenOnlySortStrategy`, реализующий дополнительное задание по сортировке элементов с чётным значением выбранного числового поля.
|
||||
|
||||
Класс должен использовать **паттерн Стратегия**: он не реализует собственный алгоритм сортировки, а делегирует сортировку переданной реализации `SortStrategy`.
|
||||
|
||||
## Контракт
|
||||
|
||||
Класс:
|
||||
|
||||
```java
|
||||
public class EvenOnlySortStrategy<T> implements SortStrategy<T>
|
||||
```
|
||||
|
||||
Должен содержать два конструктора:
|
||||
|
||||
```java
|
||||
public EvenOnlySortStrategy()
|
||||
```
|
||||
|
||||
```java
|
||||
public EvenOnlySortStrategy(SortStrategy<T> sortStrategy)
|
||||
```
|
||||
|
||||
Также реализовать метод:
|
||||
|
||||
```java
|
||||
public void setSortStrategy(SortStrategy<T> sortStrategy)
|
||||
```
|
||||
|
||||
и основной метод интерфейса `SortStrategy`:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void sort(
|
||||
MyList<T> list,
|
||||
Comparator<T> comparator
|
||||
)
|
||||
```
|
||||
|
||||
Для определения чётности числового поля использовать:
|
||||
|
||||
```java
|
||||
ToIntFunction<T>
|
||||
```
|
||||
|
||||
Поэтому рекомендуется также передавать функцию получения числового значения через конструктор или отдельный метод настройки.
|
||||
|
||||
Для `Student` в качестве функции использовать:
|
||||
|
||||
```java
|
||||
Student::getRecordBookNumber
|
||||
```
|
||||
|
||||
## Поведение при отсутствии стратегии
|
||||
|
||||
Если `sortStrategy` не задан:
|
||||
|
||||
```java
|
||||
EvenOnlySortStrategy<Student> strategy =
|
||||
new EvenOnlySortStrategy<>();
|
||||
```
|
||||
|
||||
то вызов:
|
||||
|
||||
```java
|
||||
strategy.sort(students, comparator);
|
||||
```
|
||||
|
||||
должен завершаться контролируемой ошибкой.
|
||||
|
||||
Рекомендуется выбрасывать:
|
||||
|
||||
```java
|
||||
IllegalStateException
|
||||
```
|
||||
|
||||
с понятным сообщением о том, что стратегия сортировки не задана.
|
||||
|
||||
Не следует использовать `null` как признак успешного выполнения сортировки.
|
||||
|
||||
## Поведение `setSortStrategy`
|
||||
|
||||
Метод:
|
||||
|
||||
```java
|
||||
setSortStrategy(SortStrategy<T> 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<T>` определить чётность каждого элемента.
|
||||
3. Запомнить позиции элементов с чётным значением.
|
||||
4. Сформировать отдельный `MyList<T>` только из чётных элементов.
|
||||
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<Student> strategy =
|
||||
new EvenOnlySortStrategy<>(
|
||||
new MergeSortStrategy<>(),
|
||||
Student::getRecordBookNumber
|
||||
);
|
||||
```
|
||||
|
||||
А также после создания заменить стратегию:
|
||||
|
||||
```java
|
||||
strategy.setSortStrategy(new AnotherSortStrategy<>());
|
||||
```
|
||||
|
||||
После этого специальная сортировка должна использовать новую стратегию без изменения `EvenOnlySortStrategy`.
|
||||
|
||||
## Требования к `ToIntFunction`
|
||||
|
||||
Функция получения числового значения должна передаваться извне.
|
||||
|
||||
Для `Student`:
|
||||
|
||||
```java
|
||||
Student::getRecordBookNumber
|
||||
```
|
||||
|
||||
Стратегия не должна напрямую обращаться к:
|
||||
|
||||
```java
|
||||
student.getRecordBookNumber()
|
||||
```
|
||||
|
||||
или к другим полям `Student`.
|
||||
|
||||
Это позволяет использовать класс с другими типами объектов и другими числовыми полями.
|
||||
|
||||
## Требования к коллекциям
|
||||
|
||||
Работать необходимо с:
|
||||
|
||||
```java
|
||||
MyList<T>
|
||||
```
|
||||
|
||||
Использование `ArrayList`, `LinkedList` и других стандартных коллекций для хранения элементов запрещено.
|
||||
|
||||
Вспомогательная коллекция чётных элементов также должна быть `MyList<T>`.
|
||||
|
||||
## Тесты
|
||||
|
||||
### Переданная стратегия используется
|
||||
|
||||
Передать `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<T>
|
||||
MyList<T>
|
||||
Comparator<T>
|
||||
ToIntFunction<T>
|
||||
```
|
||||
|
||||
и **не должен содержать реализации конкретного алгоритма сортировки**.
|
||||
|
||||
## Git
|
||||
|
||||
Ветка:
|
||||
|
||||
```text
|
||||
feature/even-only-sort-strategy
|
||||
```
|
||||
@@ -0,0 +1,305 @@
|
||||
# Техническое задание: генератор случайных студентов
|
||||
|
||||
## Задача
|
||||
|
||||
Реализовать класс для формирования `MyList<Student>` со случайными значениями.
|
||||
|
||||
По умолчанию ограничения диапазона для генерации не задаются. В таком случае значения должны генерироваться во всём допустимом диапазоне, установленном правилами класса `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<Student> generate(int size);
|
||||
```
|
||||
|
||||
Контракт:
|
||||
|
||||
- создаёт ровно `size` объектов `Student`;
|
||||
- все объекты должны быть валидными;
|
||||
- каждый объект создаётся через `StudentBuilder`;
|
||||
- результат возвращается как `MyList<Student>`;
|
||||
- стандартные `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>
|
||||
```
|
||||
|
||||
Дополнительную валидацию, дублирующую правила `Student`, в генераторе не реализовывать.
|
||||
|
||||
## Примеры использования
|
||||
|
||||
Без настройки диапазонов:
|
||||
|
||||
```java id="s2k8m4"
|
||||
RandomStudentGenerator generator =
|
||||
new RandomStudentGenerator();
|
||||
|
||||
MyList<Student> 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<Student> students =
|
||||
generator.generate(100);
|
||||
```
|
||||
|
||||
## Тесты
|
||||
|
||||
### Генерация без ограничений
|
||||
|
||||
```java id="r1w5p8"
|
||||
MyList<Student> 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<Student>`.
|
||||
|
||||
### Некорректный размер
|
||||
|
||||
Для отрицательного размера:
|
||||
|
||||
```java id="n2k7p4"
|
||||
generate(-1)
|
||||
```
|
||||
|
||||
ожидается `IllegalArgumentException`.
|
||||
|
||||
## Результат
|
||||
|
||||
Необходимо реализовать:
|
||||
|
||||
```text id="v8m3q6"
|
||||
RandomStudentGenerator.java
|
||||
```
|
||||
|
||||
Класс должен использовать существующие:
|
||||
|
||||
```text id="f1j9x4"
|
||||
Student.java
|
||||
StudentBuilder.java
|
||||
MyList.java
|
||||
```
|
||||
|
||||
Изменять интерфейсы `Student`, `StudentBuilder` и `MyList` для выполнения данного задания не требуется.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Техническое задание: сортировка слиянием
|
||||
|
||||
## Задача
|
||||
|
||||
Реализовать паттерн Strategy и собственный алгоритм сортировки слиянием объектов `Student`.
|
||||
|
||||
## Требования
|
||||
|
||||
1. Создать интерфейс `SortStrategy<T>` с методом:
|
||||
|
||||
```text
|
||||
sort(MyList<T> list, Comparator<T> comparator)
|
||||
```
|
||||
|
||||
1. Создать класс `MergeSortStrategy<T>`, реализующий `SortStrategy<T>`.
|
||||
|
||||
2. Реализовать алгоритм сортировки слиянием самостоятельно.
|
||||
|
||||
3. Готовые методы сортировки Java не использовать.
|
||||
|
||||
4. Сортировка должна работать с `MyList<T>`.
|
||||
|
||||
5. Сортировка должна использовать переданный `Comparator<T>`.
|
||||
|
||||
6. Реализовать сортировку `Student` по каждому из трёх полей:
|
||||
|
||||
- `groupNumber`;
|
||||
- `averageGrade`;
|
||||
- `recordBookNumber`.
|
||||
|
||||
7. Создать отдельные компараторы:
|
||||
|
||||
- `StudentGroupComparator`;
|
||||
- `StudentAverageGradeComparator`;
|
||||
- `StudentRecordBookComparator`.
|
||||
|
||||
8. `MergeSortStrategy` не должен зависеть от класса `Student`.
|
||||
|
||||
## Результат
|
||||
|
||||
```text
|
||||
SortStrategy.java
|
||||
MergeSortStrategy.java
|
||||
|
||||
StudentGroupComparator.java
|
||||
StudentAverageGradeComparator.java
|
||||
StudentRecordBookComparator.java
|
||||
```
|
||||
@@ -0,0 +1,327 @@
|
||||
# Техническое задание: заполнение `MyList<Student>` посредством Stream API
|
||||
|
||||
## Задача
|
||||
|
||||
Реализовать механизм заполнения собственной коллекции `MyList<Student>` посредством Java Stream API без использования стандартных коллекций Java в качестве промежуточного или итогового хранилища.
|
||||
|
||||
## Требования
|
||||
|
||||
1. Итоговым результатом работы потока должна быть:
|
||||
|
||||
```java
|
||||
MyList<Student>
|
||||
```
|
||||
|
||||
1. Для сбора элементов потока непосредственно в `MyList<Student>` реализовать собственный `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<T>`
|
||||
|
||||
Создать универсальный класс:
|
||||
|
||||
```java
|
||||
public class MyListCollector<T>
|
||||
implements Collector<T, MyList<T>, MyList<T>> {
|
||||
}
|
||||
```
|
||||
|
||||
Реализовать методы:
|
||||
|
||||
```text
|
||||
supplier()
|
||||
accumulator()
|
||||
combiner()
|
||||
finisher()
|
||||
characteristics()
|
||||
```
|
||||
|
||||
Назначение:
|
||||
|
||||
- `supplier()` — создать пустой `MyList<T>`;
|
||||
- `accumulator()` — добавить очередной элемент в `MyList<T>`;
|
||||
- `combiner()` — объединить две частично заполненные коллекции;
|
||||
- `finisher()` — вернуть готовый `MyList<T>`;
|
||||
- `characteristics()` — определить характеристики сборщика.
|
||||
|
||||
Коллектор должен работать с `Student` без специальной логики, связанной с его полями.
|
||||
|
||||
---
|
||||
|
||||
## `MyCollectors`
|
||||
|
||||
Создать вспомогательный класс:
|
||||
|
||||
```java
|
||||
public final class MyCollectors {
|
||||
|
||||
public static <T> Collector<T, MyList<T>, MyList<T>> toMyList() {
|
||||
return new MyListCollector<>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Класс должен предоставлять удобный способ получения собственного сборщика.
|
||||
|
||||
---
|
||||
|
||||
## Контракт использования
|
||||
|
||||
Должна поддерживаться конструкция:
|
||||
|
||||
```java
|
||||
MyList<Student> students = Stream.of(
|
||||
student1,
|
||||
student2,
|
||||
student3
|
||||
).collect(MyCollectors.toMyList());
|
||||
```
|
||||
|
||||
где `student1`, `student2`, `student3` — валидные объекты `Student`, предварительно созданные через `StudentBuilder`.
|
||||
|
||||
Результатом должен быть именно `MyList<Student>`.
|
||||
|
||||
---
|
||||
|
||||
## Пример `map`
|
||||
|
||||
`map` должен использоваться для преобразования одного `Student` в новый `Student`.
|
||||
|
||||
Исходный объект не изменяется. Для создания нового объекта использовать `with...()`.
|
||||
|
||||
Одно из преобразуемых полей должно быть `String` и формироваться по заданному бизнес-правилу:
|
||||
|
||||
```text
|
||||
Заглавная латинская буква + двузначное число
|
||||
```
|
||||
|
||||
Например:
|
||||
|
||||
```text
|
||||
A07
|
||||
B12
|
||||
Z35
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```java
|
||||
MyList<Student> 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<Student>`, после чего он непосредственно собирается в `MyList<Student>`.
|
||||
|
||||
Формат создаваемого `groupNumber` должен соответствовать:
|
||||
|
||||
```text
|
||||
^[A-Z][0-9]{2}$
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Пример `filter` совместно с `map`
|
||||
|
||||
Проверить совместную работу промежуточных операций:
|
||||
|
||||
```java
|
||||
MyList<Student> 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<Student>`.
|
||||
|
||||
Реалистичный пример:
|
||||
|
||||
```java
|
||||
MyList<Student> 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<Student> 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<Student> result = Stream.<Student>empty()
|
||||
.collect(MyCollectors.toMyList());
|
||||
```
|
||||
|
||||
Проверить:
|
||||
|
||||
```text
|
||||
result != null
|
||||
result.isEmpty()
|
||||
```
|
||||
|
||||
### Проверка `map`
|
||||
|
||||
Преобразовать несколько `Student` через `withGroupNumber()`.
|
||||
|
||||
Проверить:
|
||||
|
||||
- исходные объекты не изменились;
|
||||
- созданные объекты отличаются от исходных;
|
||||
- `groupNumber` соответствует формату `[A-Z][0-9]{2}`.
|
||||
|
||||
### Проверка `filter` и `map`
|
||||
|
||||
Проверить, что после `filter` и `map` в `MyList<Student>` попадают только необходимые студенты с корректно сформированным `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<T>`, а не от конкретной реализации `MyLinkedList<T>`.
|
||||
|
||||
`MyCollectors` должен содержать только методы создания собственных сборщиков и не должен содержать бизнес-логику.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Классы не должны зависеть от сортировки, коллекции, меню и ввода данных.
|
||||
@@ -0,0 +1,688 @@
|
||||
# Техническое задание: многопоточный поиск индексов `Student`
|
||||
|
||||
## Задача
|
||||
|
||||
Реализовать многопоточный поиск всех вхождений заданного объекта `Student` в `MyList<Student>`.
|
||||
|
||||
Метод **не должен выводить никаких сообщений в консоль**.
|
||||
|
||||
Результатом работы должен быть массив `int[]`, содержащий индексы всех элементов исходной коллекции, которые равны переданному `Student`.
|
||||
|
||||
Сравнение выполнять через существующий `Student.equals()`.
|
||||
|
||||
---
|
||||
|
||||
## Входная информация
|
||||
|
||||
Основной метод принимает:
|
||||
|
||||
```java
|
||||
MyList<Student> 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<Student> 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<Integer> findInRange(
|
||||
MyList<Student> students,
|
||||
Student target,
|
||||
int fromIndex,
|
||||
int toIndex
|
||||
);
|
||||
```
|
||||
|
||||
Обрабатывает диапазон:
|
||||
|
||||
```text
|
||||
[fromIndex, toIndex)
|
||||
```
|
||||
|
||||
и возвращает индексы найденных совпадений.
|
||||
|
||||
---
|
||||
|
||||
# Работа с многопоточностью
|
||||
|
||||
## Рекомендуемый подход
|
||||
|
||||
Не использовать общий контейнер результатов, к которому одновременно обращаются несколько потоков.
|
||||
|
||||
Также **не рекомендуется использовать мьютексы** (`synchronized`, `Lock`, `ReentrantLock`) для хранения результата.
|
||||
|
||||
Лучше использовать модель:
|
||||
|
||||
```text
|
||||
MyList<Student>
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
↓ ↓ ↓
|
||||
Поток 1 Поток 2 Поток 3
|
||||
диапазон диапазон диапазон
|
||||
│ │ │
|
||||
↓ ↓ ↓
|
||||
MyList<Integer> MyList<Integer> MyList<Integer>
|
||||
└────────────┼────────────┘
|
||||
↓
|
||||
основной поток
|
||||
↓
|
||||
merge
|
||||
↓
|
||||
int[]
|
||||
```
|
||||
|
||||
Каждый поток создаёт **собственный контейнер `MyList<Integer>`** и записывает найденные индексы только в него.
|
||||
|
||||
Это позволяет избежать состояния гонки без синхронизации общей структуры.
|
||||
|
||||
---
|
||||
|
||||
# Разбиение коллекции на потоки
|
||||
|
||||
Количество потоков рекомендуется определять на основании:
|
||||
|
||||
```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<Integer>
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```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<Integer>
|
||||
```
|
||||
|
||||
а объединение выполняется после завершения параллельной части одним потоком.
|
||||
|
||||
---
|
||||
|
||||
# Рекомендуемые классы и импорты
|
||||
|
||||
Для реализации рекомендуется использовать стандартные средства 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<MyList<Integer>>
|
||||
↓
|
||||
Future<MyList<Integer>>
|
||||
```
|
||||
|
||||
Каждый `Callable` обрабатывает свой диапазон и возвращает собственный `MyList<Integer>`.
|
||||
|
||||
После получения всех `Future` основной метод объединяет результаты.
|
||||
|
||||
После завершения работы `ExecutorService` должен быть корректно закрыт.
|
||||
|
||||
При `InterruptedException` необходимо восстановить статус прерывания текущего потока:
|
||||
|
||||
```java
|
||||
Thread.currentThread().interrupt();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Использование `MyList`
|
||||
|
||||
Сторонние контейнеры **не допускаются**.
|
||||
|
||||
Запрещено использовать:
|
||||
|
||||
```text
|
||||
ArrayList
|
||||
LinkedList
|
||||
Vector
|
||||
CopyOnWriteArrayList
|
||||
ConcurrentLinkedQueue
|
||||
и другие стандартные контейнеры
|
||||
```
|
||||
|
||||
как хранилище промежуточных или итоговых результатов.
|
||||
|
||||
Для результатов каждого потока использовать:
|
||||
|
||||
```java
|
||||
MyList<Integer>
|
||||
```
|
||||
|
||||
Итог преобразовать в:
|
||||
|
||||
```java
|
||||
int[]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Требуется ли изменять `MyList`
|
||||
|
||||
Текущего интерфейса **достаточно для реализации задачи**:
|
||||
|
||||
```java
|
||||
public interface MyList<T> {
|
||||
|
||||
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<Integer>`, существующего интерфейса достаточно.
|
||||
|
||||
### Важно
|
||||
|
||||
Метод:
|
||||
|
||||
```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<Integer>.
|
||||
9. Основной поток получает результаты через Future.
|
||||
10. Объединяет MyList<Integer> в порядке диапазонов.
|
||||
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
|
||||
```
|
||||
|
||||
изменять не требуется.
|
||||
@@ -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>`;
|
||||
- сортировки;
|
||||
- источники данных;
|
||||
- тестовые классы;
|
||||
- создание `Student`;
|
||||
- `with...()`-операции;
|
||||
- чтение и запись данных, если они используют `recordBookNumber`.
|
||||
|
||||
Например, существующий:
|
||||
|
||||
```java
|
||||
implements Comparator<Student>
|
||||
```
|
||||
|
||||
должен использовать актуальный метод:
|
||||
|
||||
```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...()`, ни внешние классы не должны позволять создать невалидный объект.
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Таким образом, мы сохраняем логическое разделение фич и одновременно можем использовать необходимый код из другой ветки.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 <commit>
|
||||
```
|
||||
|
||||
## Вариант 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`.
|
||||
|
||||
**Принцип простой: сначала сохранить то, что может понадобиться, потом двигать указатель ветки.**
|
||||
Reference in New Issue
Block a user