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/FileBackedTaskManager.java b/src/managers/FileBackedTaskManager.java deleted file mode 100644 index 7ce1cc6..0000000 --- a/src/managers/FileBackedTaskManager.java +++ /dev/null @@ -1,203 +0,0 @@ -package managers; - -import managers.exceptions.ManagerSaveException; -import model.*; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; - -import static model.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(); - } -} diff --git a/src/managers/InMemoryTaskManager.java b/src/managers/InMemoryTaskManager.java index f79fb1b..56537ee 100644 --- a/src/managers/InMemoryTaskManager.java +++ b/src/managers/InMemoryTaskManager.java @@ -2,26 +2,52 @@ import managers.history.HistoryManager; import model.Epic; -import model.Status; import model.SubTask; import model.Task; +import util.Status; +import util.TaskTimeController; +import util.exceptions.TaskTimeOverlapException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +/** + * Реализация менеджера задач, хранящая данные в оперативной памяти. + * + *

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

+ * + *

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

+ * + * @implSpec Все операции работают с данными в оперативной памяти + */ 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) { + public void addTask(Task task) throws TaskTimeOverlapException { + if (taskTimeController.isTimeOverlapping(task)) { + throw new TaskTimeOverlapException("OVERLAP! Can`t add task: " + task); + } task.setTaskId(++idCount); tasks.put(task.getTaskId(), task); + taskTimeController.add(task); } @Override @@ -31,13 +57,19 @@ 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("OVERLAP! 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.updateEpicTimeParams(epic, subTask); + taskTimeController.add(subTask); } @Override @@ -59,33 +91,35 @@ public SubTask getSubTask(int id) { } @Override - public ArrayList getTasks() { - return new ArrayList<>(tasks.values()); + public List getTasks() { + return List.copyOf(tasks.values()); } @Override - public ArrayList getEpics() { - return new ArrayList<>(epics.values()); + public List getEpics() { + return List.copyOf(epics.values()); } @Override - public ArrayList getSubTasks() { - return new ArrayList<>(subtasks.values()); + public List getSubTasks() { + return List.copyOf(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 epic.getSubtaskIds().stream() + .map(subtasks::get) + .toList(); + } - return subTasksList; + @Override + public List getPrioritizedTasks() { + return taskTimeController.getPrioritizedTasks(); } @Override @@ -114,35 +148,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); } } @@ -151,83 +196,112 @@ 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); } @Override public void clearTasks() { - clearHistoryTasks(); + tasks.forEach((id, task) -> historyManager.remove(id)); tasks.clear(); + 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()) { // удаляем из основной таблицы подзадач каждую подзадачу эпика + for (Integer subtaskId : epic.getSubtaskIds()) { + historyManager.remove(subtaskId); + taskTimeController.remove(subtasks.get(subtaskId)); subtasks.remove(subtaskId); - historyManager.remove(subtaskId); // удаляем из истории } - historyManager.remove(id); // удаляем из истории - epics.remove(id); - + historyManager.remove(epicId); + epics.remove(epicId); } @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(); } + /** + * Удаляет подзадачу по идентификатору и выполняет связанные обновления. + * + *

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

    + *
  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); // удаляем из истории + historyManager.remove(id); + taskTimeController.remove(subTask); + taskTimeController.updateEpicTimeParamsDeletion(epic, subTask, getSubTasksFromEpic(epicParentId)); subtasks.remove(id); } @Override public void clearSubTasks() { - clearHistorySubTasks(); - subtasks.clear(); - for (Epic epic : epics.values()) { - epic.clearSubtasks(); // статус эпика обновляется внутри метода - } - } + subtasks.forEach((id, subTask) -> historyManager.remove(id)); - private void clearHistoryTasks() { - for (Integer id : tasks.keySet()) { - historyManager.remove(id); - } - } + // статус и временные параметры эпика обновляется внутри метода clearSubtasks() + 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() { diff --git a/src/managers/TaskManager.java b/src/managers/TaskManager.java index 9ac718e..8450f89 100644 --- a/src/managers/TaskManager.java +++ b/src/managers/TaskManager.java @@ -4,9 +4,13 @@ import model.SubTask; import model.Task; -import java.util.ArrayList; import java.util.List; +/** + * Интерфейс менеджера задач для управления задачами, эпиками и подзадачами. + * + * @apiNote Реализации могут добавлять дополнительные проверки (например, временные пересечения) + */ public interface TaskManager { void addTask(Task task); @@ -20,13 +24,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/managers/filedbacked/FileBackedTaskManager.java b/src/managers/filedbacked/FileBackedTaskManager.java new file mode 100644 index 0000000..7174fcc --- /dev/null +++ b/src/managers/filedbacked/FileBackedTaskManager.java @@ -0,0 +1,280 @@ +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.CsvField.*; +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(","); + 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()]); + 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, 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, maybeDuration, startTime)); + } + } catch (ManagerLoadException e) { + e.printStackTrace(); + } + } + + 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.updateEpic(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..6a80e9e --- /dev/null +++ b/src/managers/filedbacked/ParserHelper.java @@ -0,0 +1,103 @@ +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; +import java.util.Optional; + +/** + * Вспомогательный класс для парсинга данных задач из файла. + * + *

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

    + *
  • Парсинг целых чисел и опциональных целых чисел
  • + *
  • Парсинг типов задач {@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 Optional parseOptionalDuration(String value) throws ManagerLoadException { + if (value == null || value.equals("null") || value.trim().isEmpty()) { + return Optional.empty(); + } + + try { + return Optional.of(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/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 6f8d3bb..a737c46 100644 --- a/src/managers/history/InMemoryHistoryManager.java +++ b/src/managers/history/InMemoryHistoryManager.java @@ -6,6 +6,18 @@ 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 +49,14 @@ public List getHistory() { return getTasks(); } + /** + * Добавляет задачу в конец двусвязного списка. + * + *

Создает новый узел для задачи и добавляет его в хвост списка. + * + * @param task задача для добавления в список + * @return созданный узел, содержащий задачу + */ private Node linkLast(Task task) { Node newNode = new Node(tail, task, null); @@ -52,6 +72,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 +105,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(); diff --git a/src/model/Epic.java b/src/model/Epic.java index 64ac2c9..f451675 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,10 +1,17 @@ package model; +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 { private final ArrayList subtaskIds; + private LocalDateTime endTime; public Epic(String title, String description, Status status) { super(title, description, status); @@ -15,7 +22,6 @@ public void addSubTask(SubTask subTask) { subtaskIds.add(subTask.getTaskId()); } - public void deleteSubTask(int id) { subtaskIds.remove(Integer.valueOf(id)); } @@ -23,25 +29,97 @@ public void deleteSubTask(int id) { public void clearSubtasks() { subtaskIds.clear(); this.setStatus(Status.NEW); + this.duration = null; + this.startTime = null; + this.endTime = 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; + } + } + + /** + * Устанавливает или обновляет время конца эпика. + * + *

Устанавливает время конца эпика, если оно еще не установлено ({@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}", - 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(this.getEndTime()) ); } diff --git a/src/model/SubTask.java b/src/model/SubTask.java index f008fcd..439a05f 100644 --- a/src/model/SubTask.java +++ b/src/model/SubTask.java @@ -1,5 +1,10 @@ package model; +import util.Status; +import util.Type; + +import java.time.LocalDateTime; + public class SubTask extends Task { private final int epicId; @@ -8,6 +13,21 @@ public SubTask(String title, String description, Status status, int epicId) { this.epicId = epicId; } + public SubTask(String title, + String description, + Status status, + int epicId, + long durationInMinutes, + LocalDateTime startTime) { + super(title, description, status, durationInMinutes, startTime); + this.epicId = epicId; + } + + public SubTask(String title, String description, Status status, int epicId, long durationInMinutes) { + super(title, description, status, durationInMinutes); + this.epicId = epicId; + } + public int getEpicId() { return epicId; } @@ -18,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()) ); } } diff --git a/src/model/Task.java b/src/model/Task.java index fb40b07..7e31d1f 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,13 +1,20 @@ 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; protected Status status; - + protected Duration duration; + protected LocalDateTime startTime; public Task(String title, String description, Status status) { this.title = title; @@ -15,6 +22,21 @@ public Task(String title, String description, Status status) { this.status = status; } + public Task(String title, String description, Status status, long durationInMinutes, LocalDateTime startTime) { + this.title = title; + this.description = description; + this.status = status; + this.duration = durationInMinutes < 0 ? null : Duration.ofMinutes(durationInMinutes); + this.startTime = startTime; + } + + 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 int getTaskId() { return taskId; } @@ -47,21 +69,56 @@ 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 void setDuration(long durationInMinutes) { + this.duration = Duration.ofMinutes(durationInMinutes); + } + + public LocalDateTime getEndTime() { + if (startTime == null || duration == null) { + return null; + } + return startTime.plus(duration); + } + public Type getType() { return Type.TASK; } @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; @@ -73,4 +130,8 @@ public boolean equals(Object obj) { this.taskId == copy.taskId; } + @Override + public int compareTo(Task t) { + return this.startTime.compareTo(t.getStartTime()); + } } diff --git a/src/util/CsvField.java b/src/util/CsvField.java new file mode 100644 index 0000000..3f3b79f --- /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; + } +} 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/util/TaskTimeController.java b/src/util/TaskTimeController.java new file mode 100644 index 0000000..4194747 --- /dev/null +++ b/src/util/TaskTimeController.java @@ -0,0 +1,232 @@ +package util; + +import model.Epic; +import model.SubTask; +import model.Task; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +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); + + return ceiling != null && ceiling.getStartTime().isBefore(task.getEndTime()); + } + + /** + * Обновляет временные параметры эпика при добавлении подзадачи. + * + *

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

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

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

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

    + *
  • Длительность: Уменьшает общую длительность эпика на длительность удаляемой подзадачи + * с помощью {@link Epic#setEpicDuration(long)}.
  • + *
  • Время начала: Находит самое раннее время начала среди валидных оставшихся подзадач + * (игнорируя задачи без временных параметров) и устанавливает его как время начала эпика.
  • + *
  • Время окончания: Находит самое позднее время окончания среди валидных оставшихся подзадач + * и устанавливает его как время окончания эпика.
  • + *
  • Очистка параметров: Если не осталось валидных подзадач с временными параметрами, + * сбрасывает все временные параметры эпика в {@code null}.
  • + *
+ * + * @param epic эпик, параметры которого следует обновить + * @param subtask удаляемая подзадача, длительность которой следует вычесть + * @param subtasks список всех оставшихся подзадач эпика после удаления подзадачи + * @implSpec Метод вызывается после удаления подзадачи + * @apiNote Игнорирует подзадачи без установленных временных параметров ({@code duration} или {@code startTime}) + * @implNote Время начала ищется по {@code startTime}, время окончания - по {@code endTime} + */ + public void updateEpicTimeParamsDeletion(Epic epic, SubTask subtask, List subtasks) { + if (hasMissingTimeFields(subtask)) return; + + epic.setEpicDuration(-subtask.getDuration().toMinutes()); // в любом случае удаляем + + if (subtasks.isEmpty()) { + epic.setStartTime(null); + epic.setDuration(null); + epic.setEndTime(null); + return; + } + + List validSubtasks = subtasks.stream() + .filter(subTask -> !hasMissingTimeFields(subTask)) + .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); + } + + + } + + 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) { + if (hasMissingTimeFields(task)) { + return; + } + timeSortedTasks.remove(task); + } + + /** + * Удаляет задачу или подзадачу из отсортированной коллекции по идентификатору. + * + *

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

Не рекомендуется для общего использования - метод следует использовать только + * если нет доступа к объекту задачи и известен только {@code id}. + * + * @param id идентификатор задачи для удаления + */ + public void remove(int id) { + timeSortedTasks.removeIf(element -> element.getTaskId() == id && !hasMissingTimeFields(element)); + } + + /** + * Удаляет все задачи типа {@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 List} с задачами, отсортированными по времени + */ + public List getPrioritizedTasks() { + return List.copyOf(timeSortedTasks); + } +} 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/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/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 { diff --git a/src/util/exceptions/TaskTimeOverlapException.java b/src/util/exceptions/TaskTimeOverlapException.java new file mode 100644 index 0000000..c8d0b64 --- /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); + } +} diff --git a/test/managers/FileBackedTaskManagerTest.java b/test/managers/FileBackedTaskManagerTest.java index 778bb7a..eb4a9c7 100644 --- a/test/managers/FileBackedTaskManagerTest.java +++ b/test/managers/FileBackedTaskManagerTest.java @@ -1,14 +1,18 @@ package managers; -import managers.exceptions.ManagerSaveException; +import managers.filedbacked.FileBackedTaskManager; import model.Epic; -import model.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; @@ -94,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 540ae7e..eb1f007 100644 --- a/test/managers/InMemoryHistoryManagerTest.java +++ b/test/managers/InMemoryHistoryManagerTest.java @@ -2,10 +2,10 @@ import managers.history.HistoryManager; import model.Epic; -import model.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 dded598..174f55f 100644 --- a/test/managers/InMemoryTaskManagerTest.java +++ b/test/managers/InMemoryTaskManagerTest.java @@ -1,8 +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.*; @@ -264,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 5e365f2..4dc9a59 100644 --- a/test/model/EpicTest.java +++ b/test/model/EpicTest.java @@ -1,10 +1,20 @@ package model; -import 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; + private final Duration durationTenMinutes = Duration.ofMinutes(tenMinutes); + private final LocalDateTime epochTime = + LocalDateTime.of(1970, 1, 1, 0, 0, 0); + @Test public void shouldBeEqualWhenIdEqual() { @@ -14,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/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 { diff --git a/test/model/TaskTest.java b/test/model/TaskTest.java index 0051c12..d0b0d98 100644 --- a/test/model/TaskTest.java +++ b/test/model/TaskTest.java @@ -1,10 +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); @@ -13,7 +26,46 @@ 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 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 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..b8e7a3b --- /dev/null +++ b/test/util/TaskTimeControllerTest.java @@ -0,0 +1,480 @@ +package util; + +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.plusMinutes(-1)); + + 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 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() { + 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.plusMinutes(-1)); + + 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)); + } + + @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