From 554ca3c8a6ce65fcbcc9d7e5ee7143f30056ad36 Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 2 Sep 2025 18:20:55 +0300 Subject: [PATCH 01/41] feat: duration and startTime add --- src/model/Epic.java | 19 +++++++++++++++++++ src/model/SubTask.java | 17 +++++++++++++++++ src/model/Task.java | 43 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index 64ac2c9..3cd82c4 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -12,9 +12,28 @@ public Epic(String title, String description, Status status) { } public void addSubTask(SubTask subTask) { + + setDurationAndStartTime(subTask); subtaskIds.add(subTask.getTaskId()); } + private void setDurationAndStartTime(SubTask subTask) { + if (subTask.getDuration() == null) { + return; + } + + duration = duration == null ? subTask.getDuration() : duration.plus(subTask.getDuration()); + + if (subTask.getStartTime() == null) { + return; + } + + if (startTime == null) { + startTime = subTask.getStartTime(); + } else { + startTime = startTime.isAfter(subTask.getStartTime()) ? subTask.getStartTime() : startTime; + } + } public void deleteSubTask(int id) { subtaskIds.remove(Integer.valueOf(id)); diff --git a/src/model/SubTask.java b/src/model/SubTask.java index f008fcd..22f50e0 100644 --- a/src/model/SubTask.java +++ b/src/model/SubTask.java @@ -1,5 +1,7 @@ package model; +import java.time.LocalDateTime; + public class SubTask extends Task { private final int epicId; @@ -8,6 +10,21 @@ public SubTask(String title, String description, Status status, int epicId) { this.epicId = epicId; } + public SubTask(String title, + String description, + Status status, + int epicId, + int durationInMinutes, + LocalDateTime startTime) { + super(title, description, status, durationInMinutes, startTime); + this.epicId = epicId; + } + + public SubTask(String title, String description, Status status, int epicId, int durationInMinutes) { + super(title, description, status, durationInMinutes); + this.epicId = epicId; + } + public int getEpicId() { return epicId; } diff --git a/src/model/Task.java b/src/model/Task.java index fb40b07..d9757a8 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,5 +1,7 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.Objects; public class Task { @@ -7,7 +9,8 @@ public class Task { protected String title; protected String description; protected Status status; - + protected Duration duration; + protected LocalDateTime startTime; public Task(String title, String description, Status status) { this.title = title; @@ -15,6 +18,21 @@ public Task(String title, String description, Status status) { this.status = status; } + public Task(String title, String description, Status status, int durationInMinutes, LocalDateTime startTime) { + this.title = title; + this.description = description; + this.status = status; + this.duration = Duration.ofMinutes(durationInMinutes); + this.startTime = startTime; + } + + public Task(String title, String description, Status status, int durationInMinutes) { + this.title = title; + this.description = description; + this.status = status; + this.duration = Duration.ofMinutes(durationInMinutes); + } + public int getTaskId() { return taskId; } @@ -47,6 +65,29 @@ public void setStatus(Status status) { this.status = status; } + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public LocalDateTime getEndTime() { + if (startTime == null || duration == null) { + return null; + } + return startTime.plus(duration); + } + public Type getType() { return Type.TASK; } From e6fa39b0e6751c856103d12e2a10204b025a7b7a Mon Sep 17 00:00:00 2001 From: Crodi Date: Tue, 2 Sep 2025 18:53:42 +0300 Subject: [PATCH 02/41] fix: simpler logic for Epic time --- src/model/Epic.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index 3cd82c4..a97445a 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,5 +1,6 @@ package model; +import java.time.LocalDateTime; import java.util.ArrayList; public class Epic extends Task { @@ -18,6 +19,7 @@ public void addSubTask(SubTask subTask) { } private void setDurationAndStartTime(SubTask subTask) { + if (subTask.getDuration() == null) { return; } @@ -28,10 +30,12 @@ private void setDurationAndStartTime(SubTask subTask) { return; } - if (startTime == null) { - startTime = subTask.getStartTime(); - } else { - startTime = startTime.isAfter(subTask.getStartTime()) ? subTask.getStartTime() : startTime; + LocalDateTime subtaskStartTime = subTask.getStartTime(); + + if (startTime == null || startTime.isAfter(subtaskStartTime)) { + // Обновляем время начала, если оно еще не установлено + // или текущее начало позже начала подзадачи + startTime = subtaskStartTime; } } From 3667a367138f1dd44cf166973dc82f6cee58d6cd Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 01:21:18 +0300 Subject: [PATCH 03/41] refactor: util package created and moved util classes --- src/{model => util}/Status.java | 2 +- src/{model => util}/Type.java | 2 +- src/{managers => util}/exceptions/ManagerSaveException.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{model => util}/Status.java (79%) rename src/{model => util}/Type.java (77%) rename src/{managers => util}/exceptions/ManagerSaveException.java (91%) diff --git a/src/model/Status.java b/src/util/Status.java similarity index 79% rename from src/model/Status.java rename to src/util/Status.java index 227c64b..9e4a21d 100644 --- a/src/model/Status.java +++ b/src/util/Status.java @@ -1,4 +1,4 @@ -package model; +package util; public enum Status { NEW, diff --git a/src/model/Type.java b/src/util/Type.java similarity index 77% rename from src/model/Type.java rename to src/util/Type.java index cfa6872..dff2b72 100644 --- a/src/model/Type.java +++ b/src/util/Type.java @@ -1,4 +1,4 @@ -package model; +package util; public enum Type { TASK, diff --git a/src/managers/exceptions/ManagerSaveException.java b/src/util/exceptions/ManagerSaveException.java similarity index 91% rename from src/managers/exceptions/ManagerSaveException.java rename to src/util/exceptions/ManagerSaveException.java index 9a00698..cb8f5a3 100644 --- a/src/managers/exceptions/ManagerSaveException.java +++ b/src/util/exceptions/ManagerSaveException.java @@ -1,4 +1,4 @@ -package managers.exceptions; +package util.exceptions; public class ManagerSaveException extends RuntimeException { From 81f44da75ea3cbcc2fb498e83f41854f46c8eda2 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 01:22:07 +0300 Subject: [PATCH 04/41] refactor: util package changes --- src/managers/FileBackedTaskManager.java | 6 ++++-- test/managers/FileBackedTaskManagerTest.java | 4 ++-- test/managers/InMemoryHistoryManagerTest.java | 2 +- test/managers/InMemoryTaskManagerTest.java | 1 + test/model/EpicTest.java | 1 + test/model/SubTaskTest.java | 1 + 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/managers/FileBackedTaskManager.java b/src/managers/FileBackedTaskManager.java index 7ce1cc6..9d8971d 100644 --- a/src/managers/FileBackedTaskManager.java +++ b/src/managers/FileBackedTaskManager.java @@ -1,13 +1,15 @@ package managers; -import managers.exceptions.ManagerSaveException; +import util.exceptions.ManagerSaveException; import model.*; +import util.Status; +import util.Type; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import static model.Type.*; +import static util.Type.*; public class FileBackedTaskManager extends InMemoryTaskManager { diff --git a/test/managers/FileBackedTaskManagerTest.java b/test/managers/FileBackedTaskManagerTest.java index 778bb7a..71bcea2 100644 --- a/test/managers/FileBackedTaskManagerTest.java +++ b/test/managers/FileBackedTaskManagerTest.java @@ -1,8 +1,8 @@ package managers; -import managers.exceptions.ManagerSaveException; +import util.exceptions.ManagerSaveException; import model.Epic; -import model.Status; +import util.Status; import model.SubTask; import model.Task; import org.junit.jupiter.api.BeforeEach; diff --git a/test/managers/InMemoryHistoryManagerTest.java b/test/managers/InMemoryHistoryManagerTest.java index 540ae7e..af5d497 100644 --- a/test/managers/InMemoryHistoryManagerTest.java +++ b/test/managers/InMemoryHistoryManagerTest.java @@ -2,7 +2,7 @@ import managers.history.HistoryManager; import model.Epic; -import model.Status; +import util.Status; import model.SubTask; import model.Task; import org.junit.jupiter.api.Test; diff --git a/test/managers/InMemoryTaskManagerTest.java b/test/managers/InMemoryTaskManagerTest.java index dded598..c4da0a9 100644 --- a/test/managers/InMemoryTaskManagerTest.java +++ b/test/managers/InMemoryTaskManagerTest.java @@ -3,6 +3,7 @@ import model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import util.Status; import static org.junit.jupiter.api.Assertions.*; diff --git a/test/model/EpicTest.java b/test/model/EpicTest.java index 5e365f2..f44d007 100644 --- a/test/model/EpicTest.java +++ b/test/model/EpicTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import util.Status; public class EpicTest { diff --git a/test/model/SubTaskTest.java b/test/model/SubTaskTest.java index f032c10..dce8d68 100644 --- a/test/model/SubTaskTest.java +++ b/test/model/SubTaskTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import util.Status; public class SubTaskTest { From 9b61c080a11f9d03362210036cca4380a225776e Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 01:22:31 +0300 Subject: [PATCH 05/41] refactor: util package changes --- test/model/TaskTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/model/TaskTest.java b/test/model/TaskTest.java index 0051c12..d068eeb 100644 --- a/test/model/TaskTest.java +++ b/test/model/TaskTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import util.Status; public class TaskTest { From bbf01f5a0782be542669acf56bac0a4b2f348b37 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 01:26:22 +0300 Subject: [PATCH 06/41] feat: add TaskTimeController - class that controls date and time and stores all tasks in priority order. add Exception for this class. made Task to implement Comparable and created compareTo method. updated toString method. add TaskTimeController and it`s methods to InMemoryTaskManager. --- src/managers/InMemoryTaskManager.java | 28 +++- src/managers/TaskManager.java | 10 +- src/model/Task.java | 32 ++++- src/util/TaskTimeController.java | 128 ++++++++++++++++++ .../exceptions/TaskTimeOverlapException.java | 16 +++ 5 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 src/util/TaskTimeController.java create mode 100644 src/util/exceptions/TaskTimeOverlapException.java diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index f79fb1b..7c4faa0 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -2,26 +2,30 @@ import managers.history.HistoryManager; import model.Epic; -import model.Status; +import util.Status; import model.SubTask; import model.Task; +import util.TaskTimeController; +import util.exceptions.TaskTimeOverlapException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; public class InMemoryTaskManager implements TaskManager { private int idCount; private final HashMap tasks = new HashMap<>(); private final HashMap epics = new HashMap<>(); private final HashMap subtasks = new HashMap<>(); - private final HistoryManager historyManager = Managers.getDefaultHistory(); + private final TaskTimeController taskTimeController = new TaskTimeController(); @Override public void addTask(Task task) { + if (taskTimeController.isTimeOverlapping(task)) { + throw new TaskTimeOverlapException("Can`t add task: " + task); + } task.setTaskId(++idCount); tasks.put(task.getTaskId(), task); + taskTimeController.add(task); } @Override @@ -32,12 +36,17 @@ public void addEpic(Epic epic) { @Override public void addSubTask(SubTask subTask) { + if (taskTimeController.isTimeOverlapping(subTask)) { + throw new TaskTimeOverlapException("Can`t add task: " + subTask); + } subTask.setTaskId(++idCount); subtasks.put(subTask.getTaskId(), subTask); Epic epic = epics.get(subTask.getEpicId()); epic.addSubTask(subTask); updateEpicStatus(epic); + + taskTimeController.add(subTask); } @Override @@ -88,6 +97,11 @@ public ArrayList getSubTasksFromEpic(int id) { return subTasksList; } + @Override + public List getPrioritizedTasks() { + return List.copyOf(taskTimeController.getTimeSortedTasks()); + } + @Override public void updateTask(Task task) { tasks.put(task.getTaskId(), task); @@ -153,12 +167,14 @@ public void deleteTask(int id) { } historyManager.remove(id); // удаляем из истории tasks.remove(id); + taskTimeController.remove(id); } @Override public void clearTasks() { clearHistoryTasks(); tasks.clear(); + taskTimeController.removeTasks(); } @Override @@ -201,6 +217,7 @@ public void deleteSubTask(int id) { historyManager.remove(id); // удаляем из истории subtasks.remove(id); + taskTimeController.remove(id); } @Override @@ -210,6 +227,7 @@ public void clearSubTasks() { for (Epic epic : epics.values()) { epic.clearSubtasks(); // статус эпика обновляется внутри метода } + taskTimeController.removeSubTasks(); } private void clearHistoryTasks() { diff --git a/src/managers/TaskManager.java b/src/managers/TaskManager.java index 9ac718e..01f0707 100644 --- a/src/managers/TaskManager.java +++ b/src/managers/TaskManager.java @@ -20,13 +20,15 @@ public interface TaskManager { SubTask getSubTask(int id); - ArrayList getTasks(); + List getTasks(); - ArrayList getEpics(); + List getEpics(); - ArrayList getSubTasks(); + List getSubTasks(); - ArrayList getSubTasksFromEpic(int id); + List getSubTasksFromEpic(int id); + + List getPrioritizedTasks(); void updateTask(Task task); diff --git a/src/model/Task.java b/src/model/Task.java index d9757a8..e53c39e 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,10 +1,14 @@ package model; +import util.Status; +import util.Type; + import java.time.Duration; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Objects; -public class Task { +public class Task implements Comparable { protected int taskId; protected String title; protected String description; @@ -94,15 +98,23 @@ public Type getType() { @Override public String toString() { - return String.format("%s{id=%d, title=%s, description=%s, status=%s}", - this.getClass(), + return String.format("%s{id=%d, title=%s, description=%s, status=%s,\n startTime=[%s], duration=[%s], endTime=[%s]}\n", + this.getClass().getName(), taskId, title, description, - status + status, + formatDateTime(startTime), + duration, + formatDateTime(getEndTime()) ); } + protected String formatDateTime(LocalDateTime dateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yy | HH:mm"); + return dateTime == null ? null : dateTime.format(formatter); + } + @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -114,4 +126,16 @@ public boolean equals(Object obj) { this.taskId == copy.taskId; } + @Override + public int compareTo(Task t) { + LocalDateTime tStartTime = t.getStartTime(); + + if (this.startTime.isAfter(tStartTime)) { + return 1; + } else if (this.startTime.equals(tStartTime)) { + return 0; + } else { + return -1; + } + } } diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java new file mode 100644 index 0000000..5e176f5 --- /dev/null +++ b/src/util/TaskTimeController.java @@ -0,0 +1,128 @@ +package util; + +import model.Task; + +import java.util.TreeSet; + +/** + * Контроллер для управления временными интервалами задач. + * Обеспечивает хранение задач в порядке приоритета и проверку пересечений временных интервалов. + * Приоритет задачи определяется по времени начала выполнения ({@code startTime}). + * + *

Класс предназначен для использования менеджерами задач для валидации временных промежутков + * и обеспечения отсутствия конфликтов в расписании. Игнорирует попытки добавить эпики и задачи + * без установленных временных параметров. + */ +public class TaskTimeController { + + private final TreeSet timeSortedTasks = new TreeSet<>(); + + /** + * Проверка пересечений временных интервалов с использованием {@code stream API}. + * Использует метод anyMatch и сравнивает значение элемента со значением {@code task}. + * Предварительно проверяет присутствие у {@code task} поля времени: {@code duration}, {@code startTime} методом + * {@link #hasMissingTimeFields(Task)}. + * Если одно из полей равно {@code null} - возвращает {@code false}. + * + * @param task задача для проверки + * @return {@code true} если есть пересечение, {@code false} если временной промежуток доступен + */ + public boolean isTimeOverlapping(Task task) { + if (hasMissingTimeFields(task)) { + return false; + } + return timeSortedTasks.stream() + .anyMatch((element) -> + element.getEndTime().isAfter(task.getStartTime()) && + element.getStartTime().isBefore(task.getEndTime())); + } + + /** + * Оптимизированная проверка пересечений временных интервалов с использованием TreeSet. + * Использует алгоритм поиска соседних элементов {@code floor/ceiling} для O(log n) сложности. + * Предварительно проверяет присутствие у {@code task} поля времени: {@code duration}, {@code startTime} методом + * {@link #hasMissingTimeFields(Task)}. + * Если одно из полей равно {@code null} - возвращает {@code false}. + * + * @param task задача для проверки + * @return {@code true} если есть пересечение, {@code false} если временной промежуток доступен + * @apiNote Готов к использованию, но в настоящее время не используется по требованию ТЗ (stream API) + */ + public boolean isTimeOverlappingWithTreeSearch(Task task) { + if (hasMissingTimeFields(task)) { + return false; + } + Task floor = timeSortedTasks.floor(task); + + if (floor != null && floor.getEndTime().isAfter(task.getStartTime())) { + return true; + } + + Task ceiling = timeSortedTasks.ceiling(task); + + if (ceiling != null && ceiling.getStartTime().isBefore(task.getEndTime())) { + return true; + } + + return false; + } + + private boolean hasMissingTimeFields(Task task) { + return task.getDuration() == null || task.getStartTime() == null; + } + + /** + * Добавляет задачу в отсортированную коллекцию, если у нее установлены временные поля. + * Задачи без времени начала или продолжительности игнорируются без выброса исключения. + * Эпики игнорируются без выброса исключения. + * + * @param task задача для добавления + * @implNote Метод молча игнорирует эпики и задачи с отсутствующими временными полями. + */ + public void add(Task task) { + if (task.getType() == Type.EPIC || hasMissingTimeFields(task)) { + return; + } + timeSortedTasks.add(task); + } + + public void remove(Task task) { + timeSortedTasks.remove(task); + } + + public void remove(int id) { + timeSortedTasks.removeIf(element -> element.getTaskId() == id); + } + + /** + * Удаляет все задачи типа {@link Type#TASK} из коллекции. + * + * @apiNote Используется для выборочной очистки только обычных задач + */ + public void removeTasks() { + timeSortedTasks.removeIf(element -> element.getType() == Type.TASK); + } + + /** + * Удаляет все задачи типа {@link Type#SUBTASK} из коллекции. + * + * @apiNote Используется для выборочной очистки только подзадач + */ + public void removeSubTasks() { + timeSortedTasks.removeIf(element -> element.getType() == Type.SUBTASK); + } + + public void clear() { + timeSortedTasks.clear(); + } + + /** + * Возвращает неизменяемую копию отсортированного набора задач. + * Изменения в возвращаемой коллекции не влияют на внутреннее состояние. + * + * @return неизменяемый {@code TreeSet} с задачами, отсортированными по времени + */ + public TreeSet getTimeSortedTasks() { + return new TreeSet<>(timeSortedTasks); + } +} diff --git a/src/util/exceptions/TaskTimeOverlapException.java b/src/util/exceptions/TaskTimeOverlapException.java new file mode 100644 index 0000000..50156f3 --- /dev/null +++ b/src/util/exceptions/TaskTimeOverlapException.java @@ -0,0 +1,16 @@ +package util.exceptions; + +public class TaskTimeOverlapException extends RuntimeException{ + + public TaskTimeOverlapException() { + super(); + } + + public TaskTimeOverlapException(String message) { + super(message); + } + + public TaskTimeOverlapException(String message, Throwable cause) { + super(message, cause); + } +} From 19e8474ca40397d5b34e92a697eedd6dae37e022 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 01:27:30 +0300 Subject: [PATCH 07/41] feat: updated toString methods and some javadoc --- src/model/Epic.java | 27 ++++++++++++++++++++++++--- src/model/SubTask.java | 12 +++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index a97445a..8a4ee3c 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,5 +1,8 @@ package model; +import util.Status; +import util.Type; + import java.time.LocalDateTime; import java.util.ArrayList; @@ -18,6 +21,21 @@ public void addSubTask(SubTask subTask) { subtaskIds.add(subTask.getTaskId()); } + /** + * Обновляет длительность и время начала эпика на основе добавляемой подзадачи. + * + *

Длительность: Суммируется длительность всех подзадач эпика. + * Если текущая длительность равна {@code null}, устанавливается длительность подзадачи. + * + *

Время начала: Устанавливается самое раннее время начала среди всех подзадач. + * Если время начала подзадачи раньше текущего времени начала эпика или время начала эпика + * не установлено, время начала эпика обновляется. + * + *

Метод игнорирует подзадачи с отсутствующими временными параметрами ({@code null}). + * + * @param subTask подзадача, на основе которой обновляются параметры эпика + * @implNote Метод вызывается при добавлении каждой новой подзадачи к эпику + */ private void setDurationAndStartTime(SubTask subTask) { if (subTask.getDuration() == null) { @@ -58,13 +76,16 @@ public Type getType() { @Override public String toString() { - return String.format("%s{id=%d, title=%s, description=%s, status=%s, subtasks=%s}", - this.getClass(), + return String.format("%s{id=%d, title=%s, description=%s, status=%s, subtasks=%s,\n startTime=[%s], duration=[%s], endTime=[%s]}\n", + this.getClass().getName(), this.taskId, this.title, this.description, this.status, - this.subtaskIds + this.subtaskIds, + formatDateTime(startTime), + duration, + formatDateTime(getEndTime()) ); } diff --git a/src/model/SubTask.java b/src/model/SubTask.java index 22f50e0..f6e1c89 100644 --- a/src/model/SubTask.java +++ b/src/model/SubTask.java @@ -1,5 +1,8 @@ package model; +import util.Status; +import util.Type; + import java.time.LocalDateTime; public class SubTask extends Task { @@ -35,13 +38,16 @@ public Type getType() { @Override public String toString() { - return String.format("%s{id=%d, title=%s, description=%s, status=%s, epicId=%d}", - this.getClass(), + return String.format("%s{id=%d, title=%s, description=%s, status=%s, epicId=%d,\n startTime=[%s], duration=[%s], endTime=[%s]}\n", + this.getClass().getName(), this.taskId, this.title, this.description, this.status, - this.epicId + this.epicId, + formatDateTime(startTime), + duration, + formatDateTime(getEndTime()) ); } } From 373eb3486566447312668e74999424d3f9ffe045 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 02:27:08 +0300 Subject: [PATCH 08/41] refactor: made updateEpicStatus in InMemoryTaskManager more readable, add javadoc for method --- src/managers/InMemoryTaskManager.java | 41 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 7c4faa0..1f1a1a3 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -128,35 +128,46 @@ public void updateSubTask(SubTask subTask) { updateEpicStatus(epics.get(oldSubTask.getEpicId())); //обновляем статус эпика } + /** + * Обновляет статус эпика на основе статусов его подзадач. + * Статус эпика определяется по следующим правилам: + *

    + *
  • Если у эпика нет подзадач - статус устанавливается в {@link Status#NEW}
  • + *
  • Если все подзадачи имеют статус {@link Status#NEW} - эпик получает статус {@link Status#NEW}
  • + *
  • Если все подзадачи имеют статус {@link Status#DONE} - эпик получает статус {@link Status#DONE}
  • + *
  • В остальных случаях (смешанные статусы или подзадачи в процессе выполнения) - + * эпик получает статус {@link Status#IN_PROGRESS}
  • + *
+ * + * @param epic эпик, статус которого следует обновить. + * @implNote Вызывается при добавлении, удалении или изменении подзадачи эпика. + */ private void updateEpicStatus(Epic epic) { - ArrayList epicChildren = epic.getSubtaskIds(); + List epicChildren = epic.getSubtaskIds(); if (epicChildren.isEmpty()) { epic.setStatus(Status.NEW); return; } - int subTasksDone = 0; - int subTasksNotNew = 0; + int total = epicChildren.size(); + int doneCount = 0; + int newCount = 0; for (Integer id : epicChildren) { Status status = subtasks.get(id).getStatus(); - if (status != Status.NEW) { - subTasksNotNew++; - } - if (status == Status.DONE) { - subTasksDone++; + if (status == Status.NEW) { + newCount++; + } else if (status == Status.DONE) { + doneCount++; } } - if (subTasksNotNew != 0) { - epic.setStatus(Status.IN_PROGRESS); - } else { + if (total == newCount) { epic.setStatus(Status.NEW); - return; - } - - if (subTasksDone != 0 && subTasksDone == epicChildren.size()) { + } else if (total == doneCount) { epic.setStatus(Status.DONE); + } else { + epic.setStatus(Status.IN_PROGRESS); } } From 1812a9493458162e7bc8ae4b3a1ba9fb7511d2a3 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 16:05:41 +0300 Subject: [PATCH 09/41] refactor: comments improvement, methods clearHistory* transformed to .forEach() methods. simplified clearSubTasks(). Change return type for get* to List from ArrayList. getSubTasksFromEpic() logic simplified with steam() feat: add throws TaskTimeOverlapException to add* methods. add taskTimeController.remove() to delete/clear methods. --- src/managers/InMemoryTaskManager.java | 70 +++++++++++---------------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 1f1a1a3..63e55a8 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -2,13 +2,17 @@ import managers.history.HistoryManager; import model.Epic; -import util.Status; import model.SubTask; import model.Task; +import util.Status; import util.TaskTimeController; import util.exceptions.TaskTimeOverlapException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; public class InMemoryTaskManager implements TaskManager { private int idCount; @@ -19,7 +23,7 @@ public class InMemoryTaskManager implements TaskManager { private final TaskTimeController taskTimeController = new TaskTimeController(); @Override - public void addTask(Task task) { + public void addTask(Task task) throws TaskTimeOverlapException { if (taskTimeController.isTimeOverlapping(task)) { throw new TaskTimeOverlapException("Can`t add task: " + task); } @@ -35,7 +39,7 @@ public void addEpic(Epic epic) { } @Override - public void addSubTask(SubTask subTask) { + public void addSubTask(SubTask subTask) throws TaskTimeOverlapException { if (taskTimeController.isTimeOverlapping(subTask)) { throw new TaskTimeOverlapException("Can`t add task: " + subTask); } @@ -68,33 +72,30 @@ public SubTask getSubTask(int id) { } @Override - public ArrayList getTasks() { + public List getTasks() { return new ArrayList<>(tasks.values()); } @Override - public ArrayList getEpics() { + public List getEpics() { return new ArrayList<>(epics.values()); } @Override - public ArrayList getSubTasks() { + public List getSubTasks() { return new ArrayList<>(subtasks.values()); } @Override - public ArrayList getSubTasksFromEpic(int id) { + public List getSubTasksFromEpic(int id) { Epic epic = epics.get(id); if (epic == null) { return new ArrayList<>(); } - ArrayList subTasksList = new ArrayList<>(); - for (Integer subtaskId : epic.getSubtaskIds()) { - subTasksList.add(subtasks.get(subtaskId)); - } - - return subTasksList; + return epic.getSubtaskIds().stream() + .map(subtasks::get) + .toList(); } @Override @@ -177,13 +178,13 @@ public void deleteTask(int id) { return; } historyManager.remove(id); // удаляем из истории + taskTimeController.remove(tasks.get(id)); // удаляем объект т.к это быстрее, чем удаление по id tasks.remove(id); - taskTimeController.remove(id); } @Override public void clearTasks() { - clearHistoryTasks(); + tasks.forEach((id, task) -> historyManager.remove(id)); tasks.clear(); taskTimeController.removeTasks(); } @@ -196,21 +197,23 @@ public void deleteEpic(int id) { Epic epic = epics.get(id); for (Integer subtaskId : epic.getSubtaskIds()) { // удаляем из основной таблицы подзадач каждую подзадачу эпика + historyManager.remove(subtaskId); // удаляем подзадачу из истории + taskTimeController.remove(subtasks.get(subtaskId)); subtasks.remove(subtaskId); - historyManager.remove(subtaskId); // удаляем из истории } - historyManager.remove(id); // удаляем из истории + historyManager.remove(id); // удаляем эпик из истории epics.remove(id); } @Override public void clearEpics() { - clearHistoryEpics(); + epics.forEach((id, epic) -> historyManager.remove(id)); epics.clear(); - clearHistorySubTasks(); + subtasks.forEach((id, subTask) -> historyManager.remove(id)); + taskTimeController.removeSubTasks(); subtasks.clear(); } @@ -227,36 +230,19 @@ public void deleteSubTask(int id) { updateEpicStatus(epic); historyManager.remove(id); // удаляем из истории + taskTimeController.remove(subtasks.get(id)); subtasks.remove(id); - taskTimeController.remove(id); } @Override public void clearSubTasks() { - clearHistorySubTasks(); - subtasks.clear(); - for (Epic epic : epics.values()) { - epic.clearSubtasks(); // статус эпика обновляется внутри метода - } - taskTimeController.removeSubTasks(); - } + subtasks.forEach((id, subTask) -> historyManager.remove(id)); - private void clearHistoryTasks() { - for (Integer id : tasks.keySet()) { - historyManager.remove(id); - } - } + epics.forEach((id, epic) -> epic.clearSubtasks()); // статус эпика обновляется внутри метода - private void clearHistoryEpics() { - for (Integer id : epics.keySet()) { - historyManager.remove(id); - } - } + taskTimeController.removeSubTasks(); - private void clearHistorySubTasks() { - for (Integer id : subtasks.keySet()) { - historyManager.remove(id); - } + subtasks.clear(); } public int getIdCount() { From df7a26ef7ca048a79a180f6c5ec49fdf9b7e380e Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 17:33:16 +0300 Subject: [PATCH 10/41] fix: int -> long in setDuration --- src/model/Task.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/model/Task.java b/src/model/Task.java index e53c39e..d3fb344 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -85,6 +85,10 @@ public void setDuration(Duration duration) { this.duration = duration; } + public void setDuration(long durationInMinutes) { + this.duration = Duration.ofMinutes(durationInMinutes); + } + public LocalDateTime getEndTime() { if (startTime == null || duration == null) { return null; From 07828dd9ad311f586dd4afa8f5be96577a362648 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 17:35:01 +0300 Subject: [PATCH 11/41] feat: method updateEpicDurationAndStartTime migrated to TaskTimeController. add set methods for Epics exclusively with javadoc. --- src/model/Epic.java | 88 ++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index 8a4ee3c..af5f7da 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -3,8 +3,10 @@ import util.Status; import util.Type; +import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.List; public class Epic extends Task { @@ -16,47 +18,9 @@ public Epic(String title, String description, Status status) { } public void addSubTask(SubTask subTask) { - - setDurationAndStartTime(subTask); subtaskIds.add(subTask.getTaskId()); } - /** - * Обновляет длительность и время начала эпика на основе добавляемой подзадачи. - * - *

Длительность: Суммируется длительность всех подзадач эпика. - * Если текущая длительность равна {@code null}, устанавливается длительность подзадачи. - * - *

Время начала: Устанавливается самое раннее время начала среди всех подзадач. - * Если время начала подзадачи раньше текущего времени начала эпика или время начала эпика - * не установлено, время начала эпика обновляется. - * - *

Метод игнорирует подзадачи с отсутствующими временными параметрами ({@code null}). - * - * @param subTask подзадача, на основе которой обновляются параметры эпика - * @implNote Метод вызывается при добавлении каждой новой подзадачи к эпику - */ - private void setDurationAndStartTime(SubTask subTask) { - - if (subTask.getDuration() == null) { - return; - } - - duration = duration == null ? subTask.getDuration() : duration.plus(subTask.getDuration()); - - if (subTask.getStartTime() == null) { - return; - } - - LocalDateTime subtaskStartTime = subTask.getStartTime(); - - if (startTime == null || startTime.isAfter(subtaskStartTime)) { - // Обновляем время начала, если оно еще не установлено - // или текущее начало позже начала подзадачи - startTime = subtaskStartTime; - } - } - public void deleteSubTask(int id) { subtaskIds.remove(Integer.valueOf(id)); } @@ -64,16 +28,60 @@ public void deleteSubTask(int id) { public void clearSubtasks() { subtaskIds.clear(); this.setStatus(Status.NEW); + this.duration = null; + this.startTime = null; } - public ArrayList getSubtaskIds() { - return subtaskIds; + public List getSubtaskIds() { + return List.copyOf(subtaskIds); } public Type getType() { return Type.EPIC; } + /** + * Устанавливает или добавляет длительность эпика в минутах. + * + *

Если текущая длительность эпика равна {@code null}, устанавливает длительность + * равной указанному количеству минут. Если длительность уже установлена, добавляет + * указанное количество минут к существующей длительности. + * + * @param durationInMinutes длительность в минутах для добавления к эпику; + */ + public void setEpicDuration(long durationInMinutes) { + this.duration = this.duration == null ? Duration.ofMinutes(durationInMinutes) : this.duration.plusMinutes(durationInMinutes); + } + + /** + * Устанавливает или добавляет длительность эпика. + * + *

Если текущая длительность эпика равна {@code null}, устанавливает длительность + * равной указанной длительности. Если длительность уже установлена, добавляет + * указанную длительность к существующей. + * + * @param duration длительность для добавления к эпику + * @apiNote Предпочтительный метод для работы с временными интервалами + */ + public void setEpicDuration(Duration duration) { + this.duration = this.duration == null ? duration : this.duration.plus(duration); + } + + /** + * Устанавливает или обновляет время начала эпика. + * + *

Устанавливает время начала эпика, если оно еще не установлено ({@code null}). + * Если время начала уже установлено, обновляет его только если новое время начала + * раньше текущего (минимальное время среди всех подзадач). + * + * @param startTime время начала для установки или сравнения; + */ + public void setEpicStartTime(LocalDateTime startTime) { + if (this.startTime == null || this.startTime.isAfter(startTime)) { + this.startTime = startTime; + } + } + @Override public String toString() { return String.format("%s{id=%d, title=%s, description=%s, status=%s, subtasks=%s,\n startTime=[%s], duration=[%s], endTime=[%s]}\n", From 5cac866cea93e816706ee2e8dd93e5a9f9bace5d Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 17:36:58 +0300 Subject: [PATCH 12/41] feat: method updateEpicDurationAndStartTime() migrated from Epic. add method updateEpicDurationAndStartTimeDeletion() to change time and duration when subtask is deleted. add and updated javadoc --- src/util/TaskTimeController.java | 86 ++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 5e176f5..1a970d8 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -1,7 +1,12 @@ package util; +import model.Epic; +import model.SubTask; import model.Task; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.Optional; import java.util.TreeSet; /** @@ -11,7 +16,7 @@ * *

Класс предназначен для использования менеджерами задач для валидации временных промежутков * и обеспечения отсутствия конфликтов в расписании. Игнорирует попытки добавить эпики и задачи - * без установленных временных параметров. + * без установленных временных параметров. Управляет временем начала и длительностью эпиков. */ public class TaskTimeController { @@ -67,6 +72,70 @@ public boolean isTimeOverlappingWithTreeSearch(Task task) { return false; } + /** + * Обновляет временные параметры эпика при добавлении подзадачи. + * + *

Длительность: При помощи метода {@link model.Epic#setEpicDuration(java.time.Duration)} + * суммируется длительность всех подзадач эпика. + * Если текущая длительность равна {@code null}, устанавливается длительность подзадачи. + * + *

Время начала: Устанавливается самое раннее время начала среди всех подзадач при помощи + * {@link Epic#setEpicStartTime(LocalDateTime)}. + * Если время начала подзадачи раньше текущего времени начала эпика или время начала эпика + * не установлено, время начала эпика обновляется. + * + *

Метод игнорирует подзадачи с отсутствующими временными параметрами ({@code null}). + * + * @param epic эпик, параметры которого следует обновить + * @param subTask подзадача, на основе которой обновляются параметры эпика + * @implNote Метод вызывается при добавлении/удалении подзадачи + */ + public void updateEpicDurationAndStartTime(Epic epic, SubTask subTask) { + + if (subTask.getDuration() != null) { + return; + } + + epic.setEpicDuration(subTask.getDuration()); + + if (subTask.getStartTime() == null) { + return; + } + + epic.setEpicStartTime(subTask.getStartTime()); + } + + /** + * Обновляет временные параметры эпика после удаления подзадачи. + * + *

Выполняет следующие операции: + *

    + *
  • Находит подзадачу с самым ранним временем начала среди оставшихся подзадач
  • + *
  • Устанавливает время начала эпика равным времени начала найденной подзадачи
  • + *
  • Если подзадач не осталось, сбрасывает время начала эпика в {@code null}
  • + *
  • Уменьшает общую длительность эпика на длительность удаляемой подзадачи
  • + *
+ * + * @param epic эпик, временные параметры которого следует обновить + * @param subtask удаляемая подзадача; используется для вычитания её длительности + * @implNote Метод должен вызываться непосредственно после удаления подзадачи из эпика и {@code timeSortedTask} + */ + public void updateEpicDurationAndStartTimeDeletion(Epic epic, SubTask subtask) { + + Optional newStartTime = timeSortedTasks.stream() + .filter(task -> task.getType() == Type.SUBTASK) + .map(task -> (SubTask) task) + .min(Comparator.comparing(Task::getStartTime)); + + if (newStartTime.isPresent()) { + epic.setStartTime(newStartTime.get().getStartTime()); + epic.setEpicDuration(-subtask.getDuration().toMinutes()); + } else { + epic.setStartTime(null); + epic.setDuration(null); + } + } + private boolean hasMissingTimeFields(Task task) { return task.getDuration() == null || task.getStartTime() == null; } @@ -90,6 +159,17 @@ public void remove(Task task) { timeSortedTasks.remove(task); } + /** + * Удаляет задачу или подзадачу из отсортированной коллекции по идентификатору. + * + *

Метод выполняет поиск элемента с указанным идентификатором и удаляет его + * из внутренней отсортированной коллекции временных интервалов. + * + *

Не рекомендуется для общего использования - метод следует использовать только + * если нет доступа к объекту задачи и известен только {@code id}. + * + * @param id идентификатор задачи для удаления + */ public void remove(int id) { timeSortedTasks.removeIf(element -> element.getTaskId() == id); } @@ -97,7 +177,7 @@ public void remove(int id) { /** * Удаляет все задачи типа {@link Type#TASK} из коллекции. * - * @apiNote Используется для выборочной очистки только обычных задач + * @apiNote Используется для выборочной очистки обычных задач */ public void removeTasks() { timeSortedTasks.removeIf(element -> element.getType() == Type.TASK); @@ -106,7 +186,7 @@ public void removeTasks() { /** * Удаляет все задачи типа {@link Type#SUBTASK} из коллекции. * - * @apiNote Используется для выборочной очистки только подзадач + * @apiNote Используется для выборочной очистки подзадач */ public void removeSubTasks() { timeSortedTasks.removeIf(element -> element.getType() == Type.SUBTASK); From c2355c0d573dc0b8256d9ea4ebaa1a211c0e3c25 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 17:39:42 +0300 Subject: [PATCH 13/41] feat: add support for updating epic`s time/duration upon addition or deletion of subtask. refactor: add comments and javadoc for complex delete methods --- src/managers/InMemoryTaskManager.java | 73 ++++++++++++++++++++------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 63e55a8..63cb2c3 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -11,8 +11,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.stream.Collector; -import java.util.stream.Collectors; public class InMemoryTaskManager implements TaskManager { private int idCount; @@ -50,6 +48,7 @@ public void addSubTask(SubTask subTask) throws TaskTimeOverlapException { epic.addSubTask(subTask); updateEpicStatus(epic); + taskTimeController.updateEpicDurationAndStartTime(epic, subTask); taskTimeController.add(subTask); } @@ -177,7 +176,7 @@ public void deleteTask(int id) { if (!tasks.containsKey(id)) { return; } - historyManager.remove(id); // удаляем из истории + historyManager.remove(id); taskTimeController.remove(tasks.get(id)); // удаляем объект т.к это быстрее, чем удаление по id tasks.remove(id); } @@ -189,22 +188,42 @@ public void clearTasks() { taskTimeController.removeTasks(); } + /** + * Удаляет эпик по идентификатору вместе со всеми его подзадачами. + * + *

Выполняет следующие операции при удалении эпика: + *

    + *
  1. Для каждой подзадачи эпика: + *
      + *
    • Удаляет подзадачу из истории просмотров
    • + *
    • Удаляет подзадачу из контроллера временных промежутков
    • + *
    • Удаляет подзадачу из основной таблицы подзадач
    • + *
    + *
  2. + *
  3. Удаляет эпик из истории просмотров
  4. + *
  5. Удаляет эпик из таблицы эпиков
  6. + *
+ * + *

Если эпик с указанным идентификатором не существует, метод завершается + * без выполнения каких-либо операций. + * + * @param epicId идентификатор эпика для удаления; должен быть положительным числом + */ @Override - public void deleteEpic(int id) { - if (!epics.containsKey(id)) { + public void deleteEpic(int epicId) { + if (!epics.containsKey(epicId)) { return; } - Epic epic = epics.get(id); + Epic epic = epics.get(epicId); - for (Integer subtaskId : epic.getSubtaskIds()) { // удаляем из основной таблицы подзадач каждую подзадачу эпика - historyManager.remove(subtaskId); // удаляем подзадачу из истории + for (Integer subtaskId : epic.getSubtaskIds()) { + historyManager.remove(subtaskId); taskTimeController.remove(subtasks.get(subtaskId)); subtasks.remove(subtaskId); } - historyManager.remove(id); // удаляем эпик из истории - epics.remove(id); - + historyManager.remove(epicId); + epics.remove(epicId); } @Override @@ -217,20 +236,39 @@ public void clearEpics() { subtasks.clear(); } + /** + * Удаляет подзадачу по идентификатору и выполняет связанные обновления. + * + *

Выполняет следующие операции при удалении подзадачи: + *

    + *
  1. Удаляет подзадачу из списка подзадач эпика
  2. + *
  3. Обновляет статус эпика с учетом оставшихся подзадач
  4. + *
  5. Удаляет подзадачу из истории просмотров
  6. + *
  7. Удаляет подзадачу из контроллера временных интервалов
  8. + *
  9. Обновляет временные параметры эпика (длительность и время начала)
  10. + *
  11. Удаляет подзадачу из основной таблицы подзадач
  12. + *
+ * + *

Если подзадача с указанным идентификатором не существует, метод завершается + * без выполнения каких-либо операций. + * + * @param id идентификатор подзадачи для удаления + */ @Override public void deleteSubTask(int id) { if (!subtasks.containsKey(id)) { return; } - - int epicParentId = subtasks.get(id).getEpicId(); - // удаляем подзадачу из листа эпика, пересчитываем статус + SubTask subTask = subtasks.get(id); + int epicParentId = subTask.getEpicId(); Epic epic = epics.get(epicParentId); + epic.deleteSubTask(id); updateEpicStatus(epic); - historyManager.remove(id); // удаляем из истории - taskTimeController.remove(subtasks.get(id)); + historyManager.remove(id); + taskTimeController.remove(subTask); + taskTimeController.updateEpicDurationAndStartTimeDeletion(epic, subTask); subtasks.remove(id); } @@ -238,7 +276,8 @@ public void deleteSubTask(int id) { public void clearSubTasks() { subtasks.forEach((id, subTask) -> historyManager.remove(id)); - epics.forEach((id, epic) -> epic.clearSubtasks()); // статус эпика обновляется внутри метода + // статус и временные параметры эпика обновляется внутри метода clearSubtasks() + epics.forEach((id, epic) -> epic.clearSubtasks()); taskTimeController.removeSubTasks(); From 574f0ba5cca5837bda041bad72c55b1bf76ce4ef Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 18:38:58 +0300 Subject: [PATCH 14/41] docs: javadoc add for complex methods --- .../history/InMemoryHistoryManager.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/managers/history/InMemoryHistoryManager.java b/src/managers/history/InMemoryHistoryManager.java index 6f8d3bb..52920b1 100644 --- a/src/managers/history/InMemoryHistoryManager.java +++ b/src/managers/history/InMemoryHistoryManager.java @@ -6,6 +6,17 @@ import java.util.HashMap; import java.util.List; +/** + * Менеджер истории просмотров задач, реализованный на двусвязном списке. + * + *

Обеспечивает хранение истории просмотров задач с соблюдением порядка просмотра + * и быстрым доступом к элементам через хэш-таблицу. + * + *

Реализация использует комбинацию HashMap для быстрого поиска узлов + * и двусвязного списка для поддержания порядка элементов. + * + * @apiNote Все операции выполняются за время O(1), кроме {@link #getHistory()} - O(n) + */ public class InMemoryHistoryManager implements HistoryManager { private Node head; private Node tail; @@ -37,6 +48,14 @@ public List getHistory() { return getTasks(); } + /** + * Добавляет задачу в конец двусвязного списка. + * + *

Создает новый узел для задачи и добавляет его в хвост списка. + * + * @param task задача для добавления в список + * @return созданный узел, содержащий задачу + */ private Node linkLast(Task task) { Node newNode = new Node(tail, task, null); @@ -52,6 +71,17 @@ private Node linkLast(Task task) { return newNode; } + /** + * Удаляет узел из двусвязного списка. + *

Обрабатывает все возможные случаи: + *

    + *
  • Удаление головного узла
  • + *
  • Удаление хвостового узла
  • + *
  • Удаление узла из середины списка
  • + *
+ * + * @param node узел для удаления + */ private void removeNode(Node node) { Node next = node.getNext(); Node prev = node.getPrev(); @@ -74,11 +104,19 @@ private void removeNode(Node node) { } + /** + * Возвращает список задач в порядке их просмотра. + * + *

Выполняет обход двусвязного списка от головы к хвосту и собирает все задачи + * в список. Порядок элементов соответствует порядку просмотра (от старых к новым). + * + * @return неизменяемый список задач в порядке просмотра + * @apiNote Возвращаемый список является копией, изменения не влияют на внутреннее состояние + */ private List getTasks() { List tasks = new ArrayList<>(); Node current = head; // копия головы, чтобы избежать потери - // проходим через всю последовательность и добавляем задачу в список while (current != null) { tasks.add(current.getTask()); current = current.getNext(); From 0ca2d29ce3179f88e63862b638f513868052f8ce Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 18:52:39 +0300 Subject: [PATCH 15/41] docs: javadoc add for classes --- src/managers/InMemoryTaskManager.java | 20 +++++++++++++++++++ src/managers/TaskManager.java | 10 +++++++--- src/managers/history/HistoryManager.java | 5 +++++ .../history/InMemoryHistoryManager.java | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 63cb2c3..bb25dc8 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -12,6 +12,26 @@ import java.util.HashMap; import java.util.List; +/** + * Реализация менеджера задач, хранящая данные в оперативной памяти. + * + *

Обеспечивает полный набор операций для управления задачами, эпиками и подзадачами: + *

    + *
  • Добавление, обновление и удаление задач
  • + *
  • Автоматическое управление статусами эпиков
  • + *
  • Контроль временных пересечений задач
  • + *
  • Ведение истории просмотров задач
  • + *
  • Поддержка списка задач в порядке приоритета
  • + *
+ * + *

Использует следующие компоненты: + *

    + *
  • {@link HistoryManager} для ведения истории просмотров
  • + *
  • {@link TaskTimeController} для контроля временных интервалов
  • + *
+ * + * @implSpec Все операции работают с данными в оперативной памяти + */ public class InMemoryTaskManager implements TaskManager { private int idCount; private final HashMap tasks = new HashMap<>(); diff --git a/src/managers/TaskManager.java b/src/managers/TaskManager.java index 01f0707..2cabed6 100644 --- a/src/managers/TaskManager.java +++ b/src/managers/TaskManager.java @@ -1,12 +1,16 @@ package managers; +import java.util.List; + import model.Epic; import model.SubTask; import model.Task; -import java.util.ArrayList; -import java.util.List; - +/** + * Интерфейс менеджера задач для управления задачами, эпиками и подзадачами. + * + * @apiNote Реализации могут добавлять дополнительные проверки (например, временные пересечения) + */ public interface TaskManager { void addTask(Task task); diff --git a/src/managers/history/HistoryManager.java b/src/managers/history/HistoryManager.java index dacd43a..7b5b4a5 100644 --- a/src/managers/history/HistoryManager.java +++ b/src/managers/history/HistoryManager.java @@ -4,6 +4,11 @@ import java.util.List; +/** + * Интерфейс менеджера истории просмотров задач. + * + * @implSpec Реализации должны обеспечивать эффективное удаление из середины списка + */ public interface HistoryManager { void addTask(Task task); diff --git a/src/managers/history/InMemoryHistoryManager.java b/src/managers/history/InMemoryHistoryManager.java index 52920b1..a737c46 100644 --- a/src/managers/history/InMemoryHistoryManager.java +++ b/src/managers/history/InMemoryHistoryManager.java @@ -8,6 +8,7 @@ /** * Менеджер истории просмотров задач, реализованный на двусвязном списке. + * Данные хранятся в оперативной памяти. * *

Обеспечивает хранение истории просмотров задач с соблюдением порядка просмотра * и быстрым доступом к элементам через хэш-таблицу. From 3ae205b04cd9e00c4663df08be9f7a7692452819 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 18:53:04 +0300 Subject: [PATCH 16/41] fix: simplified compareTo() logic --- src/model/Task.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/model/Task.java b/src/model/Task.java index d3fb344..7c89c7c 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -132,14 +132,6 @@ public boolean equals(Object obj) { @Override public int compareTo(Task t) { - LocalDateTime tStartTime = t.getStartTime(); - - if (this.startTime.isAfter(tStartTime)) { - return 1; - } else if (this.startTime.equals(tStartTime)) { - return 0; - } else { - return -1; - } + return this.startTime.compareTo(t.getStartTime()); } } From d36b12ce2c2f926a68733580705a7115a17a8f2b Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 20:57:35 +0300 Subject: [PATCH 17/41] feat: new constructors --- src/model/SubTask.java | 20 ++++++++++++++++++-- src/model/Task.java | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/model/SubTask.java b/src/model/SubTask.java index f6e1c89..f97e145 100644 --- a/src/model/SubTask.java +++ b/src/model/SubTask.java @@ -3,6 +3,7 @@ import util.Status; import util.Type; +import java.time.Duration; import java.time.LocalDateTime; public class SubTask extends Task { @@ -17,17 +18,32 @@ public SubTask(String title, String description, Status status, int epicId, - int durationInMinutes, + long durationInMinutes, LocalDateTime startTime) { super(title, description, status, durationInMinutes, startTime); this.epicId = epicId; } - public SubTask(String title, String description, Status status, int epicId, int durationInMinutes) { + public SubTask(String title, String description, Status status, int epicId, long durationInMinutes) { super(title, description, status, durationInMinutes); this.epicId = epicId; } + public SubTask(String title, + String description, + Status status, + int epicId, + Duration duration, + LocalDateTime startTime) { + super(title, description, status, duration, startTime); + this.epicId = epicId; + } + + public SubTask(String title, String description, Status status, int epicId, Duration duration) { + super(title, description, status, duration); + this.epicId = epicId; + } + public int getEpicId() { return epicId; } diff --git a/src/model/Task.java b/src/model/Task.java index 7c89c7c..84bfc59 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -22,7 +22,7 @@ public Task(String title, String description, Status status) { this.status = status; } - public Task(String title, String description, Status status, int durationInMinutes, LocalDateTime startTime) { + public Task(String title, String description, Status status, long durationInMinutes, LocalDateTime startTime) { this.title = title; this.description = description; this.status = status; @@ -30,13 +30,28 @@ public Task(String title, String description, Status status, int durationInMinut this.startTime = startTime; } - public Task(String title, String description, Status status, int durationInMinutes) { + public Task(String title, String description, Status status, long durationInMinutes) { this.title = title; this.description = description; this.status = status; this.duration = Duration.ofMinutes(durationInMinutes); } + public Task(String title, String description, Status status, Duration duration, LocalDateTime startTime) { + this.title = title; + this.description = description; + this.status = status; + this.duration = duration; + this.startTime = startTime; + } + + public Task(String title, String description, Status status, Duration duration) { + this.title = title; + this.description = description; + this.status = status; + this.duration = duration; + } + public int getTaskId() { return taskId; } From 79c574cc93603d0343a890da92b2202b304de48e Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 21:00:41 +0300 Subject: [PATCH 18/41] feat: special field endTime add. controls Epic`s endTime throw set methods, one special, one overwritten --- src/model/Epic.java | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index af5f7da..c5eb775 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -11,6 +11,7 @@ public class Epic extends Task { private final ArrayList subtaskIds; + private LocalDateTime endTime; public Epic(String title, String description, Status status) { super(title, description, status); @@ -82,6 +83,30 @@ public void setEpicStartTime(LocalDateTime startTime) { } } + /** + * Устанавливает или обновляет время конца эпика. + * + *

Устанавливает время конца эпика, если оно еще не установлено ({@code null}). + * Если время конца уже установлено, обновляет его только если новое время конца + * позже текущего (максимальное время среди всех подзадач). + * + * @param endTime время начала для установки или сравнения; + */ + public void setEpicEndTime(LocalDateTime endTime) { + if (this.endTime == null || this.endTime.isBefore(endTime)) { + this.endTime = endTime; + } + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + @Override + public LocalDateTime getEndTime() { + return endTime; + } + @Override public String toString() { return String.format("%s{id=%d, title=%s, description=%s, status=%s, subtasks=%s,\n startTime=[%s], duration=[%s], endTime=[%s]}\n", @@ -93,7 +118,7 @@ public String toString() { this.subtaskIds, formatDateTime(startTime), duration, - formatDateTime(getEndTime()) + formatDateTime(this.getEndTime()) ); } From f2c28c3dcd0a9fe385f43455c353669568a15d5d Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 21:01:23 +0300 Subject: [PATCH 19/41] refactor: name change --- src/managers/InMemoryTaskManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index bb25dc8..cd598f4 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -68,7 +68,7 @@ public void addSubTask(SubTask subTask) throws TaskTimeOverlapException { epic.addSubTask(subTask); updateEpicStatus(epic); - taskTimeController.updateEpicDurationAndStartTime(epic, subTask); + taskTimeController.updateEpicTimeParams(epic, subTask); taskTimeController.add(subTask); } @@ -288,7 +288,7 @@ public void deleteSubTask(int id) { historyManager.remove(id); taskTimeController.remove(subTask); - taskTimeController.updateEpicDurationAndStartTimeDeletion(epic, subTask); + taskTimeController.updateEpicTimeParamsDeletion(epic, subTask); subtasks.remove(id); } From c4af48b2d11ff4c6c68a56faa99d2b3c7fc59417 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 21:10:05 +0300 Subject: [PATCH 20/41] docs: updated docs --- src/model/Epic.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index c5eb775..4124970 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -55,7 +55,7 @@ public void setEpicDuration(long durationInMinutes) { } /** - * Устанавливает или добавляет длительность эпика. + * Устанавливает длительность эпика, добавляет к ней или вычитает из неё. * *

Если текущая длительность эпика равна {@code null}, устанавливает длительность * равной указанной длительности. Если длительность уже установлена, добавляет From 22cae65d55a133abb664ba7cbc92297c1e825e2c Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 21:12:28 +0300 Subject: [PATCH 21/41] fix: changed logic in updateTimeParams methods docs: updated docs --- src/util/TaskTimeController.java | 79 ++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 1a970d8..d15acec 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -4,10 +4,13 @@ import model.SubTask; import model.Task; +import java.time.Duration; import java.time.LocalDateTime; import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.TreeSet; +import java.util.stream.Collectors; /** * Контроллер для управления временными интервалами задач. @@ -75,65 +78,73 @@ public boolean isTimeOverlappingWithTreeSearch(Task task) { /** * Обновляет временные параметры эпика при добавлении подзадачи. * - *

Длительность: При помощи метода {@link model.Epic#setEpicDuration(java.time.Duration)} - * суммируется длительность всех подзадач эпика. - * Если текущая длительность равна {@code null}, устанавливается длительность подзадачи. - * - *

Время начала: Устанавливается самое раннее время начала среди всех подзадач при помощи - * {@link Epic#setEpicStartTime(LocalDateTime)}. - * Если время начала подзадачи раньше текущего времени начала эпика или время начала эпика - * не установлено, время начала эпика обновляется. - * + *

Выполняет следующие операции: + *

    + *
  • Длительность: Добавляет длительность подзадачи к общей длительности эпика + * с помощью {@link Epic#setEpicDuration(java.time.Duration)}. Если текущая длительность + * равна {@code null}, устанавливается длительность подзадачи.
  • + *
  • Время начала: Устанавливает время начала эпика равным времени начала подзадачи + * с помощью {@link Epic#setEpicStartTime(LocalDateTime)}, если оно раньше текущего + * или если время начала эпика не установлено.
  • + *
  • Время окончания: Устанавливает время окончания эпика равным времени окончания подзадачи + * с помощью {@link Epic#setEpicEndTime(LocalDateTime)}, если оно позже текущего + * или если время окончания эпика не установлено.
  • + *
*

Метод игнорирует подзадачи с отсутствующими временными параметрами ({@code null}). * * @param epic эпик, параметры которого следует обновить * @param subTask подзадача, на основе которой обновляются параметры эпика - * @implNote Метод вызывается при добавлении/удалении подзадачи + * @implNote Метод вызывается при добавлении подзадачи */ - public void updateEpicDurationAndStartTime(Epic epic, SubTask subTask) { - - if (subTask.getDuration() != null) { + public void updateEpicTimeParams(Epic epic, SubTask subTask) { + if (subTask.getDuration() == null || subTask.getStartTime() == null) { return; } epic.setEpicDuration(subTask.getDuration()); - - if (subTask.getStartTime() == null) { - return; - } - epic.setEpicStartTime(subTask.getStartTime()); + epic.setEpicEndTime(subTask.getEndTime()); } /** - * Обновляет временные параметры эпика после удаления подзадачи. + * Обновляет временные параметры эпика при удалении подзадачи. * *

Выполняет следующие операции: *

    - *
  • Находит подзадачу с самым ранним временем начала среди оставшихся подзадач
  • - *
  • Устанавливает время начала эпика равным времени начала найденной подзадачи
  • - *
  • Если подзадач не осталось, сбрасывает время начала эпика в {@code null}
  • - *
  • Уменьшает общую длительность эпика на длительность удаляемой подзадачи
  • + *
  • Длительность: Уменьшает общую длительность эпика на длительность удаляемой подзадачи + * с помощью {@link Epic#setEpicDuration(java.time.Duration)}.
  • + *
  • Время начала: Находит самое раннее время начала среди оставшихся подзадач + * и устанавливает его как время начала эпика.
  • + *
  • Время окончания: Находит самое позднее время окончания среди оставшихся подзадач + * и устанавливает его как время окончания эпика.
  • + *
  • Очистка параметров: Если подзадач не осталось, сбрасывает все временные параметры в {@code null}.
  • *
* - * @param epic эпик, временные параметры которого следует обновить - * @param subtask удаляемая подзадача; используется для вычитания её длительности - * @implNote Метод должен вызываться непосредственно после удаления подзадачи из эпика и {@code timeSortedTask} + * @param epic эпик, параметры которого следует обновить + * @param durationToSubtract длительность, которую следует вычесть + * @implSpec Метод вызывается после удаления подзадачи из эпика + * @apiNote Метод пересчитывает параметры на основе всех оставшихся подзадач */ - public void updateEpicDurationAndStartTimeDeletion(Epic epic, SubTask subtask) { + public void updateEpicTimeParamsDeletion(Epic epic, Duration durationToSubtract) { + + epic.setEpicDuration(-durationToSubtract.toMinutes()); // в любом случае удаляем - Optional newStartTime = timeSortedTasks.stream() + List subtasks = timeSortedTasks.stream() // получаем список подзадач .filter(task -> task.getType() == Type.SUBTASK) - .map(task -> (SubTask) task) - .min(Comparator.comparing(Task::getStartTime)); + .map(task -> (SubTask) task).toList(); - if (newStartTime.isPresent()) { - epic.setStartTime(newStartTime.get().getStartTime()); - epic.setEpicDuration(-subtask.getDuration().toMinutes()); - } else { + if (subtasks.isEmpty()) { epic.setStartTime(null); epic.setDuration(null); + epic.setEndTime(null); + return; } + + Optional newEndTime = subtasks.stream().max(Comparator.comparing(Task::getEndTime)); + epic.setEndTime(newEndTime.get().getEndTime()); + + Optional newStartTime = subtasks.stream().min(Comparator.comparing(Task::getStartTime)); + epic.setStartTime(newStartTime.get().getStartTime()); } private boolean hasMissingTimeFields(Task task) { From 75fd792336969161dc40ec857647bfb3f7763147 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 21:12:51 +0300 Subject: [PATCH 22/41] fix: changed param due to new method --- src/managers/InMemoryTaskManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index cd598f4..4eeafb2 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -288,7 +288,7 @@ public void deleteSubTask(int id) { historyManager.remove(id); taskTimeController.remove(subTask); - taskTimeController.updateEpicTimeParamsDeletion(epic, subTask); + taskTimeController.updateEpicTimeParamsDeletion(epic, subTask.getDuration()); subtasks.remove(id); } From 7453ee49556976831ab15787e8294db367c5af99 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 21:14:29 +0300 Subject: [PATCH 23/41] fix: TaskTimeOverlapException message clearer --- src/managers/InMemoryTaskManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 4eeafb2..43603ab 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -43,7 +43,7 @@ public class InMemoryTaskManager implements TaskManager { @Override public void addTask(Task task) throws TaskTimeOverlapException { if (taskTimeController.isTimeOverlapping(task)) { - throw new TaskTimeOverlapException("Can`t add task: " + task); + throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + task); } task.setTaskId(++idCount); tasks.put(task.getTaskId(), task); @@ -59,7 +59,7 @@ public void addEpic(Epic epic) { @Override public void addSubTask(SubTask subTask) throws TaskTimeOverlapException { if (taskTimeController.isTimeOverlapping(subTask)) { - throw new TaskTimeOverlapException("Can`t add task: " + subTask); + throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + subTask); } subTask.setTaskId(++idCount); subtasks.put(subTask.getTaskId(), subTask); From df59b0af2b70c25b1e756506b66a7d9fe3f74267 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 22:08:35 +0300 Subject: [PATCH 24/41] refactor/feat: FileBackedTaskManager() add exception with new exception ManagerLoadException handling and made code easier to read with help of ParserHelper add new fields handling to FileBackedTaskManager feat: ParserHelper - helper class to parse different Objects. docs: add javadoc and comments to complex methods --- resources/tasks.csv | 8 +- .../filedbacked/FileBackedTaskManager.java | 277 ++++++++++++++++++ src/managers/filedbacked/ParserHelper.java | 102 +++++++ src/util/exceptions/ManagerLoadException.java | 16 + test/managers/FileBackedTaskManagerTest.java | 1 + 5 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 src/managers/filedbacked/FileBackedTaskManager.java create mode 100644 src/managers/filedbacked/ParserHelper.java create mode 100644 src/util/exceptions/ManagerLoadException.java diff --git a/resources/tasks.csv b/resources/tasks.csv index 5cdfc28..964d1b1 100644 --- a/resources/tasks.csv +++ b/resources/tasks.csv @@ -1 +1,7 @@ -id,type,name,status,description,epic +id,type,name,status,description,epic,duration,startTime +1,TASK,task1,NEW,demo,null,PT9M,2025-09-03 20:34:23.587 +2,TASK,task2,NEW,demo,null,PT10M,null +3,EPIC,epic,NEW,demo,null,PT30M,2025-09-03 20:44:23.590 +4,SUBTASK,subtask1,NEW,demo,3,PT10M,2025-09-03 20:44:23.590 +5,SUBTASK,subtask2,NEW,demo,3,PT10M,2025-09-03 20:54:23.590 +6,SUBTASK,subtask3,NEW,demo,3,PT10M,2025-09-03 21:04:23.590 diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java new file mode 100644 index 0000000..1f7ee01 --- /dev/null +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -0,0 +1,277 @@ +package managers.filedbacked; + +import managers.InMemoryTaskManager; +import model.Epic; +import model.SubTask; +import model.Task; +import util.Status; +import util.Type; +import util.exceptions.ManagerLoadException; +import util.exceptions.ManagerSaveException; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static managers.filedbacked.ParserHelper.*; +import static util.Type.*; + +/** + * Менеджер задач с сохранением состояния в файл типа csv. + * + *

Расширяет функциональность {@link InMemoryTaskManager}, добавляя возможность + * сохранения и восстановления состояния задач из файла. Автоматически сохраняет + * изменения после каждой операции модификации данных. + * + *

Формат файла данных: + *

    + *
  • Первая строка: заголовок с названиями полей
  • + *
  • Последующие строки: данные задач в CSV-формате
  • + *
  • Кодировка: UTF-8
  • + *
+ * + *

Поддерживаемые операции: + *

    + *
  • Автоматическое сохранение при изменении данных
  • + *
  • Восстановление состояния из файла при запуске
  • + *
  • Создание нового файла если он не существует
  • + *
  • Обработка ошибок ввода-вывода через {@link ManagerSaveException}
  • + *
+ * + * @implSpec Все операции модификации данных автоматически вызывают сохранение + * @see InMemoryTaskManager + * @see ManagerSaveException + * @see ParserHelper + */ +public class FileBackedTaskManager extends InMemoryTaskManager { + + private final File filename; + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + public FileBackedTaskManager(File filename) { + this.filename = filename; + } + + /** + * Восстанавливает состояние менеджера задач из файла. + * + *

Если файл не существует, возвращает пустой менеджер с возможностью + * последующего сохранения данных в указанный файл. + * + *

Процесс восстановления: + *

    + *
  1. Проверяет существование файла
  2. + *
  3. Парсит каждую строку с данными задачи
  4. + *
  5. Восстанавливает задачи, эпики и подзадачи
  6. + *
  7. Устанавливает корректный счетчик идентификаторов
  8. + *
+ * + * @param filename файл для восстановления данных; должен быть валидным файлом + * @return восстановленный менеджер задач или новый пустой менеджер + * @throws ManagerLoadException если файл содержит некорректные данные + * @apiNote Автоматически восстанавливает корректный счетчик идентификаторов + * @implNote Использует {@link ParserHelper} для парсинга данных + */ + public static FileBackedTaskManager loadFromFile(File filename) throws ManagerLoadException { + + if (!filename.exists()) { + /* Если файла не существует, то возвращается пустой менеджер + * с возможностью создать файл и записывать в него*/ + return new FileBackedTaskManager(filename); + } + + FileBackedTaskManager manager = new FileBackedTaskManager(filename); + + try (BufferedReader reader = new BufferedReader(new FileReader(filename, StandardCharsets.UTF_8))) { + + String header = reader.readLine(); + validateHeader(header); + + int maxId = 0; // будет присвоен счетчику менеджера + + while (reader.ready()) { + String line = reader.readLine(); + if (line.isBlank()) { + continue; + } + String[] parts = line.split(","); + + int id = parseInteger(parts[0]); + Type type = parseType(parts[1]); + String title = parts[2]; + Status status = parseStatus(parts[3]); + String description = parts[4]; + int epicId = parseOptionalInteger(parts[5]); + Duration duration = parseOptionalDuration(parts[6]); + LocalDateTime startTime = parseOptionalDateTime(parts[7], formatter); + + maxId = Math.max(maxId, id); + manager.setIdCount(id - 1); // менеджер сам присвоит id, устанавливаем счетчик на предыдущий + + if (type == TASK) { + manager.addTask(new Task(title, description, status, duration, startTime)); + } else if (type == EPIC) { + manager.addEpic(new Epic(title, description, status)); + } else if (type == SUBTASK) { + manager.addSubTask(new SubTask(title, description, status, epicId, duration, startTime)); + } else { + System.out.println("Такой формат не существует"); + } + } + + manager.setIdCount(maxId); + + } catch (IOException e) { + e.printStackTrace(); + } + + return manager; + } + + /** + * Сохраняет текущее состояние менеджера в файл. + * + *

Формат сохранения: + *

    + *
  1. Записывает заголовок с названиями полей
  2. + *
  3. Записывает все задачи в CSV формате
  4. + *
  5. Записывает все эпики в CSV формате
  6. + *
  7. Записывает все подзадачи в CSV формате
  8. + *
+ * + * @throws ManagerSaveException если произошла ошибка записи в файл + * @implNote Метод вызывается автоматически после каждой операции модификации + * @see #writeTasks(BufferedWriter, List) + * @see #writeSubTasks(BufferedWriter, List) + */ + private void save() { + + try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename, StandardCharsets.UTF_8))) { + bw.write("id,type,name,status,description,epic,duration,startTime"); //первая служебная строка файла + bw.newLine(); + + writeTasks(bw, super.getTasks()); // записываются таски + writeTasks(bw, super.getEpics()); // записываются эпики + writeSubTasks(bw, super.getSubTasks()); // записываются подзадачи + + } catch (IOException e) { + throw new ManagerSaveException("Ошибка сохранения файла", e); + } + } + + private void writeTasks(BufferedWriter bw, List tasks) throws IOException { + + for (Task task : tasks) { + String startTime = task.getStartTime() == null ? "null" : task.getStartTime().format(formatter); + + String line = String.format("%s,%s,%s,%s,%s,null,%s,%s", + task.getTaskId(), + task.getType(), + task.getTitle(), + task.getStatus(), + task.getDescription(), + task.getDuration(), + startTime + ); + + bw.write(line); + bw.newLine(); + } + } + + private void writeSubTasks(BufferedWriter bw, List subTasks) throws IOException { + + for (SubTask subTask : subTasks) { + String startTime = subTask.getStartTime() == null ? "null" : subTask.getStartTime().format(formatter); + + String line = String.format("%s,%s,%s,%s,%s,%s,%s,%s", + subTask.getTaskId(), + subTask.getType(), + subTask.getTitle(), + subTask.getStatus(), + subTask.getDescription(), + subTask.getEpicId(), + subTask.getDuration(), + startTime + ); + + bw.write(line); + bw.newLine(); + } + } + + @Override + public void addTask(Task task) { + super.addTask(task); + save(); + } + + @Override + public void addEpic(Epic epic) { + super.addEpic(epic); + save(); + } + + @Override + public void addSubTask(SubTask subTask) { + super.addSubTask(subTask); + save(); + } + + @Override + public void updateTask(Task task) { + super.updateTask(task); + save(); + } + + @Override + public void updateEpic(Epic epic) { + super.updateTask(epic); + save(); + } + + @Override + public void updateSubTask(SubTask subTask) { + super.updateSubTask(subTask); + save(); + } + + @Override + public void deleteTask(int id) { + super.deleteTask(id); + save(); + } + + @Override + public void deleteEpic(int id) { + super.deleteEpic(id); + save(); + } + + @Override + public void deleteSubTask(int id) { + super.deleteSubTask(id); + save(); + } + + @Override + public void clearTasks() { + super.clearTasks(); + save(); + } + + @Override + public void clearEpics() { + super.clearEpics(); + save(); + } + + @Override + public void clearSubTasks() { + super.clearSubTasks(); + save(); + } +} diff --git a/src/managers/filedbacked/ParserHelper.java b/src/managers/filedbacked/ParserHelper.java new file mode 100644 index 0000000..9347add --- /dev/null +++ b/src/managers/filedbacked/ParserHelper.java @@ -0,0 +1,102 @@ +package managers.filedbacked; + +import util.Status; +import util.Type; +import util.exceptions.ManagerLoadException; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Вспомогательный класс для парсинга данных задач из файла. + * + *

Предоставляет статические методы для валидации и преобразования + * строковых значений в соответствующие типы данных: + *

    + *
  • Парсинг целых чисел и опциональных целых чисел
  • + *
  • Парсинг типов задач {@link Type}
  • + *
  • Парсинг статусов задач {@link Status}
  • + *
  • Парсинг длительности {@link Duration}
  • + *
  • Парсинг даты и времени {@link LocalDateTime}
  • + *
+ * + *

Все методы выбрасывают {@link ManagerLoadException} при ошибках парсинга. + * + * @implSpec Все методы статические + * @see FileBackedTaskManager + * @see ManagerLoadException + */ +public class ParserHelper { + + protected static void validateHeader(String header) { + if (header == null || !header.startsWith("id,type,name,status,description,epic,duration,startTime")) { + throw new ManagerLoadException("Invalid file format or missing header"); + } + } + + protected static int parseInteger(String value) throws ManagerLoadException { + if (value == null || value.isBlank()) { + throw new ManagerLoadException("Line cannot be empty"); + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + throw new ManagerLoadException("Invalid id: " + value); + } + } + + protected static int parseOptionalInteger(String value) { + if (value == null || value.equals("null") || value.trim().isEmpty()) { + return 0; + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + throw new ManagerLoadException("Invalid optional id: " + value); + } + } + + protected static Type parseType(String value) throws ManagerLoadException { + try { + return Type.valueOf(value.trim()); + } catch (IllegalArgumentException e) { + throw new ManagerLoadException("Invalid task type: " + value); + } + } + + protected static Status parseStatus(String value) throws ManagerLoadException { + try { + return Status.valueOf(value.trim()); + } catch (IllegalArgumentException e) { + throw new ManagerLoadException("Invalid status: " + value); + } + } + + protected static Duration parseOptionalDuration(String value) throws ManagerLoadException { + if (value == null || value.equals("null") || value.trim().isEmpty()) { + return null; + } + + try { + return Duration.parse(value.trim()); + } catch (Exception e) { + throw new ManagerLoadException("Invalid duration format: " + value); + } + } + + protected static LocalDateTime parseOptionalDateTime(String value, DateTimeFormatter formatter) throws ManagerLoadException { + if (value == null || value.equals("null") || value.trim().isEmpty()) { + return null; + } + + try { + return LocalDateTime.parse(value.trim(), formatter); + } catch (DateTimeParseException e) { + throw new ManagerLoadException("Invalid date format: " + value, e); + } + } +} diff --git a/src/util/exceptions/ManagerLoadException.java b/src/util/exceptions/ManagerLoadException.java new file mode 100644 index 0000000..205a409 --- /dev/null +++ b/src/util/exceptions/ManagerLoadException.java @@ -0,0 +1,16 @@ +package util.exceptions; + +public class ManagerLoadException extends ManagerSaveException { + + public ManagerLoadException() { + super(); + } + + public ManagerLoadException(String message) { + super(message); + } + + public ManagerLoadException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/test/managers/FileBackedTaskManagerTest.java b/test/managers/FileBackedTaskManagerTest.java index 71bcea2..48127ae 100644 --- a/test/managers/FileBackedTaskManagerTest.java +++ b/test/managers/FileBackedTaskManagerTest.java @@ -1,5 +1,6 @@ package managers; +import managers.filedbacked.FileBackedTaskManager; import util.exceptions.ManagerSaveException; import model.Epic; import util.Status; From e0a563a983eba7748350947eeadfa386667a2039 Mon Sep 17 00:00:00 2001 From: Crodi Date: Wed, 3 Sep 2025 22:13:20 +0300 Subject: [PATCH 25/41] fix: add check for nulls and changed param for method --- src/managers/InMemoryTaskManager.java | 2 +- src/util/TaskTimeController.java | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 43603ab..2cdff2c 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -288,7 +288,7 @@ public void deleteSubTask(int id) { historyManager.remove(id); taskTimeController.remove(subTask); - taskTimeController.updateEpicTimeParamsDeletion(epic, subTask.getDuration()); + taskTimeController.updateEpicTimeParamsDeletion(epic, subTask); subtasks.remove(id); } diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index d15acec..0f9438e 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -4,13 +4,11 @@ import model.SubTask; import model.Task; -import java.time.Duration; import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.TreeSet; -import java.util.stream.Collectors; /** * Контроллер для управления временными интервалами задач. @@ -97,7 +95,7 @@ public boolean isTimeOverlappingWithTreeSearch(Task task) { * @implNote Метод вызывается при добавлении подзадачи */ public void updateEpicTimeParams(Epic epic, SubTask subTask) { - if (subTask.getDuration() == null || subTask.getStartTime() == null) { + if (hasMissingTimeFields(subTask)) { return; } @@ -120,14 +118,15 @@ public void updateEpicTimeParams(Epic epic, SubTask subTask) { *

  • Очистка параметров: Если подзадач не осталось, сбрасывает все временные параметры в {@code null}.
  • * * - * @param epic эпик, параметры которого следует обновить - * @param durationToSubtract длительность, которую следует вычесть + * @param epic эпик, параметры которого следует обновить + * @param subtask подзадача, длительность которой следует вычесть * @implSpec Метод вызывается после удаления подзадачи из эпика * @apiNote Метод пересчитывает параметры на основе всех оставшихся подзадач */ - public void updateEpicTimeParamsDeletion(Epic epic, Duration durationToSubtract) { + public void updateEpicTimeParamsDeletion(Epic epic, SubTask subtask) { + if (hasMissingTimeFields(subtask)) return; - epic.setEpicDuration(-durationToSubtract.toMinutes()); // в любом случае удаляем + epic.setEpicDuration(-subtask.getDuration().toMinutes()); // в любом случае удаляем List subtasks = timeSortedTasks.stream() // получаем список подзадач .filter(task -> task.getType() == Type.SUBTASK) From a58f84cfd5245accfa2974cd24a39e6f23701266 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 02:28:58 +0300 Subject: [PATCH 26/41] fix: taskTimeController.getPrioritizedTasks() returns List.copyOf InMemoryTaskManager.getPrioritizedTasks returns taskTimeController.getPrioritizedTasks() --- src/managers/InMemoryTaskManager.java | 2 +- src/util/TaskTimeController.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 2cdff2c..f44aefc 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -119,7 +119,7 @@ public List getSubTasksFromEpic(int id) { @Override public List getPrioritizedTasks() { - return List.copyOf(taskTimeController.getTimeSortedTasks()); + return taskTimeController.getPrioritizedTasks(); } @Override diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 0f9438e..914ed32 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -210,9 +210,9 @@ public void clear() { * Возвращает неизменяемую копию отсортированного набора задач. * Изменения в возвращаемой коллекции не влияют на внутреннее состояние. * - * @return неизменяемый {@code TreeSet} с задачами, отсортированными по времени + * @return неизменяемый {@code List} с задачами, отсортированными по времени */ - public TreeSet getTimeSortedTasks() { - return new TreeSet<>(timeSortedTasks); + public List getPrioritizedTasks() { + return List.copyOf(timeSortedTasks); } } From e47d0c2b38ae3895bd6b32495142da5c65e31f64 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 03:16:13 +0300 Subject: [PATCH 27/41] fix: add checks for null fields --- src/util/TaskTimeController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 914ed32..8b7fce7 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -106,7 +106,7 @@ public void updateEpicTimeParams(Epic epic, SubTask subTask) { /** * Обновляет временные параметры эпика при удалении подзадачи. - * + * Игнорирует подзадачи без полей времени. *

    Выполняет следующие операции: *

      *
    • Длительность: Уменьшает общую длительность эпика на длительность удаляемой подзадачи @@ -166,6 +166,9 @@ public void add(Task task) { } public void remove(Task task) { + if (hasMissingTimeFields(task)) { + return; + } timeSortedTasks.remove(task); } @@ -173,7 +176,7 @@ public void remove(Task task) { * Удаляет задачу или подзадачу из отсортированной коллекции по идентификатору. * *

      Метод выполняет поиск элемента с указанным идентификатором и удаляет его - * из внутренней отсортированной коллекции временных интервалов. + * из внутренней отсортированной коллекции временных интервалов, если у него есть поля времени. * *

      Не рекомендуется для общего использования - метод следует использовать только * если нет доступа к объекту задачи и известен только {@code id}. @@ -181,7 +184,7 @@ public void remove(Task task) { * @param id идентификатор задачи для удаления */ public void remove(int id) { - timeSortedTasks.removeIf(element -> element.getTaskId() == id); + timeSortedTasks.removeIf(element -> element.getTaskId() == id && !hasMissingTimeFields(element)); } /** From 6cb9b06be6f7f48427534d0e8d406ad697dbd453 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 03:31:07 +0300 Subject: [PATCH 28/41] feat: enum CsvField to avoid magic numbers fix: try-catch block in FileBackedTaskManager --- .../filedbacked/FileBackedTaskManager.java | 49 +++++++++++-------- src/util/CsvField.java | 22 +++++++++ 2 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 src/util/CsvField.java diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index 1f7ee01..a0720a1 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -4,6 +4,9 @@ import model.Epic; import model.SubTask; import model.Task; + +import static util.CsvField.*; + import util.Status; import util.Type; import util.exceptions.ManagerLoadException; @@ -98,27 +101,31 @@ public static FileBackedTaskManager loadFromFile(File filename) throws ManagerLo continue; } String[] parts = line.split(","); - - int id = parseInteger(parts[0]); - Type type = parseType(parts[1]); - String title = parts[2]; - Status status = parseStatus(parts[3]); - String description = parts[4]; - int epicId = parseOptionalInteger(parts[5]); - Duration duration = parseOptionalDuration(parts[6]); - LocalDateTime startTime = parseOptionalDateTime(parts[7], formatter); - - maxId = Math.max(maxId, id); - manager.setIdCount(id - 1); // менеджер сам присвоит id, устанавливаем счетчик на предыдущий - - if (type == TASK) { - manager.addTask(new Task(title, description, status, duration, startTime)); - } else if (type == EPIC) { - manager.addEpic(new Epic(title, description, status)); - } else if (type == SUBTASK) { - manager.addSubTask(new SubTask(title, description, status, epicId, duration, startTime)); - } else { - System.out.println("Такой формат не существует"); + try { + int id = parseInteger(parts[ID.get()]); + Type type = parseType(parts[TYPE.get()]); + String title = parts[TITLE.get()]; + Status status = parseStatus(parts[STATUS.get()]); + String description = parts[DESCRIPTION.get()]; + int epicId = parseOptionalInteger(parts[EPIC_ID.get()]); + Duration duration = parseOptionalDuration(parts[DURATION.get()]); + LocalDateTime startTime = parseOptionalDateTime(parts[START_TIME.get()], formatter); + + + maxId = Math.max(maxId, id); + manager.setIdCount(id - 1); // менеджер сам присвоит id, устанавливаем счетчик на предыдущий + + if (type == TASK) { + manager.addTask(new Task(title, description, status, duration, startTime)); + } else if (type == EPIC) { + manager.addEpic(new Epic(title, description, status)); + } else if (type == SUBTASK) { + manager.addSubTask(new SubTask(title, description, status, epicId, duration, startTime)); + } else { + System.out.println("Такой формат не существует"); + } + } catch (ManagerLoadException e) { + e.printStackTrace(); } } diff --git a/src/util/CsvField.java b/src/util/CsvField.java new file mode 100644 index 0000000..fe1feea --- /dev/null +++ b/src/util/CsvField.java @@ -0,0 +1,22 @@ +package util; + +public enum CsvField { + ID(0), + TYPE(1), + TITLE(2), + STATUS(3), + DESCRIPTION(4), + EPIC_ID(5), + DURATION(6), + START_TIME(7); + + private final int index; + + CsvField(int index) { + this.index = index; + } + + public int get(){ + return index; + } +} From e01fe7168b7eede769fae5c580731ec24a1d9d67 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 04:16:44 +0300 Subject: [PATCH 29/41] fix: remove useless else --- src/managers/filedbacked/FileBackedTaskManager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index a0720a1..7b08690 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -4,9 +4,6 @@ import model.Epic; import model.SubTask; import model.Task; - -import static util.CsvField.*; - import util.Status; import util.Type; import util.exceptions.ManagerLoadException; @@ -20,6 +17,7 @@ import java.util.List; import static managers.filedbacked.ParserHelper.*; +import static util.CsvField.*; import static util.Type.*; /** @@ -92,7 +90,6 @@ public static FileBackedTaskManager loadFromFile(File filename) throws ManagerLo String header = reader.readLine(); validateHeader(header); - int maxId = 0; // будет присвоен счетчику менеджера while (reader.ready()) { @@ -111,7 +108,6 @@ public static FileBackedTaskManager loadFromFile(File filename) throws ManagerLo Duration duration = parseOptionalDuration(parts[DURATION.get()]); LocalDateTime startTime = parseOptionalDateTime(parts[START_TIME.get()], formatter); - maxId = Math.max(maxId, id); manager.setIdCount(id - 1); // менеджер сам присвоит id, устанавливаем счетчик на предыдущий @@ -121,8 +117,6 @@ public static FileBackedTaskManager loadFromFile(File filename) throws ManagerLo manager.addEpic(new Epic(title, description, status)); } else if (type == SUBTASK) { manager.addSubTask(new SubTask(title, description, status, epicId, duration, startTime)); - } else { - System.out.println("Такой формат не существует"); } } catch (ManagerLoadException e) { e.printStackTrace(); From 710cd2cacd47d25930e3844f13663cf29ea43432 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 04:19:12 +0300 Subject: [PATCH 30/41] codeStyle fix --- src/managers/TaskManager.java | 4 ++-- src/util/CsvField.java | 2 +- src/util/exceptions/TaskTimeOverlapException.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/managers/TaskManager.java b/src/managers/TaskManager.java index 2cabed6..8450f89 100644 --- a/src/managers/TaskManager.java +++ b/src/managers/TaskManager.java @@ -1,11 +1,11 @@ package managers; -import java.util.List; - import model.Epic; import model.SubTask; import model.Task; +import java.util.List; + /** * Интерфейс менеджера задач для управления задачами, эпиками и подзадачами. * diff --git a/src/util/CsvField.java b/src/util/CsvField.java index fe1feea..3f3b79f 100644 --- a/src/util/CsvField.java +++ b/src/util/CsvField.java @@ -16,7 +16,7 @@ public enum CsvField { this.index = index; } - public int get(){ + public int get() { return index; } } diff --git a/src/util/exceptions/TaskTimeOverlapException.java b/src/util/exceptions/TaskTimeOverlapException.java index 50156f3..c8d0b64 100644 --- a/src/util/exceptions/TaskTimeOverlapException.java +++ b/src/util/exceptions/TaskTimeOverlapException.java @@ -1,6 +1,6 @@ package util.exceptions; -public class TaskTimeOverlapException extends RuntimeException{ +public class TaskTimeOverlapException extends RuntimeException { public TaskTimeOverlapException() { super(); From e675dd1c743ae70ea540c7cecc9688038d04cf02 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 04:19:28 +0300 Subject: [PATCH 31/41] add tests --- test/managers/FileBackedTaskManagerTest.java | 75 +++- test/managers/InMemoryHistoryManagerTest.java | 2 +- test/managers/InMemoryTaskManagerTest.java | 100 ++++- test/model/EpicTest.java | 80 +++- test/model/TaskTest.java | 76 +++- test/util/TaskTimeControllerTest.java | 398 ++++++++++++++++++ 6 files changed, 723 insertions(+), 8 deletions(-) create mode 100644 test/util/TaskTimeControllerTest.java diff --git a/test/managers/FileBackedTaskManagerTest.java b/test/managers/FileBackedTaskManagerTest.java index 48127ae..eb4a9c7 100644 --- a/test/managers/FileBackedTaskManagerTest.java +++ b/test/managers/FileBackedTaskManagerTest.java @@ -1,15 +1,18 @@ package managers; import managers.filedbacked.FileBackedTaskManager; -import util.exceptions.ManagerSaveException; import model.Epic; -import util.Status; import model.SubTask; import model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import util.Status; +import util.exceptions.ManagerLoadException; +import util.exceptions.ManagerSaveException; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; @@ -95,4 +98,72 @@ public void shouldSupportUTF8() { assertEquals(words[i + 1], task.getDescription()); } } + + private void writeToTempFile(String line) { + String header = "id,type,name,status,description,epic,duration,startTime"; + try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { + writer.write(header); + writer.newLine(); + writer.write(line); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void shouldSkipWhenInvalidHeader() throws IOException { + File tempfile1 = File.createTempFile("task1", "csv"); + assertThrows(ManagerLoadException.class, () -> { + FileBackedTaskManager.loadFromFile(tempfile1); + }); + } + + @Test + public void shouldSkipWhenInvalidId() { + writeToTempFile("INVALID,TASK,task1,NEW,demo,null,PT9M,1970-01-01 00:00:00.000"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } + + @Test + public void shouldSkipWhenEmptyId() { + writeToTempFile(" ,TASK,task1,NEW,demo,null,PT9M,1970-01-01 00:00:00.000"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } + + @Test + public void shouldSkipWhenInvalidType() { + writeToTempFile("1,INVALID,task1,NEW,demo,null,PT9M,1970-01-01 00:00:00.000"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } + + @Test + public void shouldSkipWhenInvalidStatus() { + writeToTempFile("1,TASK,task1,INVALID,demo,null,PT9M,1970-01-01 00:00:00.000"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } + + @Test + public void shouldSkipWhenInvalidEpicId() { + writeToTempFile("1,TASK,task1,NEW,demo,INVALID,PT9M,1970-01-01 00:00:00.000"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } + + @Test + public void shouldSkipWhenInvalidDuration() { + writeToTempFile("1,TASK,task1,NEW,demo,null,INVALID,1970-01-01 00:00:00.000"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } + + @Test + public void shouldSkipWhenInvalidDateTime() { + writeToTempFile("1,TASK,task1,NEW,demo,null,PT9M,INVALID"); + FileBackedTaskManager manager1 = FileBackedTaskManager.loadFromFile(tempFile); + assertEquals(0, manager1.getTasks().size()); + } } \ No newline at end of file diff --git a/test/managers/InMemoryHistoryManagerTest.java b/test/managers/InMemoryHistoryManagerTest.java index af5d497..eb1f007 100644 --- a/test/managers/InMemoryHistoryManagerTest.java +++ b/test/managers/InMemoryHistoryManagerTest.java @@ -2,10 +2,10 @@ import managers.history.HistoryManager; import model.Epic; -import util.Status; import model.SubTask; import model.Task; import org.junit.jupiter.api.Test; +import util.Status; import static org.junit.jupiter.api.Assertions.*; diff --git a/test/managers/InMemoryTaskManagerTest.java b/test/managers/InMemoryTaskManagerTest.java index c4da0a9..174f55f 100644 --- a/test/managers/InMemoryTaskManagerTest.java +++ b/test/managers/InMemoryTaskManagerTest.java @@ -1,9 +1,14 @@ package managers; -import model.*; +import model.Epic; +import model.SubTask; +import model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import util.Status; +import util.exceptions.TaskTimeOverlapException; + +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.*; @@ -265,5 +270,98 @@ public void epicStatusShouldBeNewWhenSubTasksClear() { assertEquals(Status.NEW, epic.getStatus()); } + @Test + public void shouldThrowExceptionIfOverLap() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + Task task1 = new Task("task1", "demo", Status.NEW, 10, epochTime); + manager.addTask(task1); + + Task task2 = new Task("task2", "demo", Status.NEW, 5, epochTime); + + assertThrows(TaskTimeOverlapException.class, () -> { + manager.addTask(task2); + }); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, 5, epochTime); + + assertThrows(TaskTimeOverlapException.class, () -> { + manager.addSubTask(subTask1); + }); + } + + @Test + public void shouldDeleteTasksFromControllerWhenDeleteFromManager() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + Task task1 = new Task("task1", "demo", Status.NEW, 5, epochTime); + manager.addTask(task1); + manager.deleteTask(task1.getTaskId()); + + assertEquals(0, manager.getPrioritizedTasks().size()); + } + + @Test + public void shouldDeleteSubTasksFromControllerWhenDeleteFromManager() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, 5, epochTime); + manager.addSubTask(subTask1); + manager.deleteSubTask(subTask1.getTaskId()); + + assertEquals(0, manager.getPrioritizedTasks().size()); + } + + @Test + public void shouldDeleteTasksFromControllerClearTasksFromManager() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + Task task1 = new Task("task1", "demo", Status.NEW, 5, epochTime); + manager.addTask(task1); + manager.clearTasks(); + + assertEquals(0, manager.getPrioritizedTasks().size()); + } + + @Test + public void shouldDeleteSubTasksFromControllerWhenClearSubTasksFromManager() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, 5, epochTime); + manager.addSubTask(subTask1); + manager.clearSubTasks(); + + assertEquals(0, manager.getPrioritizedTasks().size()); + } + + @Test + public void shouldDeleteSubTasksFromControllerWhenDeleteEpicFromManager() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, 5, epochTime); + manager.addSubTask(subTask1); + manager.deleteEpic(epicId); + + assertEquals(0, manager.getPrioritizedTasks().size()); + } + + @Test + public void shouldDeleteSubTasksFromControllerWhenClearEpicsFromManager() { + LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, 5, epochTime); + manager.addSubTask(subTask1); + manager.clearEpics(); + + assertEquals(0, manager.getPrioritizedTasks().size()); + } + } \ No newline at end of file diff --git a/test/model/EpicTest.java b/test/model/EpicTest.java index f44d007..2532525 100644 --- a/test/model/EpicTest.java +++ b/test/model/EpicTest.java @@ -1,11 +1,20 @@ package model; -import org.junit.jupiter.api.Assertions; +import static org.junit.jupiter.api.Assertions.*; + import org.junit.jupiter.api.Test; import util.Status; +import java.time.Duration; +import java.time.LocalDateTime; + public class EpicTest { + private final long tenMinutes = 10; + private final Duration durationTenMinutes = Duration.ofMinutes(tenMinutes); + private final LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + @Test public void shouldBeEqualWhenIdEqual() { @@ -15,7 +24,74 @@ public void shouldBeEqualWhenIdEqual() { epic1.setTaskId(1); // устанавливаем одинаковые id epic2.setTaskId(1); - Assertions.assertEquals(epic1.getTaskId(), epic2.getTaskId(), "ids are not equal"); + assertEquals(epic1.getTaskId(), epic2.getTaskId(), "ids are not equal"); + } + + @Test + public void shouldBeEqualWhenOverLoad() { + Epic epic1 = new Epic("test1", "test1", Status.NEW); + Epic epic2 = new Epic("test1", "test1", Status.NEW); + + epic1.setEpicDuration(durationTenMinutes); + epic2.setEpicDuration(tenMinutes); + + assertEquals(epic1.getDuration(), epic2.getDuration()); + } + + @Test + public void durationShouldBeEqualAfterAddition() { + Duration expected = Duration.ofMinutes(20); + Epic epic1 = new Epic("test1", "test1", Status.NEW); + + epic1.setEpicDuration(durationTenMinutes); + assertNotNull(epic1.getDuration()); + + epic1.setEpicDuration(durationTenMinutes); + assertEquals(expected, epic1.getDuration()); + } + @Test + public void startTimeShouldBeSetWhenDateIsBefore() { + LocalDateTime expected = epochTime.plusMinutes(-tenMinutes); + Epic epic1 = new Epic("test1", "test1", Status.NEW); + epic1.setStartTime(epochTime); + + epic1.setEpicStartTime(expected); + + assertEquals(expected, epic1.getStartTime()); + } + + @Test + public void startTimeShouldNotBeSetWhenDateIsAfter() { + LocalDateTime expected = epochTime.plusMinutes(tenMinutes); + Epic epic1 = new Epic("test1", "test1", Status.NEW); + epic1.setStartTime(epochTime); + + epic1.setEpicStartTime(expected); + + assertNotEquals(expected, epic1.getStartTime()); + } + + @Test + public void endTimeShouldBeSetWhenDateIsAfter() { + LocalDateTime expected = epochTime.plusMinutes(tenMinutes); + Epic epic1 = new Epic("test1", "test1", Status.NEW); + epic1.setEndTime(epochTime); + + epic1.setEpicEndTime(expected); + + assertEquals(expected, epic1.getEndTime()); + } + + @Test + public void endTimeShouldNotBeSetWhenDateIsBefore() { + LocalDateTime expected = epochTime.plusMinutes(-tenMinutes); + Epic epic1 = new Epic("test1", "test1", Status.NEW); + epic1.setEndTime(epochTime); + + epic1.setEpicEndTime(expected); + + assertNotEquals(expected, epic1.getStartTime()); + } } \ No newline at end of file diff --git a/test/model/TaskTest.java b/test/model/TaskTest.java index d068eeb..8c91d24 100644 --- a/test/model/TaskTest.java +++ b/test/model/TaskTest.java @@ -1,11 +1,23 @@ package model; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import util.Status; +import util.Type; + +import java.time.Duration; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class TaskTest { + private final long tenMinutes = 10; + private final Duration durationTenMinutes = Duration.ofMinutes(tenMinutes); + private final LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + @Test public void shouldBeEqualWhenIdEqual() { Task task1 = new Task("test1", "test1", Status.NEW); @@ -14,7 +26,67 @@ public void shouldBeEqualWhenIdEqual() { task1.setTaskId(1); // устанавливаем одинаковые id task2.setTaskId(1); - Assertions.assertEquals(task1.getTaskId(), task2.getTaskId(), "ids are not equal"); + assertEquals(task1.getTaskId(), task2.getTaskId(), "ids are not equal"); + } + + @Test + public void shouldReturnCorrectType() { + Type expectedType = Type.TASK; + Task task1 = new Task("test1", "test1", Status.NEW); + + assertEquals(expectedType, task1.getType()); + } + + @Test + public void shouldBeEqualWhenDifferentConstructorsDuration() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes); + Task task2 = new Task("task2", "demo", Status.NEW, durationTenMinutes); + + assertEquals(task1.getDuration(), task2.getDuration()); + assertEquals(task1.getDuration(), durationTenMinutes); + } + + @Test + public void shouldBeEqualWhenDurationSet() { + Task task1 = new Task("task1", "demo", Status.NEW); + Task task2 = new Task("task2", "demo", Status.NEW); + + task1.setDuration(durationTenMinutes); + task2.setDuration(tenMinutes); + + assertEquals(task1.getDuration(), task2.getDuration()); + } + + @Test + public void shouldBeEqualWhenConstructorsAndSetLocalDateTime() { + + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, durationTenMinutes); + + task2.setStartTime(epochTime); + + assertEquals(task1.getStartTime(), task2.getStartTime()); + assertEquals(task1.getStartTime(), epochTime); + } + + @Test + public void shouldBeEqualWhenReturnEndTime() { + LocalDateTime expectedEndTime = epochTime.plus(durationTenMinutes); + + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + + assertEquals(expectedEndTime, task1.getEndTime()); + } + + @Test + public void shouldBeNullWhenNoDurationOrStartTime() { + LocalDateTime expectedEndTime = epochTime.plus(durationTenMinutes); + + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes); + Task task2 = new Task("task2", "demo", Status.NEW); + + assertNull(task1.getEndTime()); + assertNull(task2.getEndTime()); } } \ No newline at end of file diff --git a/test/util/TaskTimeControllerTest.java b/test/util/TaskTimeControllerTest.java new file mode 100644 index 0000000..c056937 --- /dev/null +++ b/test/util/TaskTimeControllerTest.java @@ -0,0 +1,398 @@ +package util; + +import managers.InMemoryTaskManager; +import managers.Managers; +import managers.TaskManager; +import model.Epic; +import model.SubTask; +import model.Task; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class TaskTimeControllerTest { + + private final TaskTimeController ttController = new TaskTimeController(); + private final long tenMinutes = 10; + private final LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + + + @Test + public void shouldBeTrueWhenOverlapSameTime() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime); + + assertTrue(ttController.isTimeOverlapping(task2)); + } + + @Test + public void shouldBeTrueWhenOverlapStartBeforeEnd() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + LocalDateTime wrongStart = epochTime.plusMinutes(8); // за 2 минут до конца таска1 + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, wrongStart); + + assertTrue(ttController.isTimeOverlapping(task2)); + } + + @Test + public void shouldBeTrueWhenOverlapEndAfterStart() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + long fiveMinutes = 5; + Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, epochTime); + + assertTrue(ttController.isTimeOverlapping(task2)); + } + + @Test + public void shouldBeTrueWhenOverlapInside() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + LocalDateTime wrongStart = epochTime.plusMinutes(1); // за 2 минут до конца таска1 + long fiveMinutes = 5; + Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, wrongStart); + + assertTrue(ttController.isTimeOverlapping(task2)); + } + + @Test + public void shouldBeNoOverlapIfNoFields() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes); + Task task2 = new Task("task2", "demo", Status.NEW); + + ttController.add(task1); + ttController.add(task1); + + assertFalse(ttController.isTimeOverlapping(task1)); + assertFalse(ttController.isTimeOverlapping(task2)); + } + + @Test + public void shouldBeNoOverlap() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); + Task task3 = new Task("task3", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(-tenMinutes)); + + assertFalse(ttController.isTimeOverlapping(task1)); + assertFalse(ttController.isTimeOverlapping(task2)); + assertFalse(ttController.isTimeOverlapping(task3)); + } + + + @Test + public void shouldNotAddEpic() { + Epic epic = new Epic("epic", "demo", Status.NEW); + + ttController.add(epic); + + assertEquals(0, ttController.getPrioritizedTasks().size()); + } + + @Test + public void shouldNotAddIfMissingTimeFields() { + Task task1 = new Task("task1", "demo", Status.NEW); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes); + + ttController.add(task1); + ttController.add(task2); + + assertEquals(0, ttController.getPrioritizedTasks().size()); + } + + @Test + public void shouldAddInPriorityOrder() { + Task[] tasks = { + new Task("task5", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(10000)), + new Task("task4", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(1000)), + new Task("task3", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(100)), + new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(10)), + new Task("task1", "demo", Status.NEW, tenMinutes, epochTime), + }; + + for (Task task : tasks) { + ttController.add(task); + } + + List priorityTasks = ttController.getPrioritizedTasks(); + + for (int i = 0; i < priorityTasks.size(); i++) { + assertEquals(tasks[tasks.length - i - 1], priorityTasks.get(i)); + } + } + + @Test + public void shouldSumDurationAndCalculateEndTimeWhenAddSubtasks() { + TaskManager manager = Managers.getDefault(); + + int epicId = 1; + Epic epic = new Epic("epic", "demo", Status.NEW); + manager.addEpic(epic); + + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 2)); + + Duration expectedDuration = Duration.ofMinutes(tenMinutes * 3); + LocalDateTime expectedEndTime = epochTime.plus(expectedDuration); + + manager.addSubTask(subTask1); + manager.addSubTask(subTask2); + manager.addSubTask(subTask3); + + assertEquals(expectedDuration, epic.getDuration()); + assertEquals(expectedEndTime, epic.getEndTime()); + } + + @Test + public void shouldRecalculateDurationAndEndTimeWhenDeleteLastSubTask() { + TaskManager manager = Managers.getDefault(); + + int epicId = 1; + Epic epic = new Epic("epic", "demo", Status.NEW); + manager.addEpic(epic); + + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 5)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 10)); + + Duration expectedDuration = Duration.ofMinutes(tenMinutes * 2); + LocalDateTime expectedEndTime = epochTime.plusMinutes(tenMinutes * 5).plusMinutes(tenMinutes); + + manager.addSubTask(subTask1); + manager.addSubTask(subTask2); + manager.addSubTask(subTask3); + + manager.deleteSubTask(4); //subtask3 + + assertEquals(expectedDuration, epic.getDuration()); + assertEquals(expectedEndTime, epic.getEndTime()); + } + + @Test + public void shouldRecalculateDurationAndEndTimeWhenDeleteFirstSubTask() { + TaskManager manager = Managers.getDefault(); + + int epicId = 1; + Epic epic = new Epic("epic", "demo", Status.NEW); + manager.addEpic(epic); + + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 2)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes)); + + Duration expectedDuration = Duration.ofMinutes(tenMinutes * 2); + LocalDateTime expectedStartTime = epochTime.plusMinutes(tenMinutes); + + manager.addSubTask(subTask1); + manager.addSubTask(subTask2); + manager.addSubTask(subTask3); + + manager.deleteSubTask(2); //subtask1 + + assertEquals(expectedDuration, epic.getDuration()); + assertEquals(expectedStartTime, epic.getStartTime()); + } + + @Test + public void shouldBeNullWhenAllDeleted() { + TaskManager manager = Managers.getDefault(); + + int epicId = 1; + Epic epic = new Epic("epic", "demo", Status.NEW); + manager.addEpic(epic); + + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 2)); + + manager.addSubTask(subTask1); + manager.addSubTask(subTask2); + manager.addSubTask(subTask3); + + manager.deleteSubTask(2); //subtask1 + manager.deleteSubTask(3); //subtask2 + manager.deleteSubTask(4); //subtask3 + + assertNull(epic.getStartTime()); + assertNull(epic.getDuration()); + assertNull(epic.getEndTime()); + } + + + @Test + public void shouldRemoveByObject() { + Task task = new Task("task1", "demo", Status.NEW); + task.setTaskId(1); + + ttController.add(task); + + ttController.remove(task); + + assertEquals(0, ttController.getPrioritizedTasks().size()); + } + + @Test + public void shouldRemoveById() { + Task task = new Task("task1", "demo", Status.NEW); + task.setTaskId(1); + + ttController.add(task); + + ttController.remove(1); + + assertEquals(0, ttController.getPrioritizedTasks().size()); + } + + @Test + public void shouldRemoveAll() { + Task task = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + task.setTaskId(1); + + Task task1 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); + task.setTaskId(2); + + ttController.add(task); + ttController.add(task1); + + ttController.clear(); + + assertEquals(0, ttController.getPrioritizedTasks().size()); + } + + @Test + public void shouldRemoveOnlyTasks() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); + + int epicId = 3; + Epic epic = new Epic("epic", "demo", Status.NEW); + epic.setTaskId(epicId); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(100)); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(1000)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(10000)); + + ttController.add(task1); + ttController.add(task2); + + ttController.add(subTask1); + ttController.add(subTask2); + ttController.add(subTask3); + + ttController.removeTasks(); + + int actualAmount = 0; + for (Task task : ttController.getPrioritizedTasks()) { + if (task.getType() == Type.TASK) { + actualAmount++; + } + } + + assertEquals(0, actualAmount); + } + + @Test + public void shouldRemoveOnlySubTasks() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); + + int epicId = 3; + Epic epic = new Epic("epic", "demo", Status.NEW); + epic.setTaskId(epicId); + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(100)); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(1000)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(10000)); + + ttController.add(task1); + ttController.add(task2); + + ttController.add(subTask1); + ttController.add(subTask2); + ttController.add(subTask3); + + ttController.removeSubTasks(); + + int actualAmount = 0; + for (Task task : ttController.getPrioritizedTasks()) { + if (task.getType() == Type.SUBTASK) { + actualAmount++; + } + } + + assertEquals(0, actualAmount); + } + + + @Test + public void shouldBeTrueWhenOverlapSameTimeTreeSearch() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime); + + assertTrue(ttController.isTimeOverlappingWithTreeSearch(task2)); + } + + @Test + public void shouldBeTrueWhenOverlapStartBeforeEndTreeSearch() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + LocalDateTime wrongStart = epochTime.plusMinutes(8); // за 2 минут до конца таска1 + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, wrongStart); + + assertTrue(ttController.isTimeOverlappingWithTreeSearch(task2)); + } + + @Test + public void shouldBeTrueWhenOverlapEndAfterStartTreeSearch() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + long fiveMinutes = 5; + Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, epochTime); + + assertTrue(ttController.isTimeOverlappingWithTreeSearch(task2)); + } + + @Test + public void shouldBeTrueWhenOverlapInsideTreeSearch() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); + ttController.add(task1); + + LocalDateTime wrongStart = epochTime.plusMinutes(1); // за 2 минут до конца таска1 + long fiveMinutes = 5; + Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, wrongStart); + + assertTrue(ttController.isTimeOverlappingWithTreeSearch(task2)); + } + + @Test + public void shouldBeNoOverlapIfNoFieldsTreeSearch() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes); + Task task2 = new Task("task2", "demo", Status.NEW); + + ttController.add(task1); + ttController.add(task1); + + assertFalse(ttController.isTimeOverlappingWithTreeSearch(task1)); + assertFalse(ttController.isTimeOverlappingWithTreeSearch(task2)); + } + +} \ No newline at end of file From cee9956a40c9c311b64ea40ba4ae3b54a8620bbb Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 04:44:31 +0300 Subject: [PATCH 32/41] fix: typo --- src/managers/filedbacked/FileBackedTaskManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index 7b08690..c851ca7 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -230,7 +230,7 @@ public void updateTask(Task task) { @Override public void updateEpic(Epic epic) { - super.updateTask(epic); + super.updateEpic(epic); save(); } From 1eec74695d14175f7d210e580579ca4402840bf0 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 04:45:00 +0300 Subject: [PATCH 33/41] fix: critical errors in updateEpicTimeParamsDeletion() --- src/managers/InMemoryTaskManager.java | 2 +- src/util/TaskTimeController.java | 49 ++++++++++++++++----------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index f44aefc..fb5c579 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -288,7 +288,7 @@ public void deleteSubTask(int id) { historyManager.remove(id); taskTimeController.remove(subTask); - taskTimeController.updateEpicTimeParamsDeletion(epic, subTask); + taskTimeController.updateEpicTimeParamsDeletion(epic, subTask, getSubTasksFromEpic(epicParentId)); subtasks.remove(id); } diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 8b7fce7..9399d27 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -105,33 +105,33 @@ public void updateEpicTimeParams(Epic epic, SubTask subTask) { } /** - * Обновляет временные параметры эпика при удалении подзадачи. - * Игнорирует подзадачи без полей времени. + * Обновляет временные параметры эпика после удаления подзадачи. + * *

      Выполняет следующие операции: *

        *
      • Длительность: Уменьшает общую длительность эпика на длительность удаляемой подзадачи - * с помощью {@link Epic#setEpicDuration(java.time.Duration)}.
      • - *
      • Время начала: Находит самое раннее время начала среди оставшихся подзадач - * и устанавливает его как время начала эпика.
      • - *
      • Время окончания: Находит самое позднее время окончания среди оставшихся подзадач + * с помощью {@link Epic#setEpicDuration(long)}.
      • + *
      • Время начала: Находит самое раннее время начала среди валидных оставшихся подзадач + * (игнорируя задачи без временных параметров) и устанавливает его как время начала эпика.
      • + *
      • Время окончания: Находит самое позднее время окончания среди валидных оставшихся подзадач * и устанавливает его как время окончания эпика.
      • - *
      • Очистка параметров: Если подзадач не осталось, сбрасывает все временные параметры в {@code null}.
      • + *
      • Очистка параметров: Если не осталось валидных подзадач с временными параметрами, + * сбрасывает все временные параметры эпика в {@code null}.
      • *
      * - * @param epic эпик, параметры которого следует обновить - * @param subtask подзадача, длительность которой следует вычесть - * @implSpec Метод вызывается после удаления подзадачи из эпика - * @apiNote Метод пересчитывает параметры на основе всех оставшихся подзадач + * @param epic эпик, параметры которого следует обновить + * @param subtask удаляемая подзадача, длительность которой следует вычесть + * @param subtasks список всех оставшихся подзадач эпика после удаления + * + * @implSpec Метод вызывается после удаления подзадачи + * @apiNote Игнорирует подзадачи без установленных временных параметров ({@code duration} или {@code startTime}) + * @implNote Время начала ищется по {@code startTime}, время окончания - по {@code endTime} */ - public void updateEpicTimeParamsDeletion(Epic epic, SubTask subtask) { + public void updateEpicTimeParamsDeletion(Epic epic, SubTask subtask, List subtasks) { if (hasMissingTimeFields(subtask)) return; epic.setEpicDuration(-subtask.getDuration().toMinutes()); // в любом случае удаляем - List subtasks = timeSortedTasks.stream() // получаем список подзадач - .filter(task -> task.getType() == Type.SUBTASK) - .map(task -> (SubTask) task).toList(); - if (subtasks.isEmpty()) { epic.setStartTime(null); epic.setDuration(null); @@ -139,11 +139,20 @@ public void updateEpicTimeParamsDeletion(Epic epic, SubTask subtask) { return; } - Optional newEndTime = subtasks.stream().max(Comparator.comparing(Task::getEndTime)); - epic.setEndTime(newEndTime.get().getEndTime()); + subtasks.stream() + .filter(subTask -> !hasMissingTimeFields(subTask)) + .max(Comparator.comparing(Task::getEndTime)) + .ifPresent( + subTask -> epic.setEndTime(subTask.getEndTime()) + ); + + subtasks.stream() + .filter(subTask -> !hasMissingTimeFields(subTask)) + .min(Comparator.comparing(Task::getStartTime)) + .ifPresent( + subTask -> epic.setStartTime(subTask.getStartTime()) + ); - Optional newStartTime = subtasks.stream().min(Comparator.comparing(Task::getStartTime)); - epic.setStartTime(newStartTime.get().getStartTime()); } private boolean hasMissingTimeFields(Task task) { From 4d4f986fce7c5181f858b5019e6283aaf948568f Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 04:52:31 +0300 Subject: [PATCH 34/41] test: edge case add --- test/util/TaskTimeControllerTest.java | 131 ++++++++++++++++++++------ 1 file changed, 100 insertions(+), 31 deletions(-) diff --git a/test/util/TaskTimeControllerTest.java b/test/util/TaskTimeControllerTest.java index c056937..b8364dc 100644 --- a/test/util/TaskTimeControllerTest.java +++ b/test/util/TaskTimeControllerTest.java @@ -1,6 +1,5 @@ package util; -import managers.InMemoryTaskManager; import managers.Managers; import managers.TaskManager; import model.Epic; @@ -80,9 +79,12 @@ public void shouldBeNoOverlapIfNoFields() { @Test public void shouldBeNoOverlap() { - Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); - Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); - Task task3 = new Task("task3", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(-tenMinutes)); + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, + epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(tenMinutes)); + Task task3 = new Task("task3", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(-tenMinutes)); assertFalse(ttController.isTimeOverlapping(task1)); assertFalse(ttController.isTimeOverlapping(task2)); @@ -113,11 +115,16 @@ public void shouldNotAddIfMissingTimeFields() { @Test public void shouldAddInPriorityOrder() { Task[] tasks = { - new Task("task5", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(10000)), - new Task("task4", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(1000)), - new Task("task3", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(100)), - new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(10)), - new Task("task1", "demo", Status.NEW, tenMinutes, epochTime), + new Task("task5", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(10000)), + new Task("task4", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(1000)), + new Task("task3", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(100)), + new Task("task2", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(10)), + new Task("task1", "demo", Status.NEW, tenMinutes, + epochTime), }; for (Task task : tasks) { @@ -140,9 +147,12 @@ public void shouldSumDurationAndCalculateEndTimeWhenAddSubtasks() { manager.addEpic(epic); - SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); - SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes)); - SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 2)); + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, + epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes * 2)); Duration expectedDuration = Duration.ofMinutes(tenMinutes * 3); LocalDateTime expectedEndTime = epochTime.plus(expectedDuration); @@ -164,9 +174,12 @@ public void shouldRecalculateDurationAndEndTimeWhenDeleteLastSubTask() { manager.addEpic(epic); - SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); - SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 5)); - SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 10)); + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, + epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes * 5)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes * 10)); Duration expectedDuration = Duration.ofMinutes(tenMinutes * 2); LocalDateTime expectedEndTime = epochTime.plusMinutes(tenMinutes * 5).plusMinutes(tenMinutes); @@ -190,9 +203,12 @@ public void shouldRecalculateDurationAndEndTimeWhenDeleteFirstSubTask() { manager.addEpic(epic); - SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); - SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 2)); - SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes)); + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, + epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes * 2)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes)); Duration expectedDuration = Duration.ofMinutes(tenMinutes * 2); LocalDateTime expectedStartTime = epochTime.plusMinutes(tenMinutes); @@ -216,9 +232,12 @@ public void shouldBeNullWhenAllDeleted() { manager.addEpic(epic); - SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime); - SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes)); - SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(tenMinutes * 2)); + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, + epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(tenMinutes * 2)); manager.addSubTask(subTask1); manager.addSubTask(subTask2); @@ -233,6 +252,46 @@ public void shouldBeNullWhenAllDeleted() { assertNull(epic.getEndTime()); } + @Test + public void shouldFindSubTaskFromRightEpic() { + TaskManager manager = Managers.getDefault(); + + int epicId1 = 1; + Epic epic = new Epic("epic1", "demo", Status.NEW); + manager.addEpic(epic); + + int epicId2 = 2; + Epic epic2 = new Epic("epic2", "demo", Status.NEW); + manager.addEpic(epic2); + + + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId1, tenMinutes, + epochTime); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId1, tenMinutes, + epochTime.plusMinutes(tenMinutes)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId1, tenMinutes, + epochTime.plusMinutes(tenMinutes * 2)); + + manager.addSubTask(subTask1); + manager.addSubTask(subTask2); + manager.addSubTask(subTask3); + + SubTask subTask4 = new SubTask("subtask4", "demo", Status.NEW, epicId2, tenMinutes, + epochTime.plusMinutes(tenMinutes * 3)); + SubTask subTask5 = new SubTask("subtask5", "demo", Status.NEW, epicId2, tenMinutes, + epochTime.plusMinutes(tenMinutes * 4)); + SubTask subTask6 = new SubTask("subtask6", "demo", Status.NEW, epicId2, tenMinutes, + epochTime.plusMinutes(tenMinutes * 5)); + + manager.addSubTask(subTask4); + manager.addSubTask(subTask5); + manager.addSubTask(subTask6); + + manager.deleteSubTask(6); //subtask4 + + assertEquals(subTask5.getStartTime(), epic2.getStartTime()); + } + @Test public void shouldRemoveByObject() { @@ -276,16 +335,21 @@ public void shouldRemoveAll() { @Test public void shouldRemoveOnlyTasks() { - Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); - Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, + epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(tenMinutes)); int epicId = 3; Epic epic = new Epic("epic", "demo", Status.NEW); epic.setTaskId(epicId); - SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(100)); - SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(1000)); - SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(10000)); + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(100)); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(1000)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(10000)); ttController.add(task1); ttController.add(task2); @@ -308,16 +372,21 @@ public void shouldRemoveOnlyTasks() { @Test public void shouldRemoveOnlySubTasks() { - Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); - Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, epochTime.plusMinutes(tenMinutes)); + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, + epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(tenMinutes)); int epicId = 3; Epic epic = new Epic("epic", "demo", Status.NEW); epic.setTaskId(epicId); - SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(100)); - SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(1000)); - SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, epochTime.plusMinutes(10000)); + SubTask subTask1 = new SubTask("subtask1", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(100)); + SubTask subTask2 = new SubTask("subtask2", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(1000)); + SubTask subTask3 = new SubTask("subtask3", "demo", Status.NEW, epicId, tenMinutes, + epochTime.plusMinutes(10000)); ttController.add(task1); ttController.add(task2); From 1b9cebb10c11c7080a9e76e35d3d909fa08256a2 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 05:28:50 +0300 Subject: [PATCH 35/41] feat: endTime update to null --- src/model/Epic.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/model/Epic.java b/src/model/Epic.java index 4124970..f451675 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -31,6 +31,7 @@ public void clearSubtasks() { this.setStatus(Status.NEW); this.duration = null; this.startTime = null; + this.endTime = null; } public List getSubtaskIds() { From 5f07b24123a2e395fbc26710f7eb81dc81819a2d Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 05:30:21 +0300 Subject: [PATCH 36/41] fix: deleted constructors that used Duration updated FileBackedTaskManager and ParserHelper to support it. test: deleted tests with this constructors --- .../filedbacked/FileBackedTaskManager.java | 9 +++++--- src/managers/filedbacked/ParserHelper.java | 7 ++++--- src/model/SubTask.java | 16 -------------- src/model/Task.java | 17 +-------------- test/model/TaskTest.java | 21 ------------------- 5 files changed, 11 insertions(+), 59 deletions(-) diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index c851ca7..637f7c6 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -15,6 +15,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Optional; import static managers.filedbacked.ParserHelper.*; import static util.CsvField.*; @@ -105,18 +106,20 @@ public static FileBackedTaskManager loadFromFile(File filename) throws ManagerLo Status status = parseStatus(parts[STATUS.get()]); String description = parts[DESCRIPTION.get()]; int epicId = parseOptionalInteger(parts[EPIC_ID.get()]); - Duration duration = parseOptionalDuration(parts[DURATION.get()]); + long maybeDuration = parseOptionalDuration(parts[DURATION.get()]) + .map(Duration::toMinutes) + .orElse(-1L); LocalDateTime startTime = parseOptionalDateTime(parts[START_TIME.get()], formatter); maxId = Math.max(maxId, id); manager.setIdCount(id - 1); // менеджер сам присвоит id, устанавливаем счетчик на предыдущий if (type == TASK) { - manager.addTask(new Task(title, description, status, duration, startTime)); + manager.addTask(new Task(title, description, status, maybeDuration, startTime)); } else if (type == EPIC) { manager.addEpic(new Epic(title, description, status)); } else if (type == SUBTASK) { - manager.addSubTask(new SubTask(title, description, status, epicId, duration, startTime)); + manager.addSubTask(new SubTask(title, description, status, epicId, maybeDuration, startTime)); } } catch (ManagerLoadException e) { e.printStackTrace(); diff --git a/src/managers/filedbacked/ParserHelper.java b/src/managers/filedbacked/ParserHelper.java index 9347add..6a80e9e 100644 --- a/src/managers/filedbacked/ParserHelper.java +++ b/src/managers/filedbacked/ParserHelper.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Optional; /** * Вспомогательный класс для парсинга данных задач из файла. @@ -76,13 +77,13 @@ protected static Status parseStatus(String value) throws ManagerLoadException { } } - protected static Duration parseOptionalDuration(String value) throws ManagerLoadException { + protected static Optional parseOptionalDuration(String value) throws ManagerLoadException { if (value == null || value.equals("null") || value.trim().isEmpty()) { - return null; + return Optional.empty(); } try { - return Duration.parse(value.trim()); + return Optional.of(Duration.parse(value.trim())); } catch (Exception e) { throw new ManagerLoadException("Invalid duration format: " + value); } diff --git a/src/model/SubTask.java b/src/model/SubTask.java index f97e145..439a05f 100644 --- a/src/model/SubTask.java +++ b/src/model/SubTask.java @@ -3,7 +3,6 @@ import util.Status; import util.Type; -import java.time.Duration; import java.time.LocalDateTime; public class SubTask extends Task { @@ -29,21 +28,6 @@ public SubTask(String title, String description, Status status, int epicId, long this.epicId = epicId; } - public SubTask(String title, - String description, - Status status, - int epicId, - Duration duration, - LocalDateTime startTime) { - super(title, description, status, duration, startTime); - this.epicId = epicId; - } - - public SubTask(String title, String description, Status status, int epicId, Duration duration) { - super(title, description, status, duration); - this.epicId = epicId; - } - public int getEpicId() { return epicId; } diff --git a/src/model/Task.java b/src/model/Task.java index 84bfc59..7e31d1f 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -26,7 +26,7 @@ public Task(String title, String description, Status status, long durationInMinu this.title = title; this.description = description; this.status = status; - this.duration = Duration.ofMinutes(durationInMinutes); + this.duration = durationInMinutes < 0 ? null : Duration.ofMinutes(durationInMinutes); this.startTime = startTime; } @@ -37,21 +37,6 @@ public Task(String title, String description, Status status, long durationInMinu this.duration = Duration.ofMinutes(durationInMinutes); } - public Task(String title, String description, Status status, Duration duration, LocalDateTime startTime) { - this.title = title; - this.description = description; - this.status = status; - this.duration = duration; - this.startTime = startTime; - } - - public Task(String title, String description, Status status, Duration duration) { - this.title = title; - this.description = description; - this.status = status; - this.duration = duration; - } - public int getTaskId() { return taskId; } diff --git a/test/model/TaskTest.java b/test/model/TaskTest.java index 8c91d24..d0b0d98 100644 --- a/test/model/TaskTest.java +++ b/test/model/TaskTest.java @@ -37,15 +37,6 @@ public void shouldReturnCorrectType() { assertEquals(expectedType, task1.getType()); } - @Test - public void shouldBeEqualWhenDifferentConstructorsDuration() { - Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes); - Task task2 = new Task("task2", "demo", Status.NEW, durationTenMinutes); - - assertEquals(task1.getDuration(), task2.getDuration()); - assertEquals(task1.getDuration(), durationTenMinutes); - } - @Test public void shouldBeEqualWhenDurationSet() { Task task1 = new Task("task1", "demo", Status.NEW); @@ -57,18 +48,6 @@ public void shouldBeEqualWhenDurationSet() { assertEquals(task1.getDuration(), task2.getDuration()); } - @Test - public void shouldBeEqualWhenConstructorsAndSetLocalDateTime() { - - Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); - Task task2 = new Task("task2", "demo", Status.NEW, durationTenMinutes); - - task2.setStartTime(epochTime); - - assertEquals(task1.getStartTime(), task2.getStartTime()); - assertEquals(task1.getStartTime(), epochTime); - } - @Test public void shouldBeEqualWhenReturnEndTime() { LocalDateTime expectedEndTime = epochTime.plus(durationTenMinutes); From 364fe2228889d2daa205be9f5ef85daefdfae2e6 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 05:31:15 +0300 Subject: [PATCH 37/41] fix: simplified logic --- src/util/TaskTimeController.java | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index 9399d27..ac33d0e 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -7,7 +7,6 @@ import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.TreeSet; /** @@ -122,7 +121,6 @@ public void updateEpicTimeParams(Epic epic, SubTask subTask) { * @param epic эпик, параметры которого следует обновить * @param subtask удаляемая подзадача, длительность которой следует вычесть * @param subtasks список всех оставшихся подзадач эпика после удаления - * * @implSpec Метод вызывается после удаления подзадачи * @apiNote Игнорирует подзадачи без установленных временных параметров ({@code duration} или {@code startTime}) * @implNote Время начала ищется по {@code startTime}, время окончания - по {@code endTime} @@ -139,19 +137,27 @@ public void updateEpicTimeParamsDeletion(Epic epic, SubTask subtask, List validSubtasks = subtasks.stream() .filter(subTask -> !hasMissingTimeFields(subTask)) - .max(Comparator.comparing(Task::getEndTime)) - .ifPresent( - subTask -> epic.setEndTime(subTask.getEndTime()) - ); + .toList(); + + if (!validSubtasks.isEmpty()) { + validSubtasks.stream() + .max(Comparator.comparing(Task::getEndTime)) + .ifPresent( + subTask -> epic.setEndTime(subTask.getEndTime())); + + validSubtasks.stream() + .min(Comparator.comparing(Task::getStartTime)) + .ifPresent( + subTask -> epic.setStartTime(subTask.getStartTime())); + + } else { + epic.setStartTime(null); + epic.setDuration(null); + epic.setEndTime(null); + } - subtasks.stream() - .filter(subTask -> !hasMissingTimeFields(subTask)) - .min(Comparator.comparing(Task::getStartTime)) - .ifPresent( - subTask -> epic.setStartTime(subTask.getStartTime()) - ); } From a12ea669b5fed664b65e43c76c6ef5769053d746 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 05:45:37 +0300 Subject: [PATCH 38/41] fix: simplified logic comment: typo test: add edge case --- src/managers/InMemoryTaskManager.java | 2 +- src/util/TaskTimeController.java | 8 ++------ test/util/TaskTimeControllerTest.java | 23 ++++++++++++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index fb5c579..4dac006 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -197,7 +197,7 @@ public void deleteTask(int id) { return; } historyManager.remove(id); - taskTimeController.remove(tasks.get(id)); // удаляем объект т.к это быстрее, чем удаление по id + taskTimeController.remove(tasks.get(id)); // Быстрее чем удаление по id tasks.remove(id); } diff --git a/src/util/TaskTimeController.java b/src/util/TaskTimeController.java index ac33d0e..4194747 100644 --- a/src/util/TaskTimeController.java +++ b/src/util/TaskTimeController.java @@ -65,11 +65,7 @@ public boolean isTimeOverlappingWithTreeSearch(Task task) { Task ceiling = timeSortedTasks.ceiling(task); - if (ceiling != null && ceiling.getStartTime().isBefore(task.getEndTime())) { - return true; - } - - return false; + return ceiling != null && ceiling.getStartTime().isBefore(task.getEndTime()); } /** @@ -120,7 +116,7 @@ public void updateEpicTimeParams(Epic epic, SubTask subTask) { * * @param epic эпик, параметры которого следует обновить * @param subtask удаляемая подзадача, длительность которой следует вычесть - * @param subtasks список всех оставшихся подзадач эпика после удаления + * @param subtasks список всех оставшихся подзадач эпика после удаления подзадачи * @implSpec Метод вызывается после удаления подзадачи * @apiNote Игнорирует подзадачи без установленных временных параметров ({@code duration} или {@code startTime}) * @implNote Время начала ищется по {@code startTime}, время окончания - по {@code endTime} diff --git a/test/util/TaskTimeControllerTest.java b/test/util/TaskTimeControllerTest.java index b8364dc..b8e7a3b 100644 --- a/test/util/TaskTimeControllerTest.java +++ b/test/util/TaskTimeControllerTest.java @@ -48,7 +48,7 @@ public void shouldBeTrueWhenOverlapEndAfterStart() { ttController.add(task1); long fiveMinutes = 5; - Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, epochTime.plusMinutes(-1)); assertTrue(ttController.isTimeOverlapping(task2)); } @@ -91,7 +91,6 @@ public void shouldBeNoOverlap() { assertFalse(ttController.isTimeOverlapping(task3)); } - @Test public void shouldNotAddEpic() { Epic epic = new Epic("epic", "demo", Status.NEW); @@ -423,7 +422,7 @@ public void shouldBeTrueWhenOverlapStartBeforeEndTreeSearch() { Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); ttController.add(task1); - LocalDateTime wrongStart = epochTime.plusMinutes(8); // за 2 минут до конца таска1 + LocalDateTime wrongStart = epochTime.plusMinutes(8); // за 2 минуты до конца таска1 Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, wrongStart); assertTrue(ttController.isTimeOverlappingWithTreeSearch(task2)); @@ -435,7 +434,7 @@ public void shouldBeTrueWhenOverlapEndAfterStartTreeSearch() { ttController.add(task1); long fiveMinutes = 5; - Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, epochTime.plusMinutes(-1)); assertTrue(ttController.isTimeOverlappingWithTreeSearch(task2)); } @@ -445,7 +444,7 @@ public void shouldBeTrueWhenOverlapInsideTreeSearch() { Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, epochTime); ttController.add(task1); - LocalDateTime wrongStart = epochTime.plusMinutes(1); // за 2 минут до конца таска1 + LocalDateTime wrongStart = epochTime.plusMinutes(1); // за 2 минуты до конца таска1 long fiveMinutes = 5; Task task2 = new Task("task2", "demo", Status.NEW, fiveMinutes, wrongStart); @@ -464,4 +463,18 @@ public void shouldBeNoOverlapIfNoFieldsTreeSearch() { assertFalse(ttController.isTimeOverlappingWithTreeSearch(task2)); } + @Test + public void shouldBeNoOverlapTreeSearch() { + Task task1 = new Task("task1", "demo", Status.NEW, tenMinutes, + epochTime); + Task task2 = new Task("task2", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(tenMinutes)); + Task task3 = new Task("task3", "demo", Status.NEW, tenMinutes, + epochTime.plusMinutes(-tenMinutes)); + + assertFalse(ttController.isTimeOverlappingWithTreeSearch(task1)); + assertFalse(ttController.isTimeOverlappingWithTreeSearch(task2)); + assertFalse(ttController.isTimeOverlappingWithTreeSearch(task3)); + } + } \ No newline at end of file From 11c3878f5918be1549f96f1d6052cfb7c20109f9 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 05:58:32 +0300 Subject: [PATCH 39/41] codeStyle fix --- src/managers/filedbacked/FileBackedTaskManager.java | 1 - test/model/EpicTest.java | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java index 637f7c6..7174fcc 100644 --- a/src/managers/filedbacked/FileBackedTaskManager.java +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -15,7 +15,6 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.Optional; import static managers.filedbacked.ParserHelper.*; import static util.CsvField.*; diff --git a/test/model/EpicTest.java b/test/model/EpicTest.java index 2532525..4dc9a59 100644 --- a/test/model/EpicTest.java +++ b/test/model/EpicTest.java @@ -1,13 +1,13 @@ package model; -import static org.junit.jupiter.api.Assertions.*; - import org.junit.jupiter.api.Test; import util.Status; import java.time.Duration; import java.time.LocalDateTime; +import static org.junit.jupiter.api.Assertions.*; + public class EpicTest { private final long tenMinutes = 10; From 0ec90d865fd0ae49a61518bfd7737f8b6b28c567 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 05:58:52 +0300 Subject: [PATCH 40/41] fix: get* now return unmodified version --- src/managers/InMemoryTaskManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index 4dac006..56537ee 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -92,17 +92,17 @@ public SubTask getSubTask(int id) { @Override public List getTasks() { - return new ArrayList<>(tasks.values()); + return List.copyOf(tasks.values()); } @Override public List getEpics() { - return new ArrayList<>(epics.values()); + return List.copyOf(epics.values()); } @Override public List getSubTasks() { - return new ArrayList<>(subtasks.values()); + return List.copyOf(subtasks.values()); } @Override From f7e92438e5ab2deac00f0c83fbd82e972161a1d8 Mon Sep 17 00:00:00 2001 From: Crodi Date: Thu, 4 Sep 2025 06:00:26 +0300 Subject: [PATCH 41/41] fix: get* now return unmodified version --- src/managers/FileBackedTaskManager.java | 205 ------------------------ 1 file changed, 205 deletions(-) delete mode 100644 src/managers/FileBackedTaskManager.java diff --git a/src/managers/FileBackedTaskManager.java b/src/managers/FileBackedTaskManager.java deleted file mode 100644 index 9d8971d..0000000 --- a/src/managers/FileBackedTaskManager.java +++ /dev/null @@ -1,205 +0,0 @@ -package managers; - -import util.exceptions.ManagerSaveException; -import model.*; -import util.Status; -import util.Type; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; - -import static util.Type.*; - -public class FileBackedTaskManager extends InMemoryTaskManager { - - private final File filename; - - public FileBackedTaskManager(File filename) { - this.filename = filename; - } - - public static FileBackedTaskManager loadFromFile(File filename) { - - if (!filename.exists()) { - /* Если файла не существует, то возвращается пустой менеджер - * с возможностью создать файл и записывать в него*/ - return new FileBackedTaskManager(filename); - } - - FileBackedTaskManager manager = new FileBackedTaskManager(filename); - - try (BufferedReader reader = new BufferedReader(new FileReader(filename, StandardCharsets.UTF_8))) { - reader.readLine(); // пропуск первой служебной строки - - /*конечный id, который будет присвоен idCount(далее счетчик) в manager*/ - int maxId = 0; - - while (reader.ready()) { - String line = reader.readLine(); - if (line.isBlank()) { - continue; - } - String[] parts = line.split(","); - - int id = Integer.parseInt(parts[0]); - - maxId = Math.max(maxId, id); // сохраняется максимальный id - - /* менеджер сам присваивает id, - * но не знает верного счетчика для каждого таска, - * поэтому счетчик устанавливается на id - 1 (то есть предыдущее значение), - * а уже в методе добавления счетчик увеличивается на один, устанавливая верное значение */ - manager.setIdCount(id - 1); - - Type type = Type.valueOf(parts[1]); - String title = parts[2]; - Status status = Status.valueOf(parts[3]); - String description = parts[4]; - - if (type == TASK) { - manager.addTask(new Task(title, description, status)); - } else if (type == EPIC) { - manager.addEpic(new Epic(title, description, status)); - } else if (type == SUBTASK) { - manager.addSubTask(new SubTask(title, description, status, Integer.parseInt(parts[5]))); - } else { - System.out.println("Такой формат не существует"); - } - } - /*счетчик устанавливается на максимальный найденный id, - * теперь отсчет id всех новых тасков будет от этого значения*/ - manager.setIdCount(maxId); - - } catch (IOException e) { - e.printStackTrace(); - } - - return manager; - } - - private void save() { - - try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename, StandardCharsets.UTF_8))) { - bw.write("id,type,name,status,description,epic"); //первая служебная строка файла - bw.newLine(); - - writeTasks(bw, super.getTasks()); // записываются таски - writeTasks(bw, super.getEpics()); // записываются эпики - writeSubTasks(bw, super.getSubTasks()); // записываются сабтаски - - } catch (IOException e) { - throw new ManagerSaveException("Ошибка сохранения файла", e); - } - } - - private void writeTasks(BufferedWriter bw, ArrayList tasks) throws IOException { - /* Для записи в файл таска или эпика требуются методы, - * которые реализованы в родительском классе Task, - * следовательно, можно воспользоваться дженериком - * и не создавать два одинаковых метода для тасков и эпиков*/ - - for (Task task : tasks) { - String line = String.format("%s,%s,%s,%s,%s", - task.getTaskId(), - task.getType(), - task.getTitle(), - task.getStatus(), - task.getDescription() - ); - - bw.write(line); - bw.newLine(); - } - } - - private void writeSubTasks(BufferedWriter bw, ArrayList subTasks) throws IOException { - /* Для записи сабтаска нам требуется метод getEpicId, - * поэтому метод writeSubTask отдельный*/ - for (SubTask subTask : subTasks) { - String line = String.format("%s,%s,%s,%s,%s,%s", - subTask.getTaskId(), - subTask.getType(), - subTask.getTitle(), - subTask.getStatus(), - subTask.getDescription(), - subTask.getEpicId() - ); - - bw.write(line); - bw.newLine(); - } - } - - @Override - public void addTask(Task task) { - super.addTask(task); - save(); - } - - @Override - public void addEpic(Epic epic) { - super.addEpic(epic); - save(); - } - - @Override - public void addSubTask(SubTask subTask) { - super.addSubTask(subTask); - save(); - } - - @Override - public void updateTask(Task task) { - super.updateTask(task); - save(); - } - - @Override - public void updateEpic(Epic epic) { - super.updateTask(epic); - save(); - } - - @Override - public void updateSubTask(SubTask subTask) { - super.updateSubTask(subTask); - save(); - } - - @Override - public void deleteTask(int id) { - super.deleteTask(id); - save(); - } - - @Override - public void deleteEpic(int id) { - super.deleteEpic(id); - save(); - } - - @Override - public void deleteSubTask(int id) { - super.deleteSubTask(id); - save(); - } - - @Override - public void clearTasks() { - super.clearTasks(); - save(); - } - - @Override - public void clearEpics() { - super.clearEpics(); - save(); - } - - @Override - public void clearSubTasks() { - super.clearSubTasks(); - save(); - } -}