Merge branch 'develop' into feature/datasource

This commit is contained in:
2026-09-01 15:49:53 +04:00
64 changed files with 8001 additions and 29 deletions
+21
View File
@@ -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>
-10
View File
@@ -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();
}
}
-19
View File
@@ -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");
}
}
+130
View File
@@ -0,0 +1,130 @@
Главное меню
{
Показать текущую коллекцию
Меню создания новой коллекции
{
Вернуться
Задать размер
Настроить способ заполнения
{
Пользовательские данные
{
Задать поле 'Название A'
Задать поле 'Название B'
Задать поле 'Название C'
Задать индекс изменяемого объекта
Изменить данные объекта
Предпросмотр
}
Случайные данные
{
Настройка диапазона генерации
{
Поле 'Название A'
{
Задать минимальное значение
Задать максимальное значение
}
Поле 'Название B'
{
Задать минимальное значение
Задать максимальное значение
}
Поле 'Название C'
{
Задать минимальное значение
Задать максимальное значение
}
}
}
Данные из файла
{
Задать путь до файла
{
Абсолютный
Относительный
}
}
}
Выбрать способ заполнения
{
Пользовательские данные *
Случайные данные
Данные из файла
}
Создать новую коллекцию
}
Меню сохранения текущей коллекции в файл
{
Вывести текущий абсолютный путь до файла
Задать путь до файла
{
Абсолютный
Относительный
}
Дописать коллекцию в текущий файл
}
Меню сортировки текущей коллекции
{
Вернуться
Настроить способ сортировки
{
Обычная сортировка
{
Сортировать по полю 'Название A'
Сортировать по полю 'Название B'
Сортировать по полю 'Название C'
}
}
Настроить способ сортировки
{
Сортировка по чётности
{
Выбрать числовое поле 'Название A'
Выбрать числовое поле 'Название B'
}
}
Выбрать способ сортировки
{
Обычная сортировка *
Сортировка по чётности
}
Сортировать текущую коллекцию
}
Меню поиска элемента в текущей коллекции
{
Вернуться
Задать значение поля 'Название A'
Задать значение поля 'Название B'
Задать значение поля 'Название C'
Найти первое вхождение в текущей коллекции
}
Меню подсчёта количества вхождений элемента
{
Задать значение поля 'Название A'
Задать значение поля 'Название B'
Задать значение поля 'Название C'
Выполнить многопоточный подсчёт
}
Завершить выполнение программы
}
+455
View File
@@ -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
```
+441
View File
@@ -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
```
+44
View File
@@ -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
```
+368
View File
@@ -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
```
+305
View File
@@ -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` для выполнения данного задания не требуется.
+48
View File
@@ -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
```
+327
View File
@@ -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` должен содержать только методы создания собственных сборщиков и не должен содержать бизнес-логику.
+46
View File
@@ -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
```
Классы не должны зависеть от сортировки, коллекции, меню и ввода данных.
+688
View File
@@ -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
```
изменять не требуется.
+326
View File
@@ -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...()`, ни внешние классы не должны позволять создать невалидный объект.
+67
View File
@@ -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`.
+122
View File
@@ -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`.
**Принцип простой: сначала сохранить то, что может понадобиться, потом двигать указатель ветки.**