From 7743828be3e4045bb4ac25e1a338e06ec0b1e6ce Mon Sep 17 00:00:00 2001 From: Sergey Date: Wed, 6 Aug 2025 12:48:19 +0300 Subject: [PATCH 01/29] feat: unlimited history list + O(1) dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * src/manager/InMemoryHistoryManager.java + Реализован двусвязный список + HashMap + add(task) теперь удаляет дубликаты через removeNode за O(1) + remove(id) — новый метод HistoryManager * src/manager/TaskManager.java + в интерфейс добавлен void remove(int id) * src/manager/InMemoryTaskManager.java + вызовы historyManager.remove(...) в removeTask / removeEpic / removeSubtask * src/manager/HistoryManager.java + объявлен метод remove(int id) * tests + HistoryManagerTest — проверяет отсутствие дублей и порядок + InMemoryTaskManagerTest — проверка очистки истории при удалении, истории > 10, дубль-просмотров + TaskManagerHistoryIntegrationTest — интеграция: удаление эпика убирает эпик и все Subtask'и из истории Issue: sprint-6 / ТЗ «неограниченная история без дублей» --- src/manager/HistoryManager.java | 9 +- src/manager/InMemoryHistoryManager.java | 61 +++++++++-- src/manager/InMemoryTaskManager.java | 100 ++++++------------ src/manager/TaskManager.java | 5 + src/test/java/manager/HistoryManagerTest.java | 61 +++++++++++ .../java/manager/InMemoryTaskManagerTest.java | 77 +++++++------- .../TaskManagerHistoryIntegrationTest.java | 37 +++++++ 7 files changed, 227 insertions(+), 123 deletions(-) create mode 100644 src/test/java/manager/HistoryManagerTest.java create mode 100644 src/test/java/manager/TaskManagerHistoryIntegrationTest.java diff --git a/src/manager/HistoryManager.java b/src/manager/HistoryManager.java index 084b9c5..c163693 100644 --- a/src/manager/HistoryManager.java +++ b/src/manager/HistoryManager.java @@ -1,9 +1,10 @@ package manager; -//Интерфейс менеджера истории просмотров задач. + import model.Task; import java.util.List; public interface HistoryManager { - void add(Task task); - List getHistory(); -} \ No newline at end of file + void add(Task task); // записать просмотр + void remove(int id); // удалить по id (нужно при удалении задач) + List getHistory();// вернуть историю в порядке просмотра +} diff --git a/src/manager/InMemoryHistoryManager.java b/src/manager/InMemoryHistoryManager.java index d45efee..8bb2cc7 100644 --- a/src/manager/InMemoryHistoryManager.java +++ b/src/manager/InMemoryHistoryManager.java @@ -2,22 +2,61 @@ import model.Task; import java.util.*; -//Хранит максимум 10 последних задач. При превышении лимита - // самая старая задача удаляется. + public class InMemoryHistoryManager implements HistoryManager { - private static final int MAX_HISTORY = 10; - private final Deque history = new ArrayDeque<>(); + /* ───── узел двусвязного списка ───── */ + private static class Node { + Task data; + Node prev; + Node next; + Node(Node prev, Task data, Node next) { + this.prev = prev; + this.data = data; + this.next = next; + } + } + + /* ───── поля ───── */ + private final Map index = new HashMap<>(); + private Node head; + private Node tail; + + /* ───── вспомогательные ───── */ + private void linkLast(Task task) { + Node oldTail = tail; + Node n = new Node(oldTail, task, null); + tail = n; + if (oldTail == null) head = n; else oldTail.next = n; + } + private void removeNode(Node n) { + if (n == null) return; + Node p = n.prev, nx = n.next; + if (p != null) p.next = nx; else head = nx; + if (nx != null) nx.prev = p; else tail = p; + } + private List getTasks() { + List list = new ArrayList<>(); + for (Node n = head; n != null; n = n.next) list.add(n.data); + return list; + } + + /* ───── интерфейс ───── */ @Override public void add(Task task) { - history.addLast(task); - if (history.size() > MAX_HISTORY) { - history.pollFirst(); - } + if (task == null) return; + Node old = index.remove(task.getId()); + removeNode(old); // убрал предыдущее вхождение + linkLast(task); // добавил новое в конец + index.put(task.getId(), tail); + } + @Override + public void remove(int id) { + Node n = index.remove(id); + removeNode(n); } -//Возвращает список просмотренных задач (в порядке просмотра). @Override public List getHistory() { - return new ArrayList<>(history); + return getTasks(); } -} \ No newline at end of file +} diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java index b299777..60019c2 100644 --- a/src/manager/InMemoryTaskManager.java +++ b/src/manager/InMemoryTaskManager.java @@ -3,21 +3,22 @@ import model.*; import java.util.*; -// InMemoryTaskManager — реализация интерфейса TaskManager, -// хранящая задачи, эпики и подзадачи в оперативной памяти. -// Поддерживает создание, обновление, удаление и получение задач всех типов, -// а также отслеживает историю просмотров через HistoryManager. +// InMemoryTaskManager хранит задачи, эпики и подзадачи в памяти +// + ведёт историю просмотров через HistoryManager. public class InMemoryTaskManager implements TaskManager { - private final Map tasks = new HashMap<>(); - private final Map epics = new HashMap<>(); + + /* ---------- хранилища ---------- */ + private final Map tasks = new HashMap<>(); + private final Map epics = new HashMap<>(); private final Map subtasks = new HashMap<>(); + + /* ---------- менеджер истории ---------- */ private final HistoryManager historyManager = Managers.getDefaultHistory(); - private int nextId = 1; - private int generateId() { - return nextId++; - } + private int nextId = 1; + private int generateId() { return nextId++; } + /* ---------- создание ---------- */ @Override public int addNewTask(Task task) { task.setId(generateId()); @@ -35,9 +36,8 @@ public int addNewEpic(Epic epic) { @Override public int addNewSubtask(Subtask subtask) { Epic epic = epics.get(subtask.getEpicId()); - if (epic == null) { - throw new IllegalArgumentException("Эпик не найден"); - } + if (epic == null) throw new IllegalArgumentException("Эпик не найден"); + int id = generateId(); subtask.setId(id); subtasks.put(id, subtask); @@ -45,39 +45,28 @@ public int addNewSubtask(Subtask subtask) { return id; } - @Override - public void updateTask(Task task) { - if (tasks.containsKey(task.getId())) { - tasks.put(task.getId(), task); - } - } - - @Override - public void updateEpic(Epic epic) { - if (epics.containsKey(epic.getId())) { - epics.put(epic.getId(), epic); - } - } - - @Override - public void updateSubtask(Subtask subtask) { - if (subtasks.containsKey(subtask.getId())) { - subtasks.put(subtask.getId(), subtask); - } - } + /* ---------- обновление ---------- */ + @Override public void updateTask(Task task) { if (tasks.containsKey(task.getId())) tasks.put(task.getId(), task); } + @Override public void updateEpic(Epic epic) { if (epics.containsKey(epic.getId())) epics.put(epic.getId(), epic); } + @Override public void updateSubtask(Subtask s) { if (subtasks.containsKey(s.getId())) subtasks.put(s.getId(), s); } + /* ---------- удаление ---------- */ @Override public void removeTask(int id) { tasks.remove(id); + historyManager.remove(id); // добавил удаляем из истории } @Override public void removeEpic(int id) { Epic epic = epics.remove(id); if (epic != null) { + // удаляем все подзадачи эпика for (int subId : epic.getSubtaskIds()) { subtasks.remove(subId); + historyManager.remove(subId); // добавил подзадача из истории } + historyManager.remove(id); // добавил сам эпик из истории } } @@ -86,47 +75,23 @@ public void removeSubtask(int id) { Subtask subtask = subtasks.remove(id); if (subtask != null) { Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - epic.getSubtaskIds().remove((Integer) id); - } + if (epic != null) epic.getSubtaskIds().remove((Integer) id); } + historyManager.remove(id); // добавил subtask из истории } + /* ---------- получение + запись в историю ---------- */ @Override - public Task getTask(int id) { - Task t = tasks.get(id); - if (t != null) historyManager.add(t); - return t; - } - - @Override - public Epic getEpic(int id) { - Epic e = epics.get(id); - if (e != null) historyManager.add(e); - return e; - } - - @Override - public Subtask getSubtask(int id) { - Subtask s = subtasks.get(id); - if (s != null) historyManager.add(s); - return s; - } - + public Task getTask(int id) { Task t = tasks.get(id); if (t != null) historyManager.add(t); return t; } @Override - public List getTasks() { - return new ArrayList<>(tasks.values()); - } - + public Epic getEpic(int id) { Epic e = epics.get(id); if (e != null) historyManager.add(e); return e; } @Override - public List getEpics() { - return new ArrayList<>(epics.values()); - } + public Subtask getSubtask(int id) { Subtask s = subtasks.get(id); if (s != null) historyManager.add(s); return s; } - @Override - public List getSubtasks() { - return new ArrayList<>(subtasks.values()); - } + /* ---------- списки ---------- */ + @Override public List getTasks() { return new ArrayList<>(tasks.values()); } + @Override public List getEpics() { return new ArrayList<>(epics.values()); } + @Override public List getSubtasks() { return new ArrayList<>(subtasks.values()); } @Override public List getEpicSubtasks(int epicId) { @@ -141,6 +106,7 @@ public List getEpicSubtasks(int epicId) { return result; } + /* ---------- история ---------- */ @Override public List getHistory() { return historyManager.getHistory(); diff --git a/src/manager/TaskManager.java b/src/manager/TaskManager.java index 6f31646..d05321e 100644 --- a/src/manager/TaskManager.java +++ b/src/manager/TaskManager.java @@ -18,10 +18,13 @@ public interface TaskManager { void updateSubtask(Subtask subtask); //Удаление задач всех типов + void removeTask(int id); void removeEpic(int id); void removeSubtask(int id); + /* ─── получение ─── */ + Task getTask(int id); Epic getEpic(int id); Subtask getSubtask(int id); @@ -31,5 +34,7 @@ public interface TaskManager { List getSubtasks(); List getEpicSubtasks(int epicId); + /* ─── история ─── */ + List getHistory(); } \ No newline at end of file diff --git a/src/test/java/manager/HistoryManagerTest.java b/src/test/java/manager/HistoryManagerTest.java new file mode 100644 index 0000000..d185c3f --- /dev/null +++ b/src/test/java/manager/HistoryManagerTest.java @@ -0,0 +1,61 @@ +package test.java.manager; + +import manager.HistoryManager; +import manager.Managers; +import model.Task; +import model.Status; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Юнит-тесты самого HistoryManager + */ +class HistoryManagerTest { + + private HistoryManager hm; + + private Task t1; + private Task t2; + private Task t3; + + @BeforeEach + void setUp() { + hm = Managers.getDefaultHistory(); // или new InMemoryHistoryManager() + + // id задаём вручную, чтобы HistoryManager.remove(id) работал корректно + t1 = new Task("T-1", "descr1", Status.NEW); t1.setId(1); + t2 = new Task("T-2", "descr2", Status.NEW); t2.setId(2); + t3 = new Task("T-3", "descr3", Status.NEW); t3.setId(3); + } + + /** add(): без дубликатов, последний просмотр переносится в конец */ + @Test + void add_movesTaskToTail_withoutDuplicates() { + hm.add(t1); + hm.add(t2); + hm.add(t3); + hm.add(t2); // повторный просмотр t2 + + List history = hm.getHistory(); + assertEquals(List.of(t1, t3, t2), history, + "Повторный просмотр должен перемещать задачу в конец истории без дублирования"); + } + + /** remove(): удаляет узел из середины за O(1) */ + @Test + void remove_deletesNodeFromAnyPosition() { + hm.add(t1); + hm.add(t2); + hm.add(t3); + + hm.remove(2); // удаляем t2 (из середины списка) + + List history = hm.getHistory(); + assertEquals(List.of(t1, t3), history, + "После удаления задачи из середины истории в списке должны остаться t1 и t3"); + } +} diff --git a/src/test/java/manager/InMemoryTaskManagerTest.java b/src/test/java/manager/InMemoryTaskManagerTest.java index aebf951..660cd50 100644 --- a/src/test/java/manager/InMemoryTaskManagerTest.java +++ b/src/test/java/manager/InMemoryTaskManagerTest.java @@ -1,7 +1,5 @@ -package test.java.manager; +package manager; -import manager.Managers; -import manager.TaskManager; import model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,56 +8,53 @@ import static org.junit.jupiter.api.Assertions.*; -// Юнит-тесты для {InMemoryTaskManager}. -// Проверяются добавление задач, история просмотров и корректная обработка ошибок. - +/** + * Юнит-тесты InMemoryTaskManager + HistoryManager. + */ class InMemoryTaskManagerTest { - private TaskManager manager; -//Создаёт новый экземпляр менеджера перед каждым тестом. + + private TaskManager tm; + @BeforeEach - void setup() { - manager = Managers.getDefault(); + void setUp() { + tm = Managers.getDefault(); // InMemoryTaskManager } -//Проверяет, что добавленная задача возвращается корректно по ID. - @Test - void shouldAddAndReturnTask() { - Task task = new Task("Test task", "Desc", Status.NEW); - int id = manager.addNewTask(task); - Task returned = manager.getTask(id); - assertNotNull(returned); - assertEquals(task.getTitle(), returned.getTitle()); - } -//Проверяет, что история просмотров сохраняет порядок и допускает повторы. + /* -------- 1. Дубликаты не сохраняются -------- */ @Test - void shouldStoreHistoryCorrectly() { - int id1 = manager.addNewTask(new Task("T1", "", Status.NEW)); - int id2 = manager.addNewTask(new Task("T2", "", Status.NEW)); + void addDuplicates_keepsOnlyLastView() { + int id = tm.addNewTask(new Task("T", "d", Status.NEW)); - manager.getTask(id1); - manager.getTask(id2); - manager.getTask(id1); + tm.getTask(id); + tm.getTask(id); + tm.getTask(id); - List history = manager.getHistory(); - assertEquals(3, history.size()); - assertEquals("T1", history.get(2).getTitle()); + List history = tm.getHistory(); + assertEquals(1, history.size(), + "В истории должен остаться единственный просмотр"); + assertEquals(id, history.get(0).getId()); } -//Проверяет, что при попытке привязать подзадачу к несуществующему эпику будет выброшено исключение. + + /* -------- 2. История может быть > 10 -------- */ @Test - void shouldThrowIfSubtaskReferencesMissingEpic() { - Subtask subtask = new Subtask("Ошибка", "Нет эпика", 999); // несуществующий epicId - assertThrows(IllegalArgumentException.class, () -> manager.addNewSubtask(subtask)); + void historyCanGrowMoreThanTen() { + for (int i = 0; i < 20; i++) { + int id = tm.addNewTask(new Task("task-" + i, "", Status.NEW)); + tm.getTask(id); + } + assertEquals(20, tm.getHistory().size(), + "История должна содержать все 20 просмотров"); } -//Проверяет, что история просмотров не превышает 10 элементов. + /* -------- 3. Удаление чистит историю -------- */ @Test - void historyShouldNotExceedTenEntries() { - for (int i = 0; i < 12; i++) { - int id = manager.addNewTask(new Task("T" + i, "", Status.NEW)); - manager.getTask(id); - } + void deletingTask_removesItFromHistory() { + int id = tm.addNewTask(new Task("X", "", Status.NEW)); + tm.getTask(id); + + tm.removeTask(id); - List history = manager.getHistory(); - assertEquals(10, history.size(), "История не должна превышать 10 элементов"); + assertTrue(tm.getHistory().isEmpty(), + "После удаления задачи записи о ней в истории быть не должно"); } } diff --git a/src/test/java/manager/TaskManagerHistoryIntegrationTest.java b/src/test/java/manager/TaskManagerHistoryIntegrationTest.java new file mode 100644 index 0000000..1a67eee --- /dev/null +++ b/src/test/java/manager/TaskManagerHistoryIntegrationTest.java @@ -0,0 +1,37 @@ +package test.java.manager; + +import manager.Managers; +import manager.TaskManager; +import model.Task; +import model.Status; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Интеграционный тест: TaskManager ↔ HistoryManager + */ +class TaskManagerHistoryIntegrationTest { + + private TaskManager tm; + + @BeforeEach + void setUp() { + tm = Managers.getDefault(); // new InMemoryTaskManager() + } + + /** Удаление задачи очищает историю */ + @Test + void deletingTask_removesItFromHistory() { + int id = tm.addNewTask(new Task("Task-1", "descr", Status.NEW)); + + tm.getTask(id); // помещаем в историю + assertEquals(1, tm.getHistory().size(), + "После просмотра история должна содержать одну запись"); + + tm.removeTask(id); // удаляем задачу + assertTrue(tm.getHistory().isEmpty(), + "После удаления задачи история должна быть пустой"); + } +} From 3646ef3b6223cfeb298514b610c738cc3e1a9517 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 7 Aug 2025 06:20:33 +0300 Subject: [PATCH 02/29] =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BB=D1=8C:=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=BD=D1=8B=D0=B5=20=D1=81=D0=BA=D0=BE=D0=B1?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20InM?= =?UTF-8?q?emoryHistoryManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/InMemoryHistoryManager.java | 72 +++++++++++++++++++------ 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/manager/InMemoryHistoryManager.java b/src/manager/InMemoryHistoryManager.java index 8bb2cc7..7ce16be 100644 --- a/src/manager/InMemoryHistoryManager.java +++ b/src/manager/InMemoryHistoryManager.java @@ -3,13 +3,15 @@ import model.Task; import java.util.*; +/** HistoryManager на базе двусвязного списка + HashMap */ public class InMemoryHistoryManager implements HistoryManager { - /* ───── узел двусвязного списка ───── */ + /* ───── узел списка ───── */ private static class Node { Task data; Node prev; Node next; + Node(Node prev, Task data, Node next) { this.prev = prev; this.data = data; @@ -23,38 +25,74 @@ private static class Node { private Node tail; /* ───── вспомогательные ───── */ + + /** добавляем просмотр в хвост */ private void linkLast(Task task) { Node oldTail = tail; - Node n = new Node(oldTail, task, null); - tail = n; - if (oldTail == null) head = n; else oldTail.next = n; + Node newNode = new Node(oldTail, task, null); // n → newNode + tail = newNode; + + if (oldTail == null) { + head = newNode; // фигурные скобки + } else { + oldTail.next = newNode; // фигурные скобки + } } - private void removeNode(Node n) { - if (n == null) return; - Node p = n.prev, nx = n.next; - if (p != null) p.next = nx; else head = nx; - if (nx != null) nx.prev = p; else tail = p; + + /** удаляем произвольный узел */ + private void removeNode(Node target) { + if (target == null) { + return; + } + + Node prev = target.prev; + Node next = target.next; + + if (prev != null) { + prev.next = next; + } else { + head = next; // фигурные скобки + } + + if (next != null) { + next.prev = prev; + } else { + tail = prev; // фигурные скобки + } } + + /** выгружаем историю списком */ private List getTasks() { List list = new ArrayList<>(); - for (Node n = head; n != null; n = n.next) list.add(n.data); + for (Node current = head; current != null; current = current.next) { + list.add(current.data); + } return list; } - /* ───── интерфейс ───── */ + /* ───── HistoryManager API ───── */ + @Override public void add(Task task) { - if (task == null) return; - Node old = index.remove(task.getId()); - removeNode(old); // убрал предыдущее вхождение - linkLast(task); // добавил новое в конец + if (task == null) { + return; // фигурные скобки + } + + /* если id уже есть — убираем старый узел */ + Node duplicate = index.remove(task.getId()); + removeNode(duplicate); + + /* вносим новый просмотр */ + linkLast(task); index.put(task.getId(), tail); } + @Override public void remove(int id) { - Node n = index.remove(id); - removeNode(n); + Node node = index.remove(id); + removeNode(node); } + @Override public List getHistory() { return getTasks(); From 587f3bce6bf45e5fb974d0be6a827266ef78646a Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 7 Aug 2025 07:27:11 +0300 Subject: [PATCH 03/29] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20IDE-=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20(.idea,=20*.iml)?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 10 ---------- .idea/libraries/junit_jupiter.xml | 17 ----------------- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ java-sprint4-hw.iml | 12 ------------ 6 files changed, 59 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/libraries/junit_jupiter.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 java-sprint4-hw.iml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 7bc07ec..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Environment-dependent path to Maven home directory -/mavenHomeManager.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/libraries/junit_jupiter.xml b/.idea/libraries/junit_jupiter.xml deleted file mode 100644 index 0725eb8..0000000 --- a/.idea/libraries/junit_jupiter.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 89ee753..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e54d3ba..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/java-sprint4-hw.iml b/java-sprint4-hw.iml deleted file mode 100644 index 547dd47..0000000 --- a/java-sprint4-hw.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file From b4b1ccc22a3a1652fc9a48fc862a606136983f67 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 7 Aug 2025 07:35:15 +0300 Subject: [PATCH 04/29] =?UTF-8?q?=D0=A7=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20.i?= =?UTF-8?q?dea:=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BD=D1=83=D0=B6?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B,=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 58 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index f68d109..28cb4f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,43 @@ ### IntelliJ IDEA ### -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ +# Игнорируем всё содержимое .idea — это папка настроек среды разработки +# Оставляем только нужные файлы, чтобы IDE могла корректно открыть проект +.idea/ +!.idea/misc.xml # Уровень JDK, базовые настройки проекта +!.idea/modules.xml # Структура модулей (если нет Maven/Gradle) +!.idea/libraries/ # Подключённые библиотеки, например JUnit +!.idea/vcs.xml # Настройки системы контроля версий (Git) -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ +# Игнорируем файлы модулей IntelliJ (.iml) — не нужны в репозитории +*.iml -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ +### Build output ### +# Исключаем папки, которые содержат скомпилированные классы и артефакты +out/ # Папка вывода сборки IntelliJ +bin/ # Папка вывода Eclipse/ручной сборки +target/ # Папка сборки Maven (если появится) + +### OS ### +# Системные файлы macOS, не должны попадать в репозиторий +.DS_Store ### VS Code ### +# Конфигурации Visual Studio Code (если кто-то откроет проект там) .vscode/ -### Mac OS ### -.DS_Store \ No newline at end of file +### Eclipse ### +# Игнорируем все файлы и папки, связанные с Eclipse IDE +.apt_generated # Автоматически сгенерированные исходники +.classpath # Файл конфигурации путей классов +.factorypath # Конфигурация аннотаций +.project # Основной файл проекта Eclipse +.settings # Папка с настройками проекта +.springBeans # Конфиги Spring Beans (если используется) +.sts4-cache # Кэш Spring Tool Suite (STS) + +### NetBeans ### +# Всё, что создаёт NetBeans IDE +nbproject/private/ # Личные настройки проекта +nbbuild/ # Папка сборки NetBeans +dist/ # Артефакты сборки (JAR и т.д.) +nbdist/ # Расширенная папка вывода +.nb-gradle/ # Кэш Gradle от NetBeans From 66a1fb65630004becf3b112db263a0383e86c502 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 7 Aug 2025 07:38:17 +0300 Subject: [PATCH 05/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=D0=B7=20.idea=20(misc.xml,=20modules.xml?= =?UTF-8?q?=20=D0=B8=20=D0=B4=D1=80.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/libraries/junit_jupiter.xml | 17 +++++++++++++++++ .idea/misc.xml | 6 ++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ 4 files changed, 37 insertions(+) create mode 100644 .idea/libraries/junit_jupiter.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/libraries/junit_jupiter.xml b/.idea/libraries/junit_jupiter.xml new file mode 100644 index 0000000..0725eb8 --- /dev/null +++ b/.idea/libraries/junit_jupiter.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..89ee753 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e54d3ba --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From aee1163188aaf566277cc8d50499e103c0639ff1 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 7 Aug 2025 20:23:54 +0300 Subject: [PATCH 06/29] =?UTF-8?q?feat:=20FileBackedTaskManager,=20TaskType?= =?UTF-8?q?=20=D0=B8=20Javadoc=20(sprint=207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/libraries/junit_jupiter.xml | 17 -- .idea/misc.xml | 6 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - out/production/java-kanban/Main$1.class | Bin 0 -> 644 bytes out/production/java-kanban/Main.class | Bin 0 -> 3014 bytes .../manager/FileBackedTaskManager$1.class | Bin 0 -> 716 bytes .../manager/FileBackedTaskManager.class | Bin 0 -> 7689 bytes .../java-kanban/manager/HistoryManager.class | Bin 0 -> 270 bytes .../manager/InMemoryHistoryManager$Node.class | Bin 0 -> 653 bytes .../manager/InMemoryHistoryManager.class | Bin 0 -> 2491 bytes .../manager/InMemoryTaskManager.class | Bin 0 -> 4962 bytes .../manager/ManagerSaveException.class | Bin 0 -> 439 bytes .../java-kanban/manager/Managers.class | Bin 0 -> 524 bytes .../java-kanban/manager/TaskManager.class | Bin 0 -> 893 bytes out/production/java-kanban/model/Epic.class | Bin 0 -> 1424 bytes out/production/java-kanban/model/Status.class | Bin 0 -> 1045 bytes .../java-kanban/model/Subtask.class | Bin 0 -> 935 bytes out/production/java-kanban/model/Task.class | Bin 0 -> 2406 bytes .../java-kanban/model/TaskType.class | Bin 0 -> 1056 bytes .../manager/HistoryManagerTest.class | Bin 0 -> 2281 bytes .../manager/InMemoryTaskManagerTest.class | Bin 0 -> 2761 bytes .../TaskManagerHistoryIntegrationTest.class | Bin 0 -> 1610 bytes src/manager/FileBackedTaskManager.java | 166 ++++++++++++++++++ src/manager/ManagerSaveException.java | 10 ++ src/model/Epic.java | 25 +-- src/model/Subtask.java | 9 +- src/model/Task.java | 34 ++-- src/model/TaskType.java | 8 + .../manager/HistoryManagerTest.java | 2 +- .../manager/InMemoryTaskManagerTest.java | 0 .../TaskManagerHistoryIntegrationTest.java | 2 +- tasks.csv | 4 + 33 files changed, 237 insertions(+), 60 deletions(-) delete mode 100644 .idea/libraries/junit_jupiter.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 out/production/java-kanban/Main$1.class create mode 100644 out/production/java-kanban/Main.class create mode 100644 out/production/java-kanban/manager/FileBackedTaskManager$1.class create mode 100644 out/production/java-kanban/manager/FileBackedTaskManager.class create mode 100644 out/production/java-kanban/manager/HistoryManager.class create mode 100644 out/production/java-kanban/manager/InMemoryHistoryManager$Node.class create mode 100644 out/production/java-kanban/manager/InMemoryHistoryManager.class create mode 100644 out/production/java-kanban/manager/InMemoryTaskManager.class create mode 100644 out/production/java-kanban/manager/ManagerSaveException.class create mode 100644 out/production/java-kanban/manager/Managers.class create mode 100644 out/production/java-kanban/manager/TaskManager.class create mode 100644 out/production/java-kanban/model/Epic.class create mode 100644 out/production/java-kanban/model/Status.class create mode 100644 out/production/java-kanban/model/Subtask.class create mode 100644 out/production/java-kanban/model/Task.class create mode 100644 out/production/java-kanban/model/TaskType.class create mode 100644 out/test/java-kanban/manager/HistoryManagerTest.class create mode 100644 out/test/java-kanban/manager/InMemoryTaskManagerTest.class create mode 100644 out/test/java-kanban/manager/TaskManagerHistoryIntegrationTest.class create mode 100644 src/manager/FileBackedTaskManager.java create mode 100644 src/manager/ManagerSaveException.java create mode 100644 src/model/TaskType.java rename src/test/{java => }/manager/HistoryManagerTest.java (98%) rename src/test/{java => }/manager/InMemoryTaskManagerTest.java (100%) rename src/test/{java => }/manager/TaskManagerHistoryIntegrationTest.java (97%) create mode 100644 tasks.csv diff --git a/.idea/libraries/junit_jupiter.xml b/.idea/libraries/junit_jupiter.xml deleted file mode 100644 index 0725eb8..0000000 --- a/.idea/libraries/junit_jupiter.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 89ee753..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e54d3ba..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/out/production/java-kanban/Main$1.class b/out/production/java-kanban/Main$1.class new file mode 100644 index 0000000000000000000000000000000000000000..5285868fb78ce576e27ed16bcc8e2619e2df1f84 GIT binary patch literal 644 zcmY*WT~E|N6g|V1c3TQ`0o?^ZK&iN^hK(l12aPXKbmMNVWd8Qjx!EL1ltcv0pI z<{4L|prq069Q6M^dv2o++k)d_1}-TFTqgcwhNd5VkVQNh@b`XI;Quk3I_TNdZO+9! zJce2r>@&>$@2baHaIuIb1}n|RGU1A(?M~0ab%vE=e#X0sCr8~djmmhkBZV3VS(awn zy5(Y7ThH{u!?(M`SGz$JY5sPahhbphHiLI%G#nj^xL~Nih?Pua@v`c-N9rxw z1;cz_CL%0PM21K=w@5Mg`+h(#82|tP literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/Main.class b/out/production/java-kanban/Main.class new file mode 100644 index 0000000000000000000000000000000000000000..e56228d1aec1720f009cae74e800385fa521e895 GIT binary patch literal 3014 zcma)8>wgo+6+I)%T4`Cp5nyn@i2^1_R#*^(UdnLn#&9Ira5<_MpQ*dW6|A9WHa})$V$$wMhrk;9 zvR$@6v&*%Y?Q&?hA3=d``#t-XU8zm+%}u*v&xGu`rc)(=_1K`GQ^Q7V642ZUQ}G*`!^uUsDXU=S#^WnkmW+=KuR=HOP;jS)5bhFKw*o$5 z)5pe!j8hVuz}j#<2Kg9|Bou6Gq92$vNB!8&XZsV0)V?8+?-1C$40-Jh*TF8U*oue* z(JQdydx3Dn{(**_=xatN*KUbUK049u5?EaaT^g~R^!!j@z5Twu!1OHg?>v_Y_Ol8 z2%A!iORonsJcvWCSA&J7m&1ef8qn|%9wuF!t7YA$V`Jq25_nX>V;YheT(XB!XNs1Q zr4;ie?uHGnxXgU)aca$?;tV|-_v45_w7myAx-?VLf!h=NfKV=%bJ;tUNJ zMm(Vxhm6zYiG}7#IbvMH1WW;M&M~iV%-vF+$R0P?-tEOuIB|GiX!}@1b*jj9k~J?0 z<8_1fqH2FgYRlyDd~M1x?A*CiRgpzbL0-cop5;P=x=F@XN$s4OG=}sngAFx==I-9* z1wxjTq6P~kfp!i}j5!bzDo)|Ff-@R^jpqbzTdXySoMo_T67xwtQ!NgsGix)_<49m1KjAvs_E#W z-P%A~_iYWTxy}=KRl{qzz{A0j8ROr-g36A!0CG>0czwF1YGo4NAiPEa^y4BWvb@^( z@dwuU)=f70-0TxKvbeLcH*?DV8gnbzpUpg!5-An$2#;W^H!Ks_`Jn3e5l_|(W%o|H|(##n{ zrR<1NI4UQncQQY!XNL8HDWB^`uQg%v-18+R4=kD9tRv^naBCv!NsJ8hy{zq|w6!|Y zsu#vN%eoZ?k+Aw{CyUt%2Ok%0Gsl<#k#KrEHu$`UGGhWpdHhd0p(-FYU(wO0g88K- z(>aGpTq<8Gj2aJ_5^g{u>XZ55?i@0T)}SHDV3=AWmoo~7GJ3IS6mb`o?cs+F-~~4$ ze7ygIPcgQFZF}z=#HZ|daE*8Q8VKN@ylbul{)K-#wvX{2S~ysorJq9ER~0D9=sZ@Z zgKLAgO=I00x~kaXK|hUK)4`tL-E#<65%u5zEwOZPcW_Vl9PX`RuLt*eqg$%j@4>NX zcNIUD?FMfZhh?Y!Vj7PGhi373v8&nI0^C^z9r zw{`g{$fVNVF5fH)6`V|^+hzCpRN5!ovlaXG3)qY+JPtp=9rze`;yOQkZqT;CuaGaX&0+5VIl6#7 z_yqrDk6ht@D6{p_>r+gV$u2T`9iNf;C@y1$y*93R9<%IuF-PA`@O+07d}CTgwlZrQd%wiitMCP1FS8euy*JCj-^)c^h-=saJ2*H@P?44-Z*;n-pwn@Yu7y9P75)IcW(thxCS-& z(KWOO|F#%JCq)SK@gE~QyRa9#`8m_q5=@+l-b5eCfQt5>fY~dbyT2t~7DfERmUM2QzRPjK+Sm&fxxCQyZRhFpmWWD^5Zgam7%mRGSuV zFf1SQGhS0X>eo84KMDprQmC+zBypnE%MO-u_3kTwR}(iK+|q>QzunfHh1*ngEV9=- z7D2{Pd=V%a$?Rob>wx^#sZ$IyEg6ZVPLw<12E?&!`{t+gI53 z<53a_-6-<>&wf4BCa6Fo3RIlRsNWXZAPy~58CL$=X>!{UY4#>g$+**uB9YV;Pg9Y) zYc#PHnmlvMdS*H!W)*7`?-2Tc0%gF{FJPD7>~mP3^*42aS!3@bjNKeA5}X{vWrAi7 zKhg6N7O;rxDB~VBC~o30o=_~{E(x2t{4+WObsz9(&B8pi=tleZ1_SH)g$;TRLKWJ# Pa;Ad&6z$wMC2sL2ffbv> literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/manager/FileBackedTaskManager.class b/out/production/java-kanban/manager/FileBackedTaskManager.class new file mode 100644 index 0000000000000000000000000000000000000000..2c52df7f802a8a25770b5bfd6d1a99d6ede1554b GIT binary patch literal 7689 zcmb7J31D2+bv<`9qdyvd+p^?sY-7t<7)c{rpnwgMZNSFD0xiapEe0o;Z|Tb#Tbhw) zMqbj8vW7M|ZAio76cQ5%N-1e=gEkA>Bxz}zwq#Vg(3O(3O}BJQHz>qC_y0fA$QqG0 zSkn9N-*?}A_nv$1{V(77@wp2CZk1FBJ`@=6n+Tv#u)NQTTLp?VYz`7Ca%Cr!D?r{6M{i zv4jO#oU^s=KWKNR34+@Mp}s_y-CLt4&{H*h2B`cd5GroxO>a zZQ$-K2Az%Ddyy(JCHH3U$7#}2_A#ASx4+ii6E%BCi84&sBGe)%&VExneohpi3cW5lubOq!6Os* zbLxC%bmBpsdYBJmneh-TbSqyR5LD$Fc1fLPC%7#|?YX;qyUn>Zgw;he8JA0a~{&Gi8xn@N>X-44N+ZaLve#XSl z;-BTq@a2rM^GOZBq%RIO<2Qe1q)#IN92>8T@D?||Lftv+e=5SarJ)`BwvsW$LR7KqMUt#WPl z;Q>3|$xRwCWMWu132+nLpaG*M#?%gnyklwQk_L>Mcn#-WTUb?DQV7e*@VYY$?nMk5 zc%6mZjjKDa>%V2<+p2UPx4Cty7U_Y0-NYOC4W9d8A{G~H&n1FfYwNv=IO7=ksrNX3 z%fxr^+dNvTzc)tyugKNxWm;opuGY!)`(3^|LViKKT=@|TJYO65<7}7WIHcKPc23f92ydfX{r+c4 zuYWGMK99y6diBiCqSFbm!?X=}$HZUaUG<^@gkd7uSomAW@zGm_0q|pEMOs3A5~NarId6*x*T#!_-JM% zb0KrqB~6Xpkgj+xC`*_YnZX=F&3rZUR8YcX1&XuKXU@@phm>oJa8Z_Jj!HVSUz$xd8I)JrFwGX0Z)uYLBhT&LyqiF`Si8QsZ`%>Jy=y-u4`GBaJd z4~cZ=jLNy90xTPA3yyNu0-XTi$LmT&UT>$qSGmCYvL zxJ?CC&!Qn)#Z{(Ijdl1@zrA?2M#78f$x%Onkf+MfkumaVaZZq&7D;o_tFQs*v(1n_ zgi4>K)p2n}9!-_{-bx;zul#BT{o;LJQ-7!}F_7%E-4t4uH&fPYfn##E@S~g;1%#n` zeQP3-PNkDpf3uzLNpz)x!d|2Ldnd=snBm&nKcL+db6p8y+~JiAW#o&~+Wt>GT306X z3#BI{F*#_+AtsKnt1F+XrLp*t#39@Jm~?Zg!RqAa-f_Xxxi!TTbj{eoFM57smp3I6 zhX(rVXTmCHX4*o>w>dApl9%hwY(JSJ<*C?a_4c;$bCiKL8{=_1xvkgY$5hIYgy5h8Navh%<_*UPCszxAHks%mo_~eu8 z`FkD;_`3vtl%j~$VFRBQyV9~zZs3zPoT`;|M48spc?Z|2&iUgAMyYphQ)C#$k<*;Y zRlJl+!%(To2Yc0x`nR0k@s+QQqMhX zjyB0=jvnV|i`?Yw`wh8SZlR;UjXAPawsAxUyYZD;ozkoh6AIxudeVc9tal>-1H-_ju`vu>TFL)h(YG#LBvW zJ}&eg&AP&HKzG|44ltq%>ikYfpA7porjap)pc?IgFsvg7g{^ggs&HTgPj?Jn`aYpK zn_Pn@_l(M|n_aHmlRKqyilth-h*yZAa}I{0#LAnPAeQ_NmhRydDNxOTt!G}>&@0za zqL%+%ha0c~+xV^A!qILb=Tp23Y+jewova))Lct_w@~9vc#G(GhZn{Mufb~2 zARWBzQkLka_T6Z4a92o~ZrLskWSv?(Be%&8V&E+zP|@bchcFye2IMM^6sZAe=1B1w zP^5TVQ%W6;ayz5BTsF%c9Qm+Du9qf`3b+C&9u+BikygBcK|9njjxaXnpGHFgbC=b2|pQ3#32QZwxST%D%lH`4# zM~)LblyX>m$qP-SA42uT=BjU^{UnO3h`bgu#4nBF<))I;#f9f_Hd;WTS2@uZ^;fq= z1AH23ix%?FxnW#riy9n#vn?8ow2k7|lwn=MsN$YL6CKjbP_#1SJ4q3{P|5qjjSS5e z>>+`4aCVmyuqxWLgdMX>3bf7%gr?1%awkK_qK-z{#gWf(;vC3_SVeE~AGhMQkw?+5 z@pwhtdaSDCv~L){*%T?L9mVf#j`;cK_gW%>+B$!vkmEn73q*`sCuMo$Y`T)!rt zj@R;M-#Ffmij3e-E{>xi8uo{oc_aAKIwNeX7{*_OjX~Toj#lT*2)^gd|8>|*~;2;X7e7KAMp8|OE8N@v-m;107_cB>5 zCd+rZrQ^~%*RX8K{+2SdY&=4i&0w63#G49 z&QI%}l&D<6FjU}ixtnu^PQ=zAaEX8*NRJz`ml=D)iQYvGAd12(dkA_e=e?8%)Lq;_ zS<|dyk$Pq`arVzEUgmRd3BQlL{%MWBb1=<6%1`zNZrx!oLK9qrMK8$m>3=%E{)f5t z5$gZUEc&&YP1n!svV8ha<=6ih^`D^r&&{G=gD_n`Kl?rXTj(Dj&l^(z{F}OGs&its z&-wuUH_R+*WxrMBi+33Lbouk?dh#Q670Jit6P~WOi2)_#w05iGSQsr8 zV?>IyZBQhGk{P zl(JD-)pAzWkmzd1q*8EBs)1ptnL^B!PO?A4PU*|6V_zZfonp;>4tEmI`|tv3n1OC*Wng0f`GK8*159%=Z~^7HfwD{tJU|vNn8gPGjhR8b literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/manager/InMemoryHistoryManager$Node.class b/out/production/java-kanban/manager/InMemoryHistoryManager$Node.class new file mode 100644 index 0000000000000000000000000000000000000000..d2e5f6e90d99efbf9895c216c3b6df9e92038b29 GIT binary patch literal 653 zcma)3%TB^T6g@-Bt5%*BAE1(`3DieFK;s5uAOT!RNL&su!D4AmTZn#^3u5BJ5AdUm zcM1y^#;}+a=1IT?gkSv3>gwvwrhvi z3`(uuPa}nN5}J+;=nO|w?((tlt(MynQ_p{G+CfOrS%#0h-bgU0&%T&5oV3@-zLW|X z9a-e4aKuB-ptq;Q<5)c&JVoyX9Yraxy5fa8e5kF({F`O1f_k4M8qpWBZQB*y*>oWM z9v?XLsY2Tua;MLITi#beHGH%K2JPOP`9pDQ%f#~E-dsqh8B$#lgiS99lh|iC_}?M2 zqUE~6Z#X;%1PQ6qU{uMn7>!e*MZVDvIi3i~CqhOm4uf!uP7Yan9YXjKWY01 z{90#3YMtryQ=RE==`ZMvZO^?~NGeRr5cb}^``q)K=RD`$-~amg7XUY~5km-J0}&HZ z91=)8lAAK;NOw7Rd+Cu?^#u;i*skr*3WSrXih&M+ULDl%Z6|k2)>lfBCsYa5 zt&?frGcl?cTjw^xtF=65qk`$&UeJNnJ z^NJ&ljDf6)Nt_nwZs8~hHc-}T0t3mW-fd0Jr|!mZ24@XSnK*~@0-9v8rJQH0)xpysx4U%k&j~r9~zi7aT!+xx>`Kh zej3iBT{bO&iT#+i;WZtak5T7qCa&WKc{leeI|w8X*4k7>A>1_aF>VQTIJUc5luEB2 z?r(KHRZ+Y@G4Uyi)MBH-h>QC$+)J=sgEr1(6SwghGg4mVtGYm6GKkfOOLdK=zC$5* zO#Sv5>YTHeUFkPGmRo3M)>|p$OdF9Mw!oWMhZg=v%Qf?JZT6j_r|rPe^C2vEkTN>9hU_EnN`^**p)T zz;{;*6(iH2H#Fhgdc&()H*F=@k^Qf~)5`P$@v>F-c|B5K3TPHw*Yd7AvR=3N8d1XF z7|j`iz~>0_PUO|f`5Z6r5a#*M+eOcLTXHn`jrTa8U+`^~I|Ro>dK)7B3dZ6#I=2ve zrgv2MRJXs{6~Pb|I5V5^_!5gch9@Gx`kuHEuANFhN22r!{fpU**v4QvJ-LNM_BG;a zGW--BS&kzrMHni@DB*#l+&+c`iy6QP3}c+L3H~K@+H?~-NXsO+%P*>wh$g|9I94+xNQqKN{xF$w|n&>1KCbL^O zsunmVT8vLqa3D*ZNisM?k_RS~=k_*8xSfn%8C#48+U%f^e;e7M9C1~b{`IQbH(5EeKa zDLwuBkIavZjA*F`Nf)?=4hqssNk%x1&n`!A6X;aBvR(%Bcdn&#?9 zZC{m>UZN1jLQDImTS_db%wq1)DRU%Pp>_+nNXp8jsuU*M)p|N`%P_xKx;#ak){m0q zWjy3lsd_(9bx2V-vWH#kP;_hjdzA1#3x3d+b7(i`5SFn*;9ed!TjvybwA;WOzX+y` treC0>`V-RqQRfoWxpmT_0buhy*O+)!M>|;K7-4)rT+U*87RNeb{{RZV5g7mg literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/manager/InMemoryTaskManager.class b/out/production/java-kanban/manager/InMemoryTaskManager.class new file mode 100644 index 0000000000000000000000000000000000000000..0d23e6bba181eaebd5ed72e078ebf3197936a1de GIT binary patch literal 4962 zcmb7H`CnAm75~0r;KA^aO{hqSfYHE!l4wHJu|#o+ghf*ngNY`O;W>^S20Dw;wlQ6k z?)#SRyIqpjwt|k<*p>9tU&?R&U-D^_e$Tz{y?LVyX@7xt?>*;y&+%y}oCGf=fRnNH@03{>>=PMN4S(5Qs+xnyeK zh@Cw*ZZAY&qM`=1uuRlhSciH8?Q?e8PB@u?k@UDTH=nt5+|IslT<;mE%yE@9P(M~Q zMnf?%(rBScEL1uR$#_~+io$73UMkz+{E^IK-W?>`R>9&XE+Na^hgp)hyoVD|* z+>vB9M|=YtdwRz_Nq5s#P`Diy?#Ctr>&|Hf_sqbCwY0h{bYnB~ly)xWMrI8dBN4j1 z#lluRXdses(oV+CIda4l@~{_C6MYu?v5n_*^RrHBK$63VvyL(m?iuNw62Sor4~dmZ zMl&)S!4B*+@rZ>-G3eDn6>B7&bC_!b<^?;IcaEMl&`KLRxj~6oEp}nIi9Hr#*lVD! zB>nVoAIrCSBE;_937U6>A{dh39`oZm#1hKpKm@}$VB(;KLlRJ(4trA9(?AUa z6xoqkaqw{qM=(NfxNBhRD&DMuZDQ0wcagv&sg#qjQ~NWC{G5}{9l98I7IMk?bT!6F zg_YMc%s@*JdNP+urV}x#%rK%7)ue^v zQbfz1opnRe0ZdsqiKh(I#^=*HJDJXoI^~o>VVAW``wn!q$0G zskKCV#NDTb6y{8%EzDzq7)1-72w8Z}&Q7o$6yr~cM#e%GT!*Dg46OHT2S!ay=zYP$ z^TLnnWI8t`R2GLXNeZ5le0nTXrQ_1(Ed!uz9eBwc0Xd_ zqxcxJt2%_)+2Hx0CfR{`(j_kZpdXj<2_e8s2HIQ#+$|#{xQq(%?o;BE^6oPhJ}Y%# z&wE41#OLXfSGtlBSTmVSr0rZj;~01-P$!PTy@6(PTRRpEbtv}#s7PPML}lmyjN-CL zX@w7PLdm2~rk#oW+>DbsZqKB+(=;|8w^LJgCMj!oGo0h}G|=IX%vHueU5T)O6VCH` z?A=vIwm?{>Jo_pbw67%b1d6Z1>lGzO9e7G=pG~2W{7KW~k;aQrRL!h6i0WNP@6T20FJfM)lq(yY&yGHdw8?B^&a)H$8P2ZGB zrg=H!xDRv#Ewaz#+;hu6cN1fjZrCmZ3tfCOgka!voI6eOg5viV_(lui zi(KwBT}K6A108Oos~eU1R&ByHPhr-aNy;{N6g}zZ-g`U5mkLNO@8=^Nd~~j7?z{PRd=OFAr;qQve(b|Gw|bUWTaEj-QZ475FKwRm1oj2=ZEQ@Z^6HKV2v`zBYuFy_}aFVP0+6y=xLa`S$xp#3!>?Xzc z1hCrtY__Qy3b|kNA^t^xNETE9ZSd$ zhN;!u5-#zqC9GV#^7cPV{nXX&CVN2UG~zMNtNj?{JNFO{;23{*PV(zC9#_5#CkNR* zzb5OXC(ig$oN=RQ#JI~&$z44v{)0{Aq>0p<*jd9Mf8z&vl|hP;!{Tlr$l>w+W%Nqq z3^dqn6Wf;2xQ)SU;fwbqT?eBF;3$I`V=&`vuL(BRF^2J^A4D&Qp>RrO&tX4^!+sEt zs%8rn8!KiqB?hjtxS9oX^j@^P=%~vAH4}Qp`#MQ-e*qr|q5mp{f<1LYInk+=edsP9 zx&goAunRylf4v0#J+7?+?XtrgM{7f$d`IX-HCUu{YMe;v2)YDbN9uwKM(x&2L93dr z74`HxLQBV$MlA`yfm!a#-56uWY??Fg3{MqqnluT zJr`);W?6#`J_OfDXSS3== zgBg+_&TDxV9dI}m&hi&@0y8+r)p-@d5MKqyIo+goRg};ymdTaynVsNR+RU)3@htv? zKQp8@Hl%L8zfe*NrO*D#GofA+D&9hmiL-ie?0%E`x`2P9EPu9fEsS`CD`7-Lx~+gu UOM8}5VIh#{$URh8R?=_$7xaoebpQYW literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/manager/ManagerSaveException.class b/out/production/java-kanban/manager/ManagerSaveException.class new file mode 100644 index 0000000000000000000000000000000000000000..1e8422e2159a76ee781ba8adde5e8a5e4676d0a8 GIT binary patch literal 439 zcmaJ-O-sW-5Pg%TO&hEA^Pq_6O{H`^t|S}TB-Ov=N$}tg@JETW zRA@kP2WIEZym>o2pI`4E04}lV!$He~+eaH6!p@_Zi69d?4eo1Qsa#&YB=V_Jg(h^) zlvdR_VR#rVcyVQvPQylKG&RMum}D|M8hhvxc5|UcDot=Zzhg0L>be)jgCqyZo+z0BqAFNqb&YOy46&bguUoL(U9QfvMl*kEAwkwLOT(4$;GWkaWOc-kBizQ z@>+#_TC7!BY21w79W0^CLcj7(Unt|TiK0pq=a&Sa1+!wkk>^WX>gQO4??o?#Ne z)9b2N^w?%aG>1 z-n00Sl?*wfH5~WEKs7eZW=P*p>~4gXp`x>XciZc@N}w1B=!~!7O?uoFOD*2W!l##7 z5S{hue=MYiBZlLdwul)`bDr~cvwlISlCXpu4AeBETQf*5m?q@Z%49@bC{iRN-5Tu6 zkkrakhN5A%)Wtp59@Ja*)NL@N+O{>~uIC7b%0j_Sbd*~vmvYjZ*6%Y1+w$00f>+g6i<>L$t z>-2Gf0{zYn#36ra0!f7_q!nhcsW6Kzg*oIE7En}J(zva02fHf2hkb6Z>=QI*mC_anC=M+~_3+pc^zKrl?Vy={u@dwx^*~S0> literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/model/Epic.class b/out/production/java-kanban/model/Epic.class new file mode 100644 index 0000000000000000000000000000000000000000..44619fc11a2f1b9fd0065e5551a3389115cde9f7 GIT binary patch literal 1424 zcmZ`(TTc^F5dKbESX#D#74U+WRz+Gs@O~?zSgTeGm_TaKr=?x%;?k1s*2p8g;*anT zm;ez`M0odU`-hC**=?W@%qDy0%$fOS=9@X^&)=;b08cO;gb#iN0ToSX7EtHQ1>GE; zvNgL}5%5o>UkkKl8@0(0S`btaQqc-kAmklRYn2ZI%_Bz1utx=M#j|tTM{U^DN=3q1 zMyZ%QLURB(kyFqnaNKpQ+J-qiW?9-|)~MJagwY`b9%EARL@tQq=v2_9q8lgna!4;2 zd2TAzS)18p3ap^R3+kdtpkxrIa9Tl+iZeLN+V-=bUR=-x0_j(ol=M4zz^cPIFEfY= zoT?Y+qNd8GspoB@TuOR*T~Kimmjrx9K|o{_To$;nPez93Ma>+uiq(0&WT!vn^#$qO zg1Ep-=c}{p{BX9S&TnTU>TEkZ&Ua^{wj6cVYAdzn+UIC(iMqAgQf=8;cQ$DDySCzN zOZ6(%%g!pLEt+hH(2IVV@qj?r5$#CiLb!q<1;Z+?;+nv*h9pb2UeqlC<)da+^~rYv z-SJGKp@SLEBH4-?xT)ZlirctDKKGj^>1MaI0%ruAV<in#3O>46zHIZyNubDZ`GUUFl4A}1t zPK;Xbyc{M*YqAa7bJ=I79zvkCpjYyi;r5SQ6?NMiqDVY(czy)JJf3P8O=)zHEw{MC z%Ky=zlGr^s*2?y<MoP%kPpRjyij{iz{q2+Ep!={{0qG^k-kkNb|rGp4-Edq ubst{K;{9%t5po2aC71(}YBXXckesrynFAvKYo7s4qy*YRfHJyUjNW`3ua&i!$A}bvJM4d z2B}tg!!TQ&JTGxTL`ReZX7+3E4jT0rjf!dV_>2yn$K}0xt-=GdI_3~(P}iEzs;?_1 z!`$0xISmG`sfg=Xz;y4gy4GM&nh2PI?|9wPhe9#}qM0^6tbwE&&jVe_ODaI7KadE218YemNM=hOK{Z>-h&ssgovtr}$JiK?8)I_*D{+lg x=?r&HAqgiX3r7{sT}3$BIo3`QkA!@^QpTELH@ZT(_t1vT*rV^S3pBK8Hu__!<^T)Qk}S&Y8Yd$HYD;!AbLY<7bI&<5KYxAs4&VW9$%r7zAt{I<&Y%p2J;SQD zT+N+~8KQgjrwmKYBh)4hjs$X3I<2=?miV7~^A~`YF z+I~GU7`Og`{X-ArfJ&7-ek}IVr(;e&kkF}AH$+1lm zP8N~AzTpN+vnBPkLYcPfzUlNgFHp?8)%} literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/model/Task.class b/out/production/java-kanban/model/Task.class new file mode 100644 index 0000000000000000000000000000000000000000..1cab621169be0ad2e62d24c6e16e39186d15eecc GIT binary patch literal 2406 zcmaJ?+fy4=9R5zS$%e2%2$x!*EmR6gphRu!rGeU(ijCaVL~2`aOR~b2B^#3sGabhl z{|d(^pY(+XbwruY=;*6|ljHcE-6h#v>Lk18`hCCKch3I(@3TJu+{1nxA%s;Z8X|}a zB%bI;de+h_d)e*XCq~H;h%TEI)43%OPNjGH5JR7exQ2dc0`Wt;Y*^W%UOy0sIHqG6 z0?CaQNx`X^mA#yV4Qd#YnEtX+FV)PdW7-utKdd2%5!$Icy3?o&3~qR=1$RvB7{*nc z)9?b$bG2o%__RuMO7Tu%ou2zB>5E)&ZBA;K!UchlSr!oaI4)sY#f*l_cu`=uw)2XgH?jCyW6o`+9v}5#Z% zbRvWW4U4!c5Zg1HRZFiEG?EHhb0=vliw^Y}FPMi_%h=KnnTydj z5hogZhpD0Pm+|-c6vcS1R{lHkYD<$1|U^@Hkv3wPW zfvg9~d}O|xta~OHzmF5R(SxlU+OC-N4r{PVWin+OX2sZQ9PS#mqQ2{?(=O@Oj$Sk6 z*q>CKeUq+h0S)D-1!mXNSdw{d*BzH5T+UNhJQsP6>fT79Y-nFrw&JD16Hwmh8b4lQbY(aQz7b znAEG6ub5+>i6UOG8?}miCR3m(wD9lE#oI;??RzU@`rvI?eSu>M-UQZZ`oNi^yz zsDvxevDoga3f?Xk|9>m5LoY7O@QMua;3@p^&UJgdN8Ofpo7?jKWZMs_5G#D&;>aRO zVLO`n4I;B}0yVREf`QD{6C{4(d)SVl5nKf_SXBK!1Yn+i@XW2?@B=ZX< zjyu2;0dS-VejE3F@J*VMPDL}5Czxmj>6xB%(c|8ZW>8W2fpCrHan0umI-zz(aG|FY z@AL!@bb>GS1g~SGhvh^kc%~>rG%h~1X{e9+XyGI^yYlFIK0Jq$|+PI0ktirG$qWCIyW zHU+ZNtj(IwG?n=k{4Yr=J}RHYDXu@m8$f5)uLn4%e9kZ)(4P=K#6z|*diN2ZN7&^k L&Rz)y_Hh0`?E3AC literal 0 HcmV?d00001 diff --git a/out/production/java-kanban/model/TaskType.class b/out/production/java-kanban/model/TaskType.class new file mode 100644 index 0000000000000000000000000000000000000000..f5a286a3ce395e77257bac88760ed54ec40f3005 GIT binary patch literal 1056 zcmZ`&?@!ZE6g_Wk*R6Ii3No4C51g_M-5i2`*g`;<32_RsW69#DZq!liI@5I|e)CV# zkqFUf`0O8Lyzgzq4YWyn-@Wgg^X|Rx_Sf%kKLKpxnTim@M7wifyZNfseP8|f!H$AK z)}bIwwz_G)WQbSBAw`af=!gnTd2eTn2d?SRc|bMypK~6MP3V|JoMNfkW@W!@GEBX_ zET+N0w2HWn8C+*jnr_Fl8D`R%%8_+!BI9ve#?V zImedUvv=PytffcTTdvjZ7DobR;5%NcIC=sX5c&%`Zs9gVxa4?Fu+A_$Dm7&8WNHy8 z8kM*ZK{NuLFYoJkfF)k7NTbUzoz7gTmy|EI)>PgE(L zsnG?On(nXziz8aRPo>MLtQQ$Hv(xjN_6vs>5W5(s9KWKC0(B=rGbeA%@yP7h3*LNyTGcg;0uys2p0; zTM(3$xW?y#j^rifA=4K~gnxk5r9LFHF8ArLsVjZzAp*SI5{ w;_e9~;VjC+QH66)5sr3-)Cpod^MR%z_zGf_Z4xRaB~wj!V%J=fCZ@C`K%q4x4aRMHqrwOfwJmDP>F}0I zrB2VexcA0E_s zQ7@wbjS^ay)rV>%t!5V^cjuP1lqsQcP|xb-kc7HmDCvg}O1 zQ=?xtOgfKiq#*<_!z@fm$}_fYJf?##VDA1Iud8DW6ijO z5idq%jNuasdDu+y#)5?LY7L!3R;}T&Gq3ID!-Rx2>rZRX+9*G<_R5c}-SThNwqm_v zcdz`|`kU>O@*~CCq|c7^!YW$3v|2^li?kNYzgp|`_+7EKtaW;=m!FlND%OT#z2u5r zI{czorSfApe90;))^jf1V#xASc6VsyUe*Se@3Oz<$7hIpF)3pTcO@uBZZWc4U;(jR z(M^_$x}ryh^LZ_2>PD7_PFC%~=#K?8&Ed`eE022&olJc$5?zOJm3Bg&62+mXBk?AO%*ksSZU~oGLRku3Jjjpj z%Z;eFofr1j6f?^8irL4qxWnq^5sm9vZK{x&({c%QF3p)h+(@bEq?*&k*qy95mvjo+ ze$0g-?v$8Qax*Iu&fPC$O+BL}^}Nm)!`ZCCTfs?DSG;x_Z)*!iP8(HIOAK_0V~cyd5>dn4*wlIU_TiUA<@Ib-|P6J#7?nDQ!#_lAbwyQHwBPQO{6#yLk9` z)U)NC&7MbGZT6qnlKtn^V|zfnDv$2Rd;PG18r3?*LuWMfV;31ol}06yNV7-9Vi!PlCZ)1gLF% z;DI{`R}TiN2M3mI@U4xDmHY(6a0$120^=ooDsa|8sMbM*z-6@H3SYD!+YtKEgCShS zG{QFCHV21`cL;4zF-OEHt`tf;GxO|KxM;Kh3;SIDa=A>p@ZW?>!#3_Yd=C>TVY(-f vtU;oWB>G8WfFy1p%I7AATZr?q5~4dKCS4LSp0~p!oSCB}BrI~R2TT6}vgnPh literal 0 HcmV?d00001 diff --git a/out/test/java-kanban/manager/InMemoryTaskManagerTest.class b/out/test/java-kanban/manager/InMemoryTaskManagerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..e39f2c8f067640e0b41838509469bb0ceb5943d8 GIT binary patch literal 2761 zcmaJ@T~iZR7=AVZ65^^95Jgaf#Wul@_=VO6Z4p6J1GEyskG8sy6&8}+xVwR3zZ7+P zVP`tiEAKkfo8I(78Bx%Rz3xnBHh-tnY5Sbr1j1K`VfUQ(yyu+vdEV!JH-G%|auL8e zJPg4He*hISDp4h&W?Y$6Vi`prjr9+Yt0T6Ast!%p>@yPlk?3FuK~x72lCcRg-Ls0W zjH+g=*LzzMLZhmEPQ9h%Gu*p95>1v|Qi?TEbcddwv79V&@sG zzpr~p!j@#YH6B78!h(+$3GI>OG9v@Fsp+Hfbu26PM+Z0IZL|ikPsTfVS3+G0UKuQg ztfbR@>Xfia*cvIuAg1wXLYxql(T4pJ0%Wx0l^~x>NaMompo~L^dAwiHESu~}s3q@i z+<9Be#F8}A_Xv&#a7@N=v=j5nWeF9Qc1LxSoCxBigsaYvVQ0Q@uQ1~*7VbLp&V1N; z<}5m|oafG*^EyncSbI{KDSXe-4`EuL(e1Tso)tbEKXMkr&KvHhxAPmHGljb$oWlD7 z#AS5gw1lu>j>g9G)EJ*R&8C`^oEGb}EY-9%LuZ;~#hrBD&MO&Cw?`6*wMh`PAHfGQ zK13&RkTfbLLc*34=t`Si zh++?E_83Q&t>`u*S#^FV4?I&=ye`1Qz%F(QTW<<5+)^!G|Z8n=N^$^XO_mf z@X^C-SflG~5T8)fS47$eWTbGJX*XFgld7kW3c__+7?N=XS1BFM>dxkDDuTs(Er{z9 zGR{wKGG6hT!hK%{sp zOij#-#tM5(V;WkP8p8$+B+j&2s(m>pq482)x3#P~s972h>eO|E&A`*y-sJjPJgeR^ zOto7X8RJ2FShwf$xr|0RsMd`MRn1xbdS*JQSoWZ%PO({OX{ISb`Wc;9kTt8zkrb>1 zG@UDf+P%uRqrBwLFoNa{!W6BN%Z{3Gs4H$WIMD5W;Dw;Q7`H-kIeiY6chHcrVlIvCNF(YjS z@dZC4Hcvk&ZLPwc5Wd7$0emgW>l+EJ%LS_Glg5M^^Yq|VT#qv1QTVvL6&D@73oa;n zI-^>xNyC`P=ho}bYi{s1rbzla?$^ttS!@~QU8Hz-my*d0uw9XHIic&S*_9Crw*t5) zVc!Nft_(GR`x18lzbl#Za3Rc(rjKoj0u!H2jw;>%Dq3!G6yuwH2bFCvAU);42Zc|u z3~n9fQ}!G%f;4A<%2l7c`2^p>FW5GZz#KL&?-#!ifO-V5t?Z0o@r+ws6E}?V4U8?F z_a{ewT9meD*!&80S8KP`Zl6Wt3+!3IULQJyy=egvABHv_F5m!rXvKjA9A*bNF^~6} z4)_;v#)scg_Z(;E(Jcb?yFmA{<$|C49cX5Q+{Mr0ZnmE=y3m4tT5h7%1rYWED+1Vo z1T>5jLKpofkRjYG&k-kdTXz#x6#mXMFliVlruG6wObcRCgt021rho-f84X2gno3_>)8r6NO=jxM2`+pB z`rw1Cd!V&TcLtnm7%^;0_9T@|MV32vscI*cC%vL#xSL?sB=?kj4~XkbHC>G<2c52oG|bfPBIv=!@RU*oAU3Q zD=jh{DespSGZ;ipK{U>AAz$7DX3>{VgT_&)d4lc0VXcEpsvZ`(~`NAi~S?{38SUp4R60k@Dd_w! z2EnuN=eY9ia4m)^=&FJ(17F}v)&8iIiq|I8hx1EGbx>EXQ@81Pl!e3R+O1{oRqW*^ zNwQ_PW;d(0CluckrsRD=u$)aAnB);F(wz8QF03*fyVG{0Xz{A>1%;e(9G9jf#(kok z+?#9sp6l_FUAs?##%UC}qzO@2e;dQ@TXNQOTe}Za6Lkh=F`W31h-&UBR%YI9do?~Q zRIO)Jz}_}VF5|p<&B)QB(P-1r-cYNCs6P5=)sdu3lu3W4B$Jz9&xp`)i%z8m1~5mb z5gXt(z9J6znzUNve3AajEF%VxNZt(J(5d0>KfFlL Ag8%>k literal 0 HcmV?d00001 diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java new file mode 100644 index 0000000..86f1402 --- /dev/null +++ b/src/manager/FileBackedTaskManager.java @@ -0,0 +1,166 @@ +package manager; + +import java.util.List; +import java.util.ArrayList; + +import model.*; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** Менеджер с автосохранением в CSV-файл. */ +public class FileBackedTaskManager extends InMemoryTaskManager { + + private final File file; + + public FileBackedTaskManager(File file) { + super(); + this.file = file; + } + + /*───────────── фабрика ─────────────*/ + public static FileBackedTaskManager loadFromFile(File file) { + FileBackedTaskManager m = new FileBackedTaskManager(file); + m.restore(); + return m; + } + + /*───────────── сохранение ─────────────*/ + private void save() { + try (BufferedWriter w = Files.newBufferedWriter(file.toPath(), + StandardCharsets.UTF_8)) { + w.write("id,type,name,status,description,epic"); + w.newLine(); + for (Task t : getTasks()) { w.write(toCsv(t)); w.newLine(); } + for (Epic e : getEpics()) { w.write(toCsv(e)); w.newLine(); } + for (Subtask s : getSubtasks()) { w.write(toCsv(s)); w.newLine(); } + } catch (IOException ex) { + throw new ManagerSaveException("Не удалось сохранить файл", ex); + } + } + + private void restore() { + if (!file.exists()) return; + + List epics = new ArrayList<>(); + List tasks = new ArrayList<>(); + List subtasks = new ArrayList<>(); + + try (BufferedReader r = Files.newBufferedReader(file.toPath(), + StandardCharsets.UTF_8)) { + r.readLine(); // пропускаем заголовок + String line; + while ((line = r.readLine()) != null) { + if (line.isBlank()) continue; + Task t = fromCsv(line); + switch (t.getType()) { + case EPIC -> epics.add((Epic) t); + case TASK -> tasks.add(t); + case SUBTASK -> subtasks.add((Subtask) t); + } + } + } catch (IOException ex) { + throw new ManagerSaveException("Не удалось прочитать файл", ex); + } + + /* порядок важен! */ + for (Epic e : epics) super.addNewEpic(e); + for (Task t : tasks) super.addNewTask(t); + for (Subtask s : subtasks) super.addNewSubtask(s); + } + + + /*───────────── CSV ─────────────*/ + private static String toCsv(Task t) { + String epicId = ""; + String type = t.getType().name(); // ← у Subtask вернёт SUBTASK + + if (t instanceof Subtask s) { + epicId = String.valueOf(s.getEpicId()); + } + return String.join(",", + String.valueOf(t.getId()), + type, + t.getTitle(), + t.getStatus().name(), + t.getDescription(), + epicId + ); + } + + private static Task fromCsv(String csv) { + String[] p = csv.split(",", -1); + int id = Integer.parseInt(p[0]); + TaskType type = TaskType.valueOf(p[1]); + String name = p[2]; + Status status = Status.valueOf(p[3]); + String descr = p[4]; + + return switch (type) { + case TASK -> { + Task t = new Task(name, descr, status); + t.setId(id); + yield t; + } + case EPIC -> { + Epic e = new Epic(name, descr); + e.setId(id); + e.setStatus(status); + yield e; + } + case SUBTASK -> { + int epicId = Integer.parseInt(p[5]); + Subtask s = new Subtask(name, descr, epicId); + s.setId(id); + s.setStatus(status); + yield s; + } + }; + } + + /*───────────── переопределения ─────────────*/ + @Override + public int addNewTask(Task t) { // возвращаем id, как в родителе + int id = super.addNewTask(t); + save(); + return id; + } + + @Override + public int addNewEpic(Epic e) { + int id = super.addNewEpic(e); + save(); + return id; + } + + @Override + public int addNewSubtask(Subtask s) { + int id = super.addNewSubtask(s); + save(); + return id; + } + + @Override public void updateTask(Task t) { super.updateTask(t); save(); } + @Override public void updateEpic(Epic e) { super.updateEpic(e); save(); } + @Override public void updateSubtask(Subtask s) { super.updateSubtask(s); save(); } + + @Override public void removeTask(int id) { super.removeTask(id); save(); } + @Override public void removeEpic(int id) { super.removeEpic(id); save(); } + @Override public void removeSubtask(int id) { super.removeSubtask(id); save(); } + + //@Override public void clear() { super.clear(); save(); } думаю пока не нужен + + /*───────────── demo ─────────────*/ + public static void main(String[] args) { + FileBackedTaskManager m = new FileBackedTaskManager(new File("tasks.csv")); + + Epic epic = new Epic("Спринт-7", "Файл-менеджер"); + m.addNewEpic(epic); + m.addNewSubtask(new Subtask("save()", "реализовать", epic.getId())); + m.addNewTask(new Task("Читать ТЗ", "вникнуть", Status.IN_PROGRESS)); + + FileBackedTaskManager restored = FileBackedTaskManager.loadFromFile(new File("tasks.csv")); + System.out.println("♻ восстановлено задач: " + restored.getTasks().size()); + } +} diff --git a/src/manager/ManagerSaveException.java b/src/manager/ManagerSaveException.java new file mode 100644 index 0000000..2e38139 --- /dev/null +++ b/src/manager/ManagerSaveException.java @@ -0,0 +1,10 @@ +package manager; + +/** + * Наша обёртка над IOException, чтобы не менять сигнатуры интерфейса. + */ +public class ManagerSaveException extends RuntimeException { + public ManagerSaveException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/model/Epic.java b/src/model/Epic.java index 7ea6494..7ae7d31 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -2,28 +2,33 @@ import java.util.ArrayList; import java.util.List; -//Класс Epic представляет эпик — задачу, содержащую список подзадач. -// * Наследуется от Task и содержит список идентификаторов всех подзадач. + +/** Эпик — агрегирующая задача, содержит список id подзадач. */ public class Epic extends Task { + private final List subtaskIds = new ArrayList<>(); public Epic(String title, String description) { super(title, description, Status.NEW); } -//Защищает subtaskIds от внешнего изменения — инкапсуляция. -//Возвращает копию списка идентификаторов подзадач. - // * Это нужно для соблюдения принципа инкапсуляции — - // * чтобы внешний код не мог напрямую изменить внутренний список. + + /*--------------- новый метод ---------------*/ + @Override + public TaskType getType() { + return TaskType.EPIC; + } + /*-------------------------------------------*/ + + /** Возвращает копию списка id подзадач (инкапсуляция). */ public List getSubtaskIds() { - return new ArrayList<>(subtaskIds); // ✅ ВОТ ТАК инкапсуляция соблюдена + return new ArrayList<>(subtaskIds); } -//Добавляет идентификатор подзадачи к эпику. -//* Проверяет, что эпик не добавляет сам себя как подзадачу. + /** Добавляет id подзадачи к эпику. */ public void addSubtaskId(int id) { if (id == this.id) { throw new IllegalArgumentException("Эпик не может быть собственным сабтаском"); } subtaskIds.add(id); } -} \ No newline at end of file +} diff --git a/src/model/Subtask.java b/src/model/Subtask.java index eb110c6..a1ae0d3 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -11,7 +11,14 @@ public Subtask(String title, String description, int epicId) { this.epicId = epicId; } + /*--------------- новое ----------------*/ + @Override + public TaskType getType() { + return TaskType.SUBTASK; + } + /*--------------------------------------*/ + public int getEpicId() { return epicId; } -} \ No newline at end of file +} diff --git a/src/model/Task.java b/src/model/Task.java index 078802d..d603b2c 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -2,24 +2,38 @@ import java.util.Objects; +/** Базовая задача. */ public class Task { + /* поля -------------------------------------------------------- */ protected String title; protected String description; - protected int id; - protected Status status; + protected int id; + protected Status status; + /* конструктор ------------------------------------------------- */ public Task(String title, String description, Status status) { - this.title = title; + this.title = title; this.description = description; - this.status = status; + this.status = status; } - public int getId() { return id; } - public void setId(int id) { this.id = id; } - public String getTitle() { return title; } - public Status getStatus() { return status; } - public void setStatus(Status status) { this.status = status; } + /* тип задачи -------------------------------------------------- */ + public TaskType getType() { // нужен сериализации + return TaskType.TASK; + } + + /* геттеры / сеттеры ------------------------------------------ */ + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public String getTitle() { return title; } + + public String getDescription() { return description; } // ← новый геттер + + public Status getStatus() { return status; } + public void setStatus(Status status) { this.status = status; } + /* equals / hashCode / toString ------------------------------- */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -41,4 +55,4 @@ public String toString() { ", status=" + status + '}'; } -} \ No newline at end of file +} diff --git a/src/model/TaskType.java b/src/model/TaskType.java new file mode 100644 index 0000000..db78cd9 --- /dev/null +++ b/src/model/TaskType.java @@ -0,0 +1,8 @@ +package model; + +/** Тип задачи: нужен для CSV-сериализации. */ +public enum TaskType { + TASK, + EPIC, + SUBTASK +} diff --git a/src/test/java/manager/HistoryManagerTest.java b/src/test/manager/HistoryManagerTest.java similarity index 98% rename from src/test/java/manager/HistoryManagerTest.java rename to src/test/manager/HistoryManagerTest.java index d185c3f..a4e032b 100644 --- a/src/test/java/manager/HistoryManagerTest.java +++ b/src/test/manager/HistoryManagerTest.java @@ -1,4 +1,4 @@ -package test.java.manager; +package manager; import manager.HistoryManager; import manager.Managers; diff --git a/src/test/java/manager/InMemoryTaskManagerTest.java b/src/test/manager/InMemoryTaskManagerTest.java similarity index 100% rename from src/test/java/manager/InMemoryTaskManagerTest.java rename to src/test/manager/InMemoryTaskManagerTest.java diff --git a/src/test/java/manager/TaskManagerHistoryIntegrationTest.java b/src/test/manager/TaskManagerHistoryIntegrationTest.java similarity index 97% rename from src/test/java/manager/TaskManagerHistoryIntegrationTest.java rename to src/test/manager/TaskManagerHistoryIntegrationTest.java index 1a67eee..a657712 100644 --- a/src/test/java/manager/TaskManagerHistoryIntegrationTest.java +++ b/src/test/manager/TaskManagerHistoryIntegrationTest.java @@ -1,4 +1,4 @@ -package test.java.manager; +package manager; import manager.Managers; import manager.TaskManager; diff --git a/tasks.csv b/tasks.csv new file mode 100644 index 0000000..3a050ba --- /dev/null +++ b/tasks.csv @@ -0,0 +1,4 @@ +id,type,name,status,description,epic +3,TASK,Читать ТЗ,IN_PROGRESS,вникнуть, +1,EPIC,Спринт-7,NEW,Файл-менеджер, +2,SUBTASK,save(),NEW,реализовать,1 From 7417ec72d26e78a2be0496b8adca8fb105106515 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 00:25:58 +0300 Subject: [PATCH 07/29] chore: ignore demo CSV file --- .gitignore | 65 ++++++++++++++++++++++++++++++------------------------ tasks.csv | 4 ---- 2 files changed, 36 insertions(+), 33 deletions(-) delete mode 100644 tasks.csv diff --git a/.gitignore b/.gitignore index 28cb4f5..7b616d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,43 +1,50 @@ ### IntelliJ IDEA ### -# Игнорируем всё содержимое .idea — это папка настроек среды разработки -# Оставляем только нужные файлы, чтобы IDE могла корректно открыть проект +# По умолчанию игнорируем всю .idea (локальные настройки) .idea/ -!.idea/misc.xml # Уровень JDK, базовые настройки проекта -!.idea/modules.xml # Структура модулей (если нет Maven/Gradle) -!.idea/libraries/ # Подключённые библиотеки, например JUnit -!.idea/vcs.xml # Настройки системы контроля версий (Git) -# Игнорируем файлы модулей IntelliJ (.iml) — не нужны в репозитории +# Разрешаем только минимально нужные файлы проекта +!.idea/misc.xml # JDK / базовые настройки +!.idea/modules.xml # Структура модулей +!.idea/vcs.xml # Настройки VCS (Git) +# ВАЖНО: .idea/libraries НЕ коммитим, чтобы не тянуть локальные JUnit и т.п. +# (Если осознанно нужно шарить библиотеки — раскомментируй следующую строку) +# !.idea/libraries/ + +# Файлы модулей IntelliJ коммитить не надо *.iml -### Build output ### -# Исключаем папки, которые содержат скомпилированные классы и артефакты -out/ # Папка вывода сборки IntelliJ -bin/ # Папка вывода Eclipse/ручной сборки -target/ # Папка сборки Maven (если появится) +### Build output / compiled ### +out/ # IntelliJ build output +bin/ # Eclipse/ручная сборка +build/ # Gradle build (на будущее) +target/ # Maven build (на будущее) +*.class # Скомпилированные классы + +### Локальные файлы/логи ### +tasks.csv # Файл автосохранения менеджера +*.log +hs_err_pid* +replay_pid* ### OS ### -# Системные файлы macOS, не должны попадать в репозиторий -.DS_Store +.DS_Store # macOS +Thumbs.db # Windows ### VS Code ### -# Конфигурации Visual Studio Code (если кто-то откроет проект там) .vscode/ ### Eclipse ### -# Игнорируем все файлы и папки, связанные с Eclipse IDE -.apt_generated # Автоматически сгенерированные исходники -.classpath # Файл конфигурации путей классов -.factorypath # Конфигурация аннотаций -.project # Основной файл проекта Eclipse -.settings # Папка с настройками проекта -.springBeans # Конфиги Spring Beans (если используется) -.sts4-cache # Кэш Spring Tool Suite (STS) +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache ### NetBeans ### -# Всё, что создаёт NetBeans IDE -nbproject/private/ # Личные настройки проекта -nbbuild/ # Папка сборки NetBeans -dist/ # Артефакты сборки (JAR и т.д.) -nbdist/ # Расширенная папка вывода -.nb-gradle/ # Кэш Gradle от NetBeans +nbproject/private/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ diff --git a/tasks.csv b/tasks.csv deleted file mode 100644 index 3a050ba..0000000 --- a/tasks.csv +++ /dev/null @@ -1,4 +0,0 @@ -id,type,name,status,description,epic -3,TASK,Читать ТЗ,IN_PROGRESS,вникнуть, -1,EPIC,Спринт-7,NEW,Файл-менеджер, -2,SUBTASK,save(),NEW,реализовать,1 From ef16c915b3d857ca29b68cc713bed5cbcffb5918 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:12:42 +0300 Subject: [PATCH 08/29] =?UTF-8?q?chore:=20ignore=20/out=20=D0=B8=20tasks.c?= =?UTF-8?q?sv;=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D1=80=D0=B5=D0=BF=D0=BE=20=D0=B2=D1=81=D0=B5=20=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ out/production/java-kanban/Main$1.class | Bin 644 -> 0 bytes out/production/java-kanban/Main.class | Bin 3014 -> 0 bytes .../manager/FileBackedTaskManager$1.class | Bin 716 -> 0 bytes .../manager/FileBackedTaskManager.class | Bin 7689 -> 0 bytes .../java-kanban/manager/HistoryManager.class | Bin 270 -> 0 bytes .../manager/InMemoryHistoryManager$Node.class | Bin 653 -> 0 bytes .../manager/InMemoryHistoryManager.class | Bin 2491 -> 0 bytes .../manager/InMemoryTaskManager.class | Bin 4962 -> 0 bytes .../manager/ManagerSaveException.class | Bin 439 -> 0 bytes .../java-kanban/manager/Managers.class | Bin 524 -> 0 bytes .../java-kanban/manager/TaskManager.class | Bin 893 -> 0 bytes out/production/java-kanban/model/Epic.class | Bin 1424 -> 0 bytes out/production/java-kanban/model/Status.class | Bin 1045 -> 0 bytes out/production/java-kanban/model/Subtask.class | Bin 935 -> 0 bytes out/production/java-kanban/model/Task.class | Bin 2406 -> 0 bytes out/production/java-kanban/model/TaskType.class | Bin 1056 -> 0 bytes .../java-kanban/manager/HistoryManagerTest.class | Bin 2281 -> 0 bytes .../manager/InMemoryTaskManagerTest.class | Bin 2761 -> 0 bytes .../TaskManagerHistoryIntegrationTest.class | Bin 1610 -> 0 bytes 20 files changed, 2 insertions(+) delete mode 100644 out/production/java-kanban/Main$1.class delete mode 100644 out/production/java-kanban/Main.class delete mode 100644 out/production/java-kanban/manager/FileBackedTaskManager$1.class delete mode 100644 out/production/java-kanban/manager/FileBackedTaskManager.class delete mode 100644 out/production/java-kanban/manager/HistoryManager.class delete mode 100644 out/production/java-kanban/manager/InMemoryHistoryManager$Node.class delete mode 100644 out/production/java-kanban/manager/InMemoryHistoryManager.class delete mode 100644 out/production/java-kanban/manager/InMemoryTaskManager.class delete mode 100644 out/production/java-kanban/manager/ManagerSaveException.class delete mode 100644 out/production/java-kanban/manager/Managers.class delete mode 100644 out/production/java-kanban/manager/TaskManager.class delete mode 100644 out/production/java-kanban/model/Epic.class delete mode 100644 out/production/java-kanban/model/Status.class delete mode 100644 out/production/java-kanban/model/Subtask.class delete mode 100644 out/production/java-kanban/model/Task.class delete mode 100644 out/production/java-kanban/model/TaskType.class delete mode 100644 out/test/java-kanban/manager/HistoryManagerTest.class delete mode 100644 out/test/java-kanban/manager/InMemoryTaskManagerTest.class delete mode 100644 out/test/java-kanban/manager/TaskManagerHistoryIntegrationTest.class diff --git a/.gitignore b/.gitignore index 7b616d1..0dca000 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/out/ +/tasks.csv ### IntelliJ IDEA ### # По умолчанию игнорируем всю .idea (локальные настройки) .idea/ diff --git a/out/production/java-kanban/Main$1.class b/out/production/java-kanban/Main$1.class deleted file mode 100644 index 5285868fb78ce576e27ed16bcc8e2619e2df1f84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 644 zcmY*WT~E|N6g|V1c3TQ`0o?^ZK&iN^hK(l12aPXKbmMNVWd8Qjx!EL1ltcv0pI z<{4L|prq069Q6M^dv2o++k)d_1}-TFTqgcwhNd5VkVQNh@b`XI;Quk3I_TNdZO+9! zJce2r>@&>$@2baHaIuIb1}n|RGU1A(?M~0ab%vE=e#X0sCr8~djmmhkBZV3VS(awn zy5(Y7ThH{u!?(M`SGz$JY5sPahhbphHiLI%G#nj^xL~Nih?Pua@v`c-N9rxw z1;cz_CL%0PM21K=w@5Mg`+h(#82|tP diff --git a/out/production/java-kanban/Main.class b/out/production/java-kanban/Main.class deleted file mode 100644 index e56228d1aec1720f009cae74e800385fa521e895..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3014 zcma)8>wgo+6+I)%T4`Cp5nyn@i2^1_R#*^(UdnLn#&9Ira5<_MpQ*dW6|A9WHa})$V$$wMhrk;9 zvR$@6v&*%Y?Q&?hA3=d``#t-XU8zm+%}u*v&xGu`rc)(=_1K`GQ^Q7V642ZUQ}G*`!^uUsDXU=S#^WnkmW+=KuR=HOP;jS)5bhFKw*o$5 z)5pe!j8hVuz}j#<2Kg9|Bou6Gq92$vNB!8&XZsV0)V?8+?-1C$40-Jh*TF8U*oue* z(JQdydx3Dn{(**_=xatN*KUbUK049u5?EaaT^g~R^!!j@z5Twu!1OHg?>v_Y_Ol8 z2%A!iORonsJcvWCSA&J7m&1ef8qn|%9wuF!t7YA$V`Jq25_nX>V;YheT(XB!XNs1Q zr4;ie?uHGnxXgU)aca$?;tV|-_v45_w7myAx-?VLf!h=NfKV=%bJ;tUNJ zMm(Vxhm6zYiG}7#IbvMH1WW;M&M~iV%-vF+$R0P?-tEOuIB|GiX!}@1b*jj9k~J?0 z<8_1fqH2FgYRlyDd~M1x?A*CiRgpzbL0-cop5;P=x=F@XN$s4OG=}sngAFx==I-9* z1wxjTq6P~kfp!i}j5!bzDo)|Ff-@R^jpqbzTdXySoMo_T67xwtQ!NgsGix)_<49m1KjAvs_E#W z-P%A~_iYWTxy}=KRl{qzz{A0j8ROr-g36A!0CG>0czwF1YGo4NAiPEa^y4BWvb@^( z@dwuU)=f70-0TxKvbeLcH*?DV8gnbzpUpg!5-An$2#;W^H!Ks_`Jn3e5l_|(W%o|H|(##n{ zrR<1NI4UQncQQY!XNL8HDWB^`uQg%v-18+R4=kD9tRv^naBCv!NsJ8hy{zq|w6!|Y zsu#vN%eoZ?k+Aw{CyUt%2Ok%0Gsl<#k#KrEHu$`UGGhWpdHhd0p(-FYU(wO0g88K- z(>aGpTq<8Gj2aJ_5^g{u>XZ55?i@0T)}SHDV3=AWmoo~7GJ3IS6mb`o?cs+F-~~4$ ze7ygIPcgQFZF}z=#HZ|daE*8Q8VKN@ylbul{)K-#wvX{2S~ysorJq9ER~0D9=sZ@Z zgKLAgO=I00x~kaXK|hUK)4`tL-E#<65%u5zEwOZPcW_Vl9PX`RuLt*eqg$%j@4>NX zcNIUD?FMfZhh?Y!Vj7PGhi373v8&nI0^C^z9r zw{`g{$fVNVF5fH)6`V|^+hzCpRN5!ovlaXG3)qY+JPtp=9rze`;yOQkZqT;CuaGaX&0+5VIl6#7 z_yqrDk6ht@D6{p_>r+gV$u2T`9iNf;C@y1$y*93R9<%IuF-PA`@O+07d}CTgwlZrQd%wiitMCP1FS8euy*JCj-^)c^h-=saJ2*H@P?44-Z*;n-pwn@Yu7y9P75)IcW(thxCS-& z(KWOO|F#%JCq)SK@gE~QyRa9#`8m_q5=@+l-b5eCfQt5>fY~dbyT2t~7DfERmUM2QzRPjK+Sm&fxxCQyZRhFpmWWD^5Zgam7%mRGSuV zFf1SQGhS0X>eo84KMDprQmC+zBypnE%MO-u_3kTwR}(iK+|q>QzunfHh1*ngEV9=- z7D2{Pd=V%a$?Rob>wx^#sZ$IyEg6ZVPLw<12E?&!`{t+gI53 z<53a_-6-<>&wf4BCa6Fo3RIlRsNWXZAPy~58CL$=X>!{UY4#>g$+**uB9YV;Pg9Y) zYc#PHnmlvMdS*H!W)*7`?-2Tc0%gF{FJPD7>~mP3^*42aS!3@bjNKeA5}X{vWrAi7 zKhg6N7O;rxDB~VBC~o30o=_~{E(x2t{4+WObsz9(&B8pi=tleZ1_SH)g$;TRLKWJ# Pa;Ad&6z$wMC2sL2ffbv> diff --git a/out/production/java-kanban/manager/FileBackedTaskManager.class b/out/production/java-kanban/manager/FileBackedTaskManager.class deleted file mode 100644 index 2c52df7f802a8a25770b5bfd6d1a99d6ede1554b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7689 zcmb7J31D2+bv<`9qdyvd+p^?sY-7t<7)c{rpnwgMZNSFD0xiapEe0o;Z|Tb#Tbhw) zMqbj8vW7M|ZAio76cQ5%N-1e=gEkA>Bxz}zwq#Vg(3O(3O}BJQHz>qC_y0fA$QqG0 zSkn9N-*?}A_nv$1{V(77@wp2CZk1FBJ`@=6n+Tv#u)NQTTLp?VYz`7Ca%Cr!D?r{6M{i zv4jO#oU^s=KWKNR34+@Mp}s_y-CLt4&{H*h2B`cd5GroxO>a zZQ$-K2Az%Ddyy(JCHH3U$7#}2_A#ASx4+ii6E%BCi84&sBGe)%&VExneohpi3cW5lubOq!6Os* zbLxC%bmBpsdYBJmneh-TbSqyR5LD$Fc1fLPC%7#|?YX;qyUn>Zgw;he8JA0a~{&Gi8xn@N>X-44N+ZaLve#XSl z;-BTq@a2rM^GOZBq%RIO<2Qe1q)#IN92>8T@D?||Lftv+e=5SarJ)`BwvsW$LR7KqMUt#WPl z;Q>3|$xRwCWMWu132+nLpaG*M#?%gnyklwQk_L>Mcn#-WTUb?DQV7e*@VYY$?nMk5 zc%6mZjjKDa>%V2<+p2UPx4Cty7U_Y0-NYOC4W9d8A{G~H&n1FfYwNv=IO7=ksrNX3 z%fxr^+dNvTzc)tyugKNxWm;opuGY!)`(3^|LViKKT=@|TJYO65<7}7WIHcKPc23f92ydfX{r+c4 zuYWGMK99y6diBiCqSFbm!?X=}$HZUaUG<^@gkd7uSomAW@zGm_0q|pEMOs3A5~NarId6*x*T#!_-JM% zb0KrqB~6Xpkgj+xC`*_YnZX=F&3rZUR8YcX1&XuKXU@@phm>oJa8Z_Jj!HVSUz$xd8I)JrFwGX0Z)uYLBhT&LyqiF`Si8QsZ`%>Jy=y-u4`GBaJd z4~cZ=jLNy90xTPA3yyNu0-XTi$LmT&UT>$qSGmCYvL zxJ?CC&!Qn)#Z{(Ijdl1@zrA?2M#78f$x%Onkf+MfkumaVaZZq&7D;o_tFQs*v(1n_ zgi4>K)p2n}9!-_{-bx;zul#BT{o;LJQ-7!}F_7%E-4t4uH&fPYfn##E@S~g;1%#n` zeQP3-PNkDpf3uzLNpz)x!d|2Ldnd=snBm&nKcL+db6p8y+~JiAW#o&~+Wt>GT306X z3#BI{F*#_+AtsKnt1F+XrLp*t#39@Jm~?Zg!RqAa-f_Xxxi!TTbj{eoFM57smp3I6 zhX(rVXTmCHX4*o>w>dApl9%hwY(JSJ<*C?a_4c;$bCiKL8{=_1xvkgY$5hIYgy5h8Navh%<_*UPCszxAHks%mo_~eu8 z`FkD;_`3vtl%j~$VFRBQyV9~zZs3zPoT`;|M48spc?Z|2&iUgAMyYphQ)C#$k<*;Y zRlJl+!%(To2Yc0x`nR0k@s+QQqMhX zjyB0=jvnV|i`?Yw`wh8SZlR;UjXAPawsAxUyYZD;ozkoh6AIxudeVc9tal>-1H-_ju`vu>TFL)h(YG#LBvW zJ}&eg&AP&HKzG|44ltq%>ikYfpA7porjap)pc?IgFsvg7g{^ggs&HTgPj?Jn`aYpK zn_Pn@_l(M|n_aHmlRKqyilth-h*yZAa}I{0#LAnPAeQ_NmhRydDNxOTt!G}>&@0za zqL%+%ha0c~+xV^A!qILb=Tp23Y+jewova))Lct_w@~9vc#G(GhZn{Mufb~2 zARWBzQkLka_T6Z4a92o~ZrLskWSv?(Be%&8V&E+zP|@bchcFye2IMM^6sZAe=1B1w zP^5TVQ%W6;ayz5BTsF%c9Qm+Du9qf`3b+C&9u+BikygBcK|9njjxaXnpGHFgbC=b2|pQ3#32QZwxST%D%lH`4# zM~)LblyX>m$qP-SA42uT=BjU^{UnO3h`bgu#4nBF<))I;#f9f_Hd;WTS2@uZ^;fq= z1AH23ix%?FxnW#riy9n#vn?8ow2k7|lwn=MsN$YL6CKjbP_#1SJ4q3{P|5qjjSS5e z>>+`4aCVmyuqxWLgdMX>3bf7%gr?1%awkK_qK-z{#gWf(;vC3_SVeE~AGhMQkw?+5 z@pwhtdaSDCv~L){*%T?L9mVf#j`;cK_gW%>+B$!vkmEn73q*`sCuMo$Y`T)!rt zj@R;M-#Ffmij3e-E{>xi8uo{oc_aAKIwNeX7{*_OjX~Toj#lT*2)^gd|8>|*~;2;X7e7KAMp8|OE8N@v-m;107_cB>5 zCd+rZrQ^~%*RX8K{+2SdY&=4i&0w63#G49 z&QI%}l&D<6FjU}ixtnu^PQ=zAaEX8*NRJz`ml=D)iQYvGAd12(dkA_e=e?8%)Lq;_ zS<|dyk$Pq`arVzEUgmRd3BQlL{%MWBb1=<6%1`zNZrx!oLK9qrMK8$m>3=%E{)f5t z5$gZUEc&&YP1n!svV8ha<=6ih^`D^r&&{G=gD_n`Kl?rXTj(Dj&l^(z{F}OGs&its z&-wuUH_R+*WxrMBi+33Lbouk?dh#Q670Jit6P~WOi2)_#w05iGSQsr8 zV?>IyZBQhGk{P zl(JD-)pAzWkmzd1q*8EBs)1ptnL^B!PO?A4PU*|6V_zZfonp;>4tEmI`|tv3n1OC*Wng0f`GK8*159%=Z~^7HfwD{tJU|vNn8gPGjhR8b diff --git a/out/production/java-kanban/manager/InMemoryHistoryManager$Node.class b/out/production/java-kanban/manager/InMemoryHistoryManager$Node.class deleted file mode 100644 index d2e5f6e90d99efbf9895c216c3b6df9e92038b29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 653 zcma)3%TB^T6g@-Bt5%*BAE1(`3DieFK;s5uAOT!RNL&su!D4AmTZn#^3u5BJ5AdUm zcM1y^#;}+a=1IT?gkSv3>gwvwrhvi z3`(uuPa}nN5}J+;=nO|w?((tlt(MynQ_p{G+CfOrS%#0h-bgU0&%T&5oV3@-zLW|X z9a-e4aKuB-ptq;Q<5)c&JVoyX9Yraxy5fa8e5kF({F`O1f_k4M8qpWBZQB*y*>oWM z9v?XLsY2Tua;MLITi#beHGH%K2JPOP`9pDQ%f#~E-dsqh8B$#lgiS99lh|iC_}?M2 zqUE~6Z#X;%1PQ6qU{uMn7>!e*MZVDvIi3i~CqhOm4uf!uP7Yan9YXjKWY01 z{90#3YMtryQ=RE==`ZMvZO^?~NGeRr5cb}^``q)K=RD`$-~amg7XUY~5km-J0}&HZ z91=)8lAAK;NOw7Rd+Cu?^#u;i*skr*3WSrXih&M+ULDl%Z6|k2)>lfBCsYa5 zt&?frGcl?cTjw^xtF=65qk`$&UeJNnJ z^NJ&ljDf6)Nt_nwZs8~hHc-}T0t3mW-fd0Jr|!mZ24@XSnK*~@0-9v8rJQH0)xpysx4U%k&j~r9~zi7aT!+xx>`Kh zej3iBT{bO&iT#+i;WZtak5T7qCa&WKc{leeI|w8X*4k7>A>1_aF>VQTIJUc5luEB2 z?r(KHRZ+Y@G4Uyi)MBH-h>QC$+)J=sgEr1(6SwghGg4mVtGYm6GKkfOOLdK=zC$5* zO#Sv5>YTHeUFkPGmRo3M)>|p$OdF9Mw!oWMhZg=v%Qf?JZT6j_r|rPe^C2vEkTN>9hU_EnN`^**p)T zz;{;*6(iH2H#Fhgdc&()H*F=@k^Qf~)5`P$@v>F-c|B5K3TPHw*Yd7AvR=3N8d1XF z7|j`iz~>0_PUO|f`5Z6r5a#*M+eOcLTXHn`jrTa8U+`^~I|Ro>dK)7B3dZ6#I=2ve zrgv2MRJXs{6~Pb|I5V5^_!5gch9@Gx`kuHEuANFhN22r!{fpU**v4QvJ-LNM_BG;a zGW--BS&kzrMHni@DB*#l+&+c`iy6QP3}c+L3H~K@+H?~-NXsO+%P*>wh$g|9I94+xNQqKN{xF$w|n&>1KCbL^O zsunmVT8vLqa3D*ZNisM?k_RS~=k_*8xSfn%8C#48+U%f^e;e7M9C1~b{`IQbH(5EeKa zDLwuBkIavZjA*F`Nf)?=4hqssNk%x1&n`!A6X;aBvR(%Bcdn&#?9 zZC{m>UZN1jLQDImTS_db%wq1)DRU%Pp>_+nNXp8jsuU*M)p|N`%P_xKx;#ak){m0q zWjy3lsd_(9bx2V-vWH#kP;_hjdzA1#3x3d+b7(i`5SFn*;9ed!TjvybwA;WOzX+y` treC0>`V-RqQRfoWxpmT_0buhy*O+)!M>|;K7-4)rT+U*87RNeb{{RZV5g7mg diff --git a/out/production/java-kanban/manager/InMemoryTaskManager.class b/out/production/java-kanban/manager/InMemoryTaskManager.class deleted file mode 100644 index 0d23e6bba181eaebd5ed72e078ebf3197936a1de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4962 zcmb7H`CnAm75~0r;KA^aO{hqSfYHE!l4wHJu|#o+ghf*ngNY`O;W>^S20Dw;wlQ6k z?)#SRyIqpjwt|k<*p>9tU&?R&U-D^_e$Tz{y?LVyX@7xt?>*;y&+%y}oCGf=fRnNH@03{>>=PMN4S(5Qs+xnyeK zh@Cw*ZZAY&qM`=1uuRlhSciH8?Q?e8PB@u?k@UDTH=nt5+|IslT<;mE%yE@9P(M~Q zMnf?%(rBScEL1uR$#_~+io$73UMkz+{E^IK-W?>`R>9&XE+Na^hgp)hyoVD|* z+>vB9M|=YtdwRz_Nq5s#P`Diy?#Ctr>&|Hf_sqbCwY0h{bYnB~ly)xWMrI8dBN4j1 z#lluRXdses(oV+CIda4l@~{_C6MYu?v5n_*^RrHBK$63VvyL(m?iuNw62Sor4~dmZ zMl&)S!4B*+@rZ>-G3eDn6>B7&bC_!b<^?;IcaEMl&`KLRxj~6oEp}nIi9Hr#*lVD! zB>nVoAIrCSBE;_937U6>A{dh39`oZm#1hKpKm@}$VB(;KLlRJ(4trA9(?AUa z6xoqkaqw{qM=(NfxNBhRD&DMuZDQ0wcagv&sg#qjQ~NWC{G5}{9l98I7IMk?bT!6F zg_YMc%s@*JdNP+urV}x#%rK%7)ue^v zQbfz1opnRe0ZdsqiKh(I#^=*HJDJXoI^~o>VVAW``wn!q$0G zskKCV#NDTb6y{8%EzDzq7)1-72w8Z}&Q7o$6yr~cM#e%GT!*Dg46OHT2S!ay=zYP$ z^TLnnWI8t`R2GLXNeZ5le0nTXrQ_1(Ed!uz9eBwc0Xd_ zqxcxJt2%_)+2Hx0CfR{`(j_kZpdXj<2_e8s2HIQ#+$|#{xQq(%?o;BE^6oPhJ}Y%# z&wE41#OLXfSGtlBSTmVSr0rZj;~01-P$!PTy@6(PTRRpEbtv}#s7PPML}lmyjN-CL zX@w7PLdm2~rk#oW+>DbsZqKB+(=;|8w^LJgCMj!oGo0h}G|=IX%vHueU5T)O6VCH` z?A=vIwm?{>Jo_pbw67%b1d6Z1>lGzO9e7G=pG~2W{7KW~k;aQrRL!h6i0WNP@6T20FJfM)lq(yY&yGHdw8?B^&a)H$8P2ZGB zrg=H!xDRv#Ewaz#+;hu6cN1fjZrCmZ3tfCOgka!voI6eOg5viV_(lui zi(KwBT}K6A108Oos~eU1R&ByHPhr-aNy;{N6g}zZ-g`U5mkLNO@8=^Nd~~j7?z{PRd=OFAr;qQve(b|Gw|bUWTaEj-QZ475FKwRm1oj2=ZEQ@Z^6HKV2v`zBYuFy_}aFVP0+6y=xLa`S$xp#3!>?Xzc z1hCrtY__Qy3b|kNA^t^xNETE9ZSd$ zhN;!u5-#zqC9GV#^7cPV{nXX&CVN2UG~zMNtNj?{JNFO{;23{*PV(zC9#_5#CkNR* zzb5OXC(ig$oN=RQ#JI~&$z44v{)0{Aq>0p<*jd9Mf8z&vl|hP;!{Tlr$l>w+W%Nqq z3^dqn6Wf;2xQ)SU;fwbqT?eBF;3$I`V=&`vuL(BRF^2J^A4D&Qp>RrO&tX4^!+sEt zs%8rn8!KiqB?hjtxS9oX^j@^P=%~vAH4}Qp`#MQ-e*qr|q5mp{f<1LYInk+=edsP9 zx&goAunRylf4v0#J+7?+?XtrgM{7f$d`IX-HCUu{YMe;v2)YDbN9uwKM(x&2L93dr z74`HxLQBV$MlA`yfm!a#-56uWY??Fg3{MqqnluT zJr`);W?6#`J_OfDXSS3== zgBg+_&TDxV9dI}m&hi&@0y8+r)p-@d5MKqyIo+goRg};ymdTaynVsNR+RU)3@htv? zKQp8@Hl%L8zfe*NrO*D#GofA+D&9hmiL-ie?0%E`x`2P9EPu9fEsS`CD`7-Lx~+gu UOM8}5VIh#{$URh8R?=_$7xaoebpQYW diff --git a/out/production/java-kanban/manager/ManagerSaveException.class b/out/production/java-kanban/manager/ManagerSaveException.class deleted file mode 100644 index 1e8422e2159a76ee781ba8adde5e8a5e4676d0a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaJ-O-sW-5Pg%TO&hEA^Pq_6O{H`^t|S}TB-Ov=N$}tg@JETW zRA@kP2WIEZym>o2pI`4E04}lV!$He~+eaH6!p@_Zi69d?4eo1Qsa#&YB=V_Jg(h^) zlvdR_VR#rVcyVQvPQylKG&RMum}D|M8hhvxc5|UcDot=Zzhg0L>be)jgCqyZo+z0BqAFNqb&YOy46&bguUoL(U9QfvMl*kEAwkwLOT(4$;GWkaWOc-kBizQ z@>+#_TC7!BY21w79W0^CLcj7(Unt|TiK0pq=a&Sa1+!wkk>^WX>gQO4??o?#Ne z)9b2N^w?%aG>1 z-n00Sl?*wfH5~WEKs7eZW=P*p>~4gXp`x>XciZc@N}w1B=!~!7O?uoFOD*2W!l##7 z5S{hue=MYiBZlLdwul)`bDr~cvwlISlCXpu4AeBETQf*5m?q@Z%49@bC{iRN-5Tu6 zkkrakhN5A%)Wtp59@Ja*)NL@N+O{>~uIC7b%0j_Sbd*~vmvYjZ*6%Y1+w$00f>+g6i<>L$t z>-2Gf0{zYn#36ra0!f7_q!nhcsW6Kzg*oIE7En}J(zva02fHf2hkb6Z>=QI*mC_anC=M+~_3+pc^zKrl?Vy={u@dwx^*~S0> diff --git a/out/production/java-kanban/model/Epic.class b/out/production/java-kanban/model/Epic.class deleted file mode 100644 index 44619fc11a2f1b9fd0065e5551a3389115cde9f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1424 zcmZ`(TTc^F5dKbESX#D#74U+WRz+Gs@O~?zSgTeGm_TaKr=?x%;?k1s*2p8g;*anT zm;ez`M0odU`-hC**=?W@%qDy0%$fOS=9@X^&)=;b08cO;gb#iN0ToSX7EtHQ1>GE; zvNgL}5%5o>UkkKl8@0(0S`btaQqc-kAmklRYn2ZI%_Bz1utx=M#j|tTM{U^DN=3q1 zMyZ%QLURB(kyFqnaNKpQ+J-qiW?9-|)~MJagwY`b9%EARL@tQq=v2_9q8lgna!4;2 zd2TAzS)18p3ap^R3+kdtpkxrIa9Tl+iZeLN+V-=bUR=-x0_j(ol=M4zz^cPIFEfY= zoT?Y+qNd8GspoB@TuOR*T~Kimmjrx9K|o{_To$;nPez93Ma>+uiq(0&WT!vn^#$qO zg1Ep-=c}{p{BX9S&TnTU>TEkZ&Ua^{wj6cVYAdzn+UIC(iMqAgQf=8;cQ$DDySCzN zOZ6(%%g!pLEt+hH(2IVV@qj?r5$#CiLb!q<1;Z+?;+nv*h9pb2UeqlC<)da+^~rYv z-SJGKp@SLEBH4-?xT)ZlirctDKKGj^>1MaI0%ruAV<in#3O>46zHIZyNubDZ`GUUFl4A}1t zPK;Xbyc{M*YqAa7bJ=I79zvkCpjYyi;r5SQ6?NMiqDVY(czy)JJf3P8O=)zHEw{MC z%Ky=zlGr^s*2?y<MoP%kPpRjyij{iz{q2+Ep!={{0qG^k-kkNb|rGp4-Edq ubst{K;{9%t5po2aC71(}YBXXckesrynFAvKYo7s4qy*YRfHJyUjNW`3ua&i!$A}bvJM4d z2B}tg!!TQ&JTGxTL`ReZX7+3E4jT0rjf!dV_>2yn$K}0xt-=GdI_3~(P}iEzs;?_1 z!`$0xISmG`sfg=Xz;y4gy4GM&nh2PI?|9wPhe9#}qM0^6tbwE&&jVe_ODaI7KadE218YemNM=hOK{Z>-h&ssgovtr}$JiK?8)I_*D{+lg x=?r&HAqgiX3r7{sT}3$BIo3`QkA!@^QpTELH@ZT(_t1vT*rV^S3pBK8Hu__!<^T)Qk}S&Y8Yd$HYD;!AbLY<7bI&<5KYxAs4&VW9$%r7zAt{I<&Y%p2J;SQD zT+N+~8KQgjrwmKYBh)4hjs$X3I<2=?miV7~^A~`YF z+I~GU7`Og`{X-ArfJ&7-ek}IVr(;e&kkF}AH$+1lm zP8N~AzTpN+vnBPkLYcPfzUlNgFHp?8)%} diff --git a/out/production/java-kanban/model/Task.class b/out/production/java-kanban/model/Task.class deleted file mode 100644 index 1cab621169be0ad2e62d24c6e16e39186d15eecc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2406 zcmaJ?+fy4=9R5zS$%e2%2$x!*EmR6gphRu!rGeU(ijCaVL~2`aOR~b2B^#3sGabhl z{|d(^pY(+XbwruY=;*6|ljHcE-6h#v>Lk18`hCCKch3I(@3TJu+{1nxA%s;Z8X|}a zB%bI;de+h_d)e*XCq~H;h%TEI)43%OPNjGH5JR7exQ2dc0`Wt;Y*^W%UOy0sIHqG6 z0?CaQNx`X^mA#yV4Qd#YnEtX+FV)PdW7-utKdd2%5!$Icy3?o&3~qR=1$RvB7{*nc z)9?b$bG2o%__RuMO7Tu%ou2zB>5E)&ZBA;K!UchlSr!oaI4)sY#f*l_cu`=uw)2XgH?jCyW6o`+9v}5#Z% zbRvWW4U4!c5Zg1HRZFiEG?EHhb0=vliw^Y}FPMi_%h=KnnTydj z5hogZhpD0Pm+|-c6vcS1R{lHkYD<$1|U^@Hkv3wPW zfvg9~d}O|xta~OHzmF5R(SxlU+OC-N4r{PVWin+OX2sZQ9PS#mqQ2{?(=O@Oj$Sk6 z*q>CKeUq+h0S)D-1!mXNSdw{d*BzH5T+UNhJQsP6>fT79Y-nFrw&JD16Hwmh8b4lQbY(aQz7b znAEG6ub5+>i6UOG8?}miCR3m(wD9lE#oI;??RzU@`rvI?eSu>M-UQZZ`oNi^yz zsDvxevDoga3f?Xk|9>m5LoY7O@QMua;3@p^&UJgdN8Ofpo7?jKWZMs_5G#D&;>aRO zVLO`n4I;B}0yVREf`QD{6C{4(d)SVl5nKf_SXBK!1Yn+i@XW2?@B=ZX< zjyu2;0dS-VejE3F@J*VMPDL}5Czxmj>6xB%(c|8ZW>8W2fpCrHan0umI-zz(aG|FY z@AL!@bb>GS1g~SGhvh^kc%~>rG%h~1X{e9+XyGI^yYlFIK0Jq$|+PI0ktirG$qWCIyW zHU+ZNtj(IwG?n=k{4Yr=J}RHYDXu@m8$f5)uLn4%e9kZ)(4P=K#6z|*diN2ZN7&^k L&Rz)y_Hh0`?E3AC diff --git a/out/production/java-kanban/model/TaskType.class b/out/production/java-kanban/model/TaskType.class deleted file mode 100644 index f5a286a3ce395e77257bac88760ed54ec40f3005..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1056 zcmZ`&?@!ZE6g_Wk*R6Ii3No4C51g_M-5i2`*g`;<32_RsW69#DZq!liI@5I|e)CV# zkqFUf`0O8Lyzgzq4YWyn-@Wgg^X|Rx_Sf%kKLKpxnTim@M7wifyZNfseP8|f!H$AK z)}bIwwz_G)WQbSBAw`af=!gnTd2eTn2d?SRc|bMypK~6MP3V|JoMNfkW@W!@GEBX_ zET+N0w2HWn8C+*jnr_Fl8D`R%%8_+!BI9ve#?V zImedUvv=PytffcTTdvjZ7DobR;5%NcIC=sX5c&%`Zs9gVxa4?Fu+A_$Dm7&8WNHy8 z8kM*ZK{NuLFYoJkfF)k7NTbUzoz7gTmy|EI)>PgE(L zsnG?On(nXziz8aRPo>MLtQQ$Hv(xjN_6vs>5W5(s9KWKC0(B=rGbeA%@yP7h3*LNyTGcg;0uys2p0; zTM(3$xW?y#j^rifA=4K~gnxk5r9LFHF8ArLsVjZzAp*SI5{ w;_e9~;VjC+QH66)5sr3-)Cpod^MR%z_zGf_Z4xRaB~wj!V%J=fCZ@C`K%q4x4aRMHqrwOfwJmDP>F}0I zrB2VexcA0E_s zQ7@wbjS^ay)rV>%t!5V^cjuP1lqsQcP|xb-kc7HmDCvg}O1 zQ=?xtOgfKiq#*<_!z@fm$}_fYJf?##VDA1Iud8DW6ijO z5idq%jNuasdDu+y#)5?LY7L!3R;}T&Gq3ID!-Rx2>rZRX+9*G<_R5c}-SThNwqm_v zcdz`|`kU>O@*~CCq|c7^!YW$3v|2^li?kNYzgp|`_+7EKtaW;=m!FlND%OT#z2u5r zI{czorSfApe90;))^jf1V#xASc6VsyUe*Se@3Oz<$7hIpF)3pTcO@uBZZWc4U;(jR z(M^_$x}ryh^LZ_2>PD7_PFC%~=#K?8&Ed`eE022&olJc$5?zOJm3Bg&62+mXBk?AO%*ksSZU~oGLRku3Jjjpj z%Z;eFofr1j6f?^8irL4qxWnq^5sm9vZK{x&({c%QF3p)h+(@bEq?*&k*qy95mvjo+ ze$0g-?v$8Qax*Iu&fPC$O+BL}^}Nm)!`ZCCTfs?DSG;x_Z)*!iP8(HIOAK_0V~cyd5>dn4*wlIU_TiUA<@Ib-|P6J#7?nDQ!#_lAbwyQHwBPQO{6#yLk9` z)U)NC&7MbGZT6qnlKtn^V|zfnDv$2Rd;PG18r3?*LuWMfV;31ol}06yNV7-9Vi!PlCZ)1gLF% z;DI{`R}TiN2M3mI@U4xDmHY(6a0$120^=ooDsa|8sMbM*z-6@H3SYD!+YtKEgCShS zG{QFCHV21`cL;4zF-OEHt`tf;GxO|KxM;Kh3;SIDa=A>p@ZW?>!#3_Yd=C>TVY(-f vtU;oWB>G8WfFy1p%I7AATZr?q5~4dKCS4LSp0~p!oSCB}BrI~R2TT6}vgnPh diff --git a/out/test/java-kanban/manager/InMemoryTaskManagerTest.class b/out/test/java-kanban/manager/InMemoryTaskManagerTest.class deleted file mode 100644 index e39f2c8f067640e0b41838509469bb0ceb5943d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2761 zcmaJ@T~iZR7=AVZ65^^95Jgaf#Wul@_=VO6Z4p6J1GEyskG8sy6&8}+xVwR3zZ7+P zVP`tiEAKkfo8I(78Bx%Rz3xnBHh-tnY5Sbr1j1K`VfUQ(yyu+vdEV!JH-G%|auL8e zJPg4He*hISDp4h&W?Y$6Vi`prjr9+Yt0T6Ast!%p>@yPlk?3FuK~x72lCcRg-Ls0W zjH+g=*LzzMLZhmEPQ9h%Gu*p95>1v|Qi?TEbcddwv79V&@sG zzpr~p!j@#YH6B78!h(+$3GI>OG9v@Fsp+Hfbu26PM+Z0IZL|ikPsTfVS3+G0UKuQg ztfbR@>Xfia*cvIuAg1wXLYxql(T4pJ0%Wx0l^~x>NaMompo~L^dAwiHESu~}s3q@i z+<9Be#F8}A_Xv&#a7@N=v=j5nWeF9Qc1LxSoCxBigsaYvVQ0Q@uQ1~*7VbLp&V1N; z<}5m|oafG*^EyncSbI{KDSXe-4`EuL(e1Tso)tbEKXMkr&KvHhxAPmHGljb$oWlD7 z#AS5gw1lu>j>g9G)EJ*R&8C`^oEGb}EY-9%LuZ;~#hrBD&MO&Cw?`6*wMh`PAHfGQ zK13&RkTfbLLc*34=t`Si zh++?E_83Q&t>`u*S#^FV4?I&=ye`1Qz%F(QTW<<5+)^!G|Z8n=N^$^XO_mf z@X^C-SflG~5T8)fS47$eWTbGJX*XFgld7kW3c__+7?N=XS1BFM>dxkDDuTs(Er{z9 zGR{wKGG6hT!hK%{sp zOij#-#tM5(V;WkP8p8$+B+j&2s(m>pq482)x3#P~s972h>eO|E&A`*y-sJjPJgeR^ zOto7X8RJ2FShwf$xr|0RsMd`MRn1xbdS*JQSoWZ%PO({OX{ISb`Wc;9kTt8zkrb>1 zG@UDf+P%uRqrBwLFoNa{!W6BN%Z{3Gs4H$WIMD5W;Dw;Q7`H-kIeiY6chHcrVlIvCNF(YjS z@dZC4Hcvk&ZLPwc5Wd7$0emgW>l+EJ%LS_Glg5M^^Yq|VT#qv1QTVvL6&D@73oa;n zI-^>xNyC`P=ho}bYi{s1rbzla?$^ttS!@~QU8Hz-my*d0uw9XHIic&S*_9Crw*t5) zVc!Nft_(GR`x18lzbl#Za3Rc(rjKoj0u!H2jw;>%Dq3!G6yuwH2bFCvAU);42Zc|u z3~n9fQ}!G%f;4A<%2l7c`2^p>FW5GZz#KL&?-#!ifO-V5t?Z0o@r+ws6E}?V4U8?F z_a{ewT9meD*!&80S8KP`Zl6Wt3+!3IULQJyy=egvABHv_F5m!rXvKjA9A*bNF^~6} z4)_;v#)scg_Z(;E(Jcb?yFmA{<$|C49cX5Q+{Mr0ZnmE=y3m4tT5h7%1rYWED+1Vo z1T>5jLKpofkRjYG&k-kdTXz#x6#mXMFliVlruG6wObcRCgt021rho-f84X2gno3_>)8r6NO=jxM2`+pB z`rw1Cd!V&TcLtnm7%^;0_9T@|MV32vscI*cC%vL#xSL?sB=?kj4~XkbHC>G<2c52oG|bfPBIv=!@RU*oAU3Q zD=jh{DespSGZ;ipK{U>AAz$7DX3>{VgT_&)d4lc0VXcEpsvZ`(~`NAi~S?{38SUp4R60k@Dd_w! z2EnuN=eY9ia4m)^=&FJ(17F}v)&8iIiq|I8hx1EGbx>EXQ@81Pl!e3R+O1{oRqW*^ zNwQ_PW;d(0CluckrsRD=u$)aAnB);F(wz8QF03*fyVG{0Xz{A>1%;e(9G9jf#(kok z+?#9sp6l_FUAs?##%UC}qzO@2e;dQ@TXNQOTe}Za6Lkh=F`W31h-&UBR%YI9do?~Q zRIO)Jz}_}VF5|p<&B)QB(P-1r-cYNCs6P5=)sdu3lu3W4B$Jz9&xp`)i%z8m1~5mb z5gXt(z9J6znzUNve3AajEF%VxNZt(J(5d0>KfFlL Ag8%>k From 925f518976989bfa214ee9628e5ded2149a16970 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:31 +0300 Subject: [PATCH 09/29] =?UTF-8?q?feat:=20FileBackedTaskManager=20=E2=80=94?= =?UTF-8?q?=20CSV=20save/restore,=20autosave=20=D0=B2=20=D0=BC=D1=83=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=D1=85,=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/FileBackedTaskManager.java | 206 ++++++++++++++++--------- 1 file changed, 136 insertions(+), 70 deletions(-) diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java index 86f1402..f6c23d1 100644 --- a/src/manager/FileBackedTaskManager.java +++ b/src/manager/FileBackedTaskManager.java @@ -1,15 +1,20 @@ package manager; -import java.util.List; -import java.util.ArrayList; - import model.*; -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; -/** Менеджер с автосохранением в CSV-файл. */ +/** + * Менеджер с автоматическим сохранением в CSV-файл. + * Наследуем InMemoryTaskManager и добавляем автосохранение. + */ public class FileBackedTaskManager extends InMemoryTaskManager { private final File file; @@ -19,44 +24,70 @@ public FileBackedTaskManager(File file) { this.file = file; } - /*───────────── фабрика ─────────────*/ + /* ───────────── фабрика ───────────── */ + public static FileBackedTaskManager loadFromFile(File file) { FileBackedTaskManager m = new FileBackedTaskManager(file); m.restore(); return m; } - /*───────────── сохранение ─────────────*/ + /* ───────────── сохранение ───────────── */ + + /** Сохраняет все задачи в CSV: id,type,name,status,description,epic */ private void save() { - try (BufferedWriter w = Files.newBufferedWriter(file.toPath(), - StandardCharsets.UTF_8)) { + try (BufferedWriter w = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { w.write("id,type,name,status,description,epic"); w.newLine(); - for (Task t : getTasks()) { w.write(toCsv(t)); w.newLine(); } - for (Epic e : getEpics()) { w.write(toCsv(e)); w.newLine(); } - for (Subtask s : getSubtasks()) { w.write(toCsv(s)); w.newLine(); } + + // порядок не критичен, но читается приятнее + for (Task t : getTasks()) { + w.write(t.toCsvRow()); + w.newLine(); + } + for (Epic e : getEpics()) { + w.write(e.toCsvRow()); + w.newLine(); + } + for (Subtask s : getSubtasks()) { + w.write(s.toCsvRow()); + w.newLine(); + } } catch (IOException ex) { throw new ManagerSaveException("Не удалось сохранить файл", ex); } } + /* ───────────── восстановление ───────────── */ + + /** + * Читает CSV и восстанавливает состояние. + * ВАЖНО: не перебиваем зафиксированные в файле ID. + * Поэтому используем прямые put*-методы из базового класса и выставляем nextId. + */ private void restore() { - if (!file.exists()) return; + if (!file.exists()) { + return; + } - List epics = new ArrayList<>(); - List tasks = new ArrayList<>(); - List subtasks = new ArrayList<>(); + List epics = new ArrayList<>(); + List tasks = new ArrayList<>(); + List subtasks = new ArrayList<>(); - try (BufferedReader r = Files.newBufferedReader(file.toPath(), - StandardCharsets.UTF_8)) { - r.readLine(); // пропускаем заголовок + try (BufferedReader r = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + String header = r.readLine(); // заголовок + if (header == null) { + return; + } String line; while ((line = r.readLine()) != null) { - if (line.isBlank()) continue; + if (line.isBlank()) { + continue; + } Task t = fromCsv(line); switch (t.getType()) { - case EPIC -> epics.add((Epic) t); - case TASK -> tasks.add(t); + case EPIC -> epics.add((Epic) t); + case TASK -> tasks.add(t); case SUBTASK -> subtasks.add((Subtask) t); } } @@ -64,64 +95,65 @@ private void restore() { throw new ManagerSaveException("Не удалось прочитать файл", ex); } - /* порядок важен! */ - for (Epic e : epics) super.addNewEpic(e); - for (Task t : tasks) super.addNewTask(t); - for (Subtask s : subtasks) super.addNewSubtask(s); - } + // Важно: сначала эпики, затем задачи, затем подзадачи + for (Epic e : epics) { + super.putEpicPreserveId(e); + } + for (Task t : tasks) { + super.putTaskPreserveId(t); + } + for (Subtask s : subtasks) { + super.putSubtaskPreserveId(s); + } + // ---> СДВИГАЕМ nextId TODO:так же для себя делал,убрал второстепенные замечания! + int maxId = 0; + for (Task t : tasks) maxId = Math.max(maxId, t.getId()); + for (Epic e : epics) maxId = Math.max(maxId, e.getId()); + for (Subtask s : subtasks) maxId = Math.max(maxId, s.getId()); + super.setNextIdAfterRestore(maxId + 1); + } - /*───────────── CSV ─────────────*/ - private static String toCsv(Task t) { - String epicId = ""; - String type = t.getType().name(); // ← у Subtask вернёт SUBTASK - if (t instanceof Subtask s) { - epicId = String.valueOf(s.getEpicId()); - } - return String.join(",", - String.valueOf(t.getId()), - type, - t.getTitle(), - t.getStatus().name(), - t.getDescription(), - epicId - ); - } + /* ───────────── CSV утилиты ───────────── */ private static Task fromCsv(String csv) { String[] p = csv.split(",", -1); - int id = Integer.parseInt(p[0]); - TaskType type = TaskType.valueOf(p[1]); - String name = p[2]; - Status status = Status.valueOf(p[3]); - String descr = p[4]; - - return switch (type) { - case TASK -> { - Task t = new Task(name, descr, status); + + int id = Integer.parseInt(p[0]); + TaskType type = TaskType.valueOf(p[1]); + String name = p[2]; + Status status = Status.valueOf(p[3]); + String description = p[4]; + + switch (type) { + case TASK: { + Task t = new Task(name, description, status); t.setId(id); - yield t; + return t; } - case EPIC -> { - Epic e = new Epic(name, descr); + case EPIC: { + Epic e = new Epic(name, description); e.setId(id); e.setStatus(status); - yield e; + return e; } - case SUBTASK -> { + case SUBTASK: { int epicId = Integer.parseInt(p[5]); - Subtask s = new Subtask(name, descr, epicId); + Subtask s = new Subtask(name, description, epicId); s.setId(id); s.setStatus(status); - yield s; + return s; } - }; + default: + throw new IllegalStateException("Неизвестный тип: " + type); + } } - /*───────────── переопределения ─────────────*/ + /* ───────────── переопределения с автоматическим сохранением ───────────── */ + @Override - public int addNewTask(Task t) { // возвращаем id, как в родителе + public int addNewTask(Task t) { int id = super.addNewTask(t); save(); return id; @@ -141,17 +173,51 @@ public int addNewSubtask(Subtask s) { return id; } - @Override public void updateTask(Task t) { super.updateTask(t); save(); } - @Override public void updateEpic(Epic e) { super.updateEpic(e); save(); } - @Override public void updateSubtask(Subtask s) { super.updateSubtask(s); save(); } + @Override + public void updateTask(Task t) { + super.updateTask(t); + save(); + } + + @Override + public void updateEpic(Epic e) { + super.updateEpic(e); + save(); + } + + @Override + public void updateSubtask(Subtask s) { + super.updateSubtask(s); + save(); + } + + @Override + public void removeTask(int id) { + super.removeTask(id); + save(); + } + + @Override + public void removeEpic(int id) { + super.removeEpic(id); + save(); + } + + @Override + public void removeSubtask(int id) { + super.removeSubtask(id); + save(); + } - @Override public void removeTask(int id) { super.removeTask(id); save(); } - @Override public void removeEpic(int id) { super.removeEpic(id); save(); } - @Override public void removeSubtask(int id) { super.removeSubtask(id); save(); } + // Если в интерфейсе есть метод clear, раскомментировать. Это я себе на будущее! + /*TODO(clear): @Override + public void clear() { + super.clear(); + save(); + } */ - //@Override public void clear() { super.clear(); save(); } думаю пока не нужен + /* ───────────── demo ───────────── */ - /*───────────── demo ─────────────*/ public static void main(String[] args) { FileBackedTaskManager m = new FileBackedTaskManager(new File("tasks.csv")); From 8a34af1ca22760f5b125af8bef812906ca24a6dc Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:31 +0300 Subject: [PATCH 10/29] =?UTF-8?q?feat(core):=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B7=20=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=20remove*,=20?= =?UTF-8?q?=D1=85=D0=B5=D0=BB=D0=BF=D0=B5=D1=80=20=D0=B4=D0=BB=D1=8F=20nex?= =?UTF-8?q?tId=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20restore=20(=D0=B5=D1=81?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/InMemoryTaskManager.java | 166 +++++++++++++++++++++------ 1 file changed, 128 insertions(+), 38 deletions(-) diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java index 60019c2..8dc000e 100644 --- a/src/manager/InMemoryTaskManager.java +++ b/src/manager/InMemoryTaskManager.java @@ -1,22 +1,28 @@ package manager; import model.*; + import java.util.*; -// InMemoryTaskManager хранит задачи, эпики и подзадачи в памяти -// + ведёт историю просмотров через HistoryManager. +/** + * InMemoryTaskManager хранит задачи в памяти и ведет историю. + */ public class InMemoryTaskManager implements TaskManager { /* ---------- хранилища ---------- */ - private final Map tasks = new HashMap<>(); - private final Map epics = new HashMap<>(); - private final Map subtasks = new HashMap<>(); + protected final Map tasks = new HashMap<>(); + protected final Map epics = new HashMap<>(); + protected final Map subtasks = new HashMap<>(); - /* ---------- менеджер истории ---------- */ + /* ---------- история ---------- */ private final HistoryManager historyManager = Managers.getDefaultHistory(); - private int nextId = 1; - private int generateId() { return nextId++; } + /* ---------- генератор ID ---------- */ + protected int nextId = 1; + + private int generateId() { + return nextId++; + } /* ---------- создание ---------- */ @Override @@ -36,62 +42,111 @@ public int addNewEpic(Epic epic) { @Override public int addNewSubtask(Subtask subtask) { Epic epic = epics.get(subtask.getEpicId()); - if (epic == null) throw new IllegalArgumentException("Эпик не найден"); - - int id = generateId(); - subtask.setId(id); - subtasks.put(id, subtask); - epic.addSubtaskId(id); - return id; + if (epic == null) { + throw new IllegalArgumentException("Эпик не найден"); + } + subtask.setId(generateId()); + subtasks.put(subtask.getId(), subtask); + epic.addSubtaskId(subtask.getId()); + return subtask.getId(); } /* ---------- обновление ---------- */ - @Override public void updateTask(Task task) { if (tasks.containsKey(task.getId())) tasks.put(task.getId(), task); } - @Override public void updateEpic(Epic epic) { if (epics.containsKey(epic.getId())) epics.put(epic.getId(), epic); } - @Override public void updateSubtask(Subtask s) { if (subtasks.containsKey(s.getId())) subtasks.put(s.getId(), s); } + @Override + public void updateTask(Task task) { + if (tasks.containsKey(task.getId())) { + tasks.put(task.getId(), task); + } + } + + @Override + public void updateEpic(Epic epic) { + if (epics.containsKey(epic.getId())) { + epics.put(epic.getId(), epic); + } + } + + @Override + public void updateSubtask(Subtask s) { + if (subtasks.containsKey(s.getId())) { + subtasks.put(s.getId(), s); + } + } /* ---------- удаление ---------- */ @Override public void removeTask(int id) { tasks.remove(id); - historyManager.remove(id); // добавил удаляем из истории + historyManager.remove(id); } @Override public void removeEpic(int id) { Epic epic = epics.remove(id); if (epic != null) { - // удаляем все подзадачи эпика - for (int subId : epic.getSubtaskIds()) { - subtasks.remove(subId); - historyManager.remove(subId); // добавил подзадача из истории + for (int sid : epic.getSubtaskIds()) { + subtasks.remove(sid); + historyManager.remove(sid); } - historyManager.remove(id); // добавил сам эпик из истории + historyManager.remove(id); } } @Override public void removeSubtask(int id) { - Subtask subtask = subtasks.remove(id); - if (subtask != null) { - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) epic.getSubtaskIds().remove((Integer) id); + Subtask s = subtasks.remove(id); + if (s != null) { + Epic epic = epics.get(s.getEpicId()); + if (epic != null) { + epic.getSubtaskIds().remove((Integer) id); + } } - historyManager.remove(id); // добавил subtask из истории + historyManager.remove(id); } - /* ---------- получение + запись в историю ---------- */ + /* ---------- получение + история ---------- */ @Override - public Task getTask(int id) { Task t = tasks.get(id); if (t != null) historyManager.add(t); return t; } + public Task getTask(int id) { + Task t = tasks.get(id); + if (t != null) { + historyManager.add(t); + } + return t; + } + @Override - public Epic getEpic(int id) { Epic e = epics.get(id); if (e != null) historyManager.add(e); return e; } + public Epic getEpic(int id) { + Epic e = epics.get(id); + if (e != null) { + historyManager.add(e); + } + return e; + } + @Override - public Subtask getSubtask(int id) { Subtask s = subtasks.get(id); if (s != null) historyManager.add(s); return s; } + public Subtask getSubtask(int id) { + Subtask s = subtasks.get(id); + if (s != null) { + historyManager.add(s); + } + return s; + } /* ---------- списки ---------- */ - @Override public List getTasks() { return new ArrayList<>(tasks.values()); } - @Override public List getEpics() { return new ArrayList<>(epics.values()); } - @Override public List getSubtasks() { return new ArrayList<>(subtasks.values()); } + @Override + public List getTasks() { + return new ArrayList<>(tasks.values()); + } + + @Override + public List getEpics() { + return new ArrayList<>(epics.values()); + } + + @Override + public List getSubtasks() { + return new ArrayList<>(subtasks.values()); + } @Override public List getEpicSubtasks(int epicId) { @@ -100,15 +155,50 @@ public List getEpicSubtasks(int epicId) { if (epic != null) { for (int id : epic.getSubtaskIds()) { Subtask s = subtasks.get(id); - if (s != null) result.add(s); + if (s != null) { + result.add(s); + } } } return result; } - /* ---------- история ---------- */ @Override public List getHistory() { return historyManager.getHistory(); } + + /* ---------- защищённые хуки для восстановления из файла ---------- */ + + /** Кладем задачу с уже заданным id (не трогаем историю, TODO: без увеличения nextId). */ + protected void putTaskPreserveId(Task task) { + tasks.put(task.getId(), task); + bumpNextId(task.getId()); + } + + /** Кладем эпик с уже заданным id. */ + protected void putEpicPreserveId(Epic epic) { + epics.put(epic.getId(), epic); + bumpNextId(epic.getId()); + } + + /** Кладем Subtask с уже заданным id и привязываем к эпику. */ + protected void putSubtaskPreserveId(Subtask subtask) { + subtasks.put(subtask.getId(), subtask); + Epic epic = epics.get(subtask.getEpicId()); + if (epic != null) { + epic.addSubtaskId(subtask.getId()); + } + bumpNextId(subtask.getId()); + } + private void bumpNextId(int usedId) { + if (usedId >= nextId) { + nextId = usedId + 1; + } + } + + /** Вызывается после восстановления, чтобы новые id шли дальше. */ + protected void setNextIdAfterRestore(int nextId) { + this.nextId = Math.max(this.nextId, nextId); + } } From 4119bd883e9bd0b6147f2103d0449d9049bfc65b Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 11/29] =?UTF-8?q?style(history):=20=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=BD=D1=8B=D0=B5=20=D1=81=D0=BA=D0=BE=D0=B1=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=20Google=20Style,=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=BC?= =?UTF-8?q?=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5=20=D1=87=D0=B8=D1=81=D1=82=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/InMemoryHistoryManager.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/manager/InMemoryHistoryManager.java b/src/manager/InMemoryHistoryManager.java index 7ce16be..9f97a16 100644 --- a/src/manager/InMemoryHistoryManager.java +++ b/src/manager/InMemoryHistoryManager.java @@ -3,7 +3,7 @@ import model.Task; import java.util.*; -/** HistoryManager на базе двусвязного списка + HashMap */ +/** HistoryManager на базе двойного связанного списка + HashMap */ public class InMemoryHistoryManager implements HistoryManager { /* ───── узел списка ───── */ @@ -26,7 +26,7 @@ private static class Node { /* ───── вспомогательные ───── */ - /** добавляем просмотр в хвост */ + /** Добавляем просмотр в хвост */ private void linkLast(Task task) { Node oldTail = tail; Node newNode = new Node(oldTail, task, null); // n → newNode @@ -39,7 +39,7 @@ private void linkLast(Task task) { } } - /** удаляем произвольный узел */ + /** Удаляем произвольный узел */ private void removeNode(Node target) { if (target == null) { return; @@ -61,7 +61,7 @@ private void removeNode(Node target) { } } - /** выгружаем историю списком */ + /** Выгружаем историю списком */ private List getTasks() { List list = new ArrayList<>(); for (Node current = head; current != null; current = current.next) { From 8c89624e66e5c451a14aad61dbd1a26775193e31 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 12/29] =?UTF-8?q?docs(api):=20JavaDoc=20=D0=BA=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D1=83=20=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=B4=D0=B6=D0=B5=D1=80=D0=B0,=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D1=82=D1=83=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/TaskManager.java | 83 ++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/src/manager/TaskManager.java b/src/manager/TaskManager.java index d05321e..6c0c8c3 100644 --- a/src/manager/TaskManager.java +++ b/src/manager/TaskManager.java @@ -1,40 +1,99 @@ package manager; -import model.*; +import model.Epic; +import model.Subtask; +import model.Task; + import java.util.List; -// Интерфейс менеджера задач. -// Определяет базовые методы для управления обычными задачами, эпиками и подзадачами. -// Также предоставляет методы для получения истории просмотров. + +/** + * Интерфейс менеджера задач. + * Определяет операции для создания, изменения, удаления и получения + * задач всех типов (Task, Epic, Subtask), а также получения истории просмотров. + */ public interface TaskManager { - //Создание задач всех типов + /* ===================== Создание ===================== */ + + /** + * Добавить обычную задачу. + * @param task задача + * @return присвоенный идентификатор + */ int addNewTask(Task task); + + /** + * Добавить эпик. + * @param epic эпик + * @return присвоенный идентификатор + */ int addNewEpic(Epic epic); + + /** + * Добавить подзадачу. + * @param subtask подзадача + * @return присвоенный идентификатор + */ int addNewSubtask(Subtask subtask); - //Обновление задач всех типов + /* ===================== Обновление ===================== */ + + /** + * Обновить обычную задачу (по её id внутри объекта). + * Если задачи с таким id нет — обновление игнорируется. + */ void updateTask(Task task); + + /** + * Обновить эпик (по его id внутри объекта). + * Если эпика с таким id нет — обновление игнорируется. + */ void updateEpic(Epic epic); + + /** + * Обновить подзадачу (по её id внутри объекта). + * Если подзадачи с таким id нет — обновление игнорируется. + */ void updateSubtask(Subtask subtask); - //Удаление задач всех типов - + /* ===================== Удаление ===================== */ + + /** Удалить обычную задачу по id. */ void removeTask(int id); + + /** Удалить эпик по id (его подзадачи тоже удаляются). */ void removeEpic(int id); + + /** Удалить подзадачу по id. */ void removeSubtask(int id); - /* ─── получение ─── */ + /* ===================== Получение (одна) ===================== */ + + /** Получить задачу по id (добавляется в историю просмотров). */ Task getTask(int id); + /** Получить эпик по id (добавляется в историю просмотров). */ Epic getEpic(int id); + + /** Получить подзадачу по id (добавляется в историю просмотров). */ Subtask getSubtask(int id); + /* ===================== Получение (списки) ===================== */ + + /** Все обычные задачи. */ List getTasks(); + + /** Все эпики. */ List getEpics(); + + /** Все подзадачи. */ List getSubtasks(); + + /** Подзадачи конкретного эпика. */ List getEpicSubtasks(int epicId); - /* ─── история ─── */ - + /* ===================== История ===================== */ + + /** Последние просмотренные задачи (до 10, без дублей). */ List getHistory(); -} \ No newline at end of file +} From 50bc2e73fc1926db9aa1b21c7a0df5cc4f7358fa Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 13/29] feat(model): enum TaskType {TASK, EPIC, SUBTASK} --- src/model/TaskType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/TaskType.java b/src/model/TaskType.java index db78cd9..fbdbeb1 100644 --- a/src/model/TaskType.java +++ b/src/model/TaskType.java @@ -1,6 +1,6 @@ package model; -/** Тип задачи: нужен для CSV-сериализации. */ +/** Тип задачи: нужен для CSV сериализация. */ public enum TaskType { TASK, EPIC, From a8af70b64e740f6aa5889dcee0c39ae8bcdeb505 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 14/29] =?UTF-8?q?feat(model):=20getDescription()=20+=20toC?= =?UTF-8?q?svRow();=20=D0=B0=D0=BA=D0=BA=D1=83=D1=80=D0=B0=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20equals/hashCode/toString?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Task.java | 69 ++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/src/model/Task.java b/src/model/Task.java index d603b2c..fcf9174 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -4,42 +4,71 @@ /** Базовая задача. */ public class Task { - /* поля -------------------------------------------------------- */ protected String title; protected String description; - protected int id; - protected Status status; + protected int id; + protected Status status; - /* конструктор ------------------------------------------------- */ public Task(String title, String description, Status status) { - this.title = title; + this.title = title; this.description = description; - this.status = status; + this.status = status; } - /* тип задачи -------------------------------------------------- */ - public TaskType getType() { // нужен сериализации + /** Тип задачи. */ + public TaskType getType() { return TaskType.TASK; } - /* геттеры / сеттеры ------------------------------------------ */ - public int getId() { return id; } - public void setId(int id) { this.id = id; } + /* ========= геттеры/сеттеры ========= */ - public String getTitle() { return title; } + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } - public String getDescription() { return description; } // ← новый геттер + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } - public Status getStatus() { return status; } - public void setStatus(Status status) { this.status = status; } + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + /* ========= CSV-представление (для FileBackedTaskManager) ========= */ + public String toCsvRow() { + // у обычной задачи поле epic пустое + return String.join(",", + String.valueOf(getId()), + getType().name(), + getTitle(), + getStatus().name(), + getDescription(), + "" + ); + } - /* equals / hashCode / toString ------------------------------- */ @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Task)) return false; - Task task = (Task) o; - return id == task.id; + if (this == o) { + return true; + } + // pattern matching TODO: — убирает предупреждение «Variable 'task' can be replaced with pattern variable» + if (!(o instanceof Task other)) { + return false; + } + return id == other.id; } @Override From a067fbc0048771a6b83f06d4fa1ba957f89fcd0f Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 15/29] =?UTF-8?q?feat(model):=20=D1=85=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=20subtaskIds=20=D1=81=20=D0=B8=D0=BD=D0=BA=D0=B0=D0=BF=D1=81?= =?UTF-8?q?=D1=83=D0=BB=D1=8F=D1=86=D0=B8=D0=B5=D0=B9=20(=D0=BA=D0=BE?= =?UTF-8?q?=D0=BF=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=20getSubtaskIds)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Epic.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index 7ae7d31..4eecf5f 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -3,31 +3,28 @@ import java.util.ArrayList; import java.util.List; -/** Эпик — агрегирующая задача, содержит список id подзадач. */ +/** Эпик — задача, содержащая подзадачи. */ public class Epic extends Task { - private final List subtaskIds = new ArrayList<>(); public Epic(String title, String description) { super(title, description, Status.NEW); } - /*--------------- новый метод ---------------*/ @Override public TaskType getType() { return TaskType.EPIC; } - /*-------------------------------------------*/ - /** Возвращает копию списка id подзадач (инкапсуляция). */ + /** Возвращает копию списка идентификаторов подзадач. */ public List getSubtaskIds() { return new ArrayList<>(subtaskIds); } - /** Добавляет id подзадачи к эпику. */ + /** Добавляет id подзадачи (эпик не может ссылаться сам на себя). */ public void addSubtaskId(int id) { if (id == this.id) { - throw new IllegalArgumentException("Эпик не может быть собственным сабтаском"); + throw new IllegalArgumentException("Эпик не может быть собственным Subtask"); } subtaskIds.add(id); } From 9dc7b3a297e89d961a766be077d9a3dfad29469f Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 16/29] =?UTF-8?q?feat(model):=20=D0=BF=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=20epicId=20+=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20id=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=BA=D1=82=D0=BE=D1=80=D0=B5,=20=D0=B3=D0=B5?= =?UTF-8?q?=D1=82=D1=82=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Subtask.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/model/Subtask.java b/src/model/Subtask.java index a1ae0d3..df813ed 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -11,14 +11,24 @@ public Subtask(String title, String description, int epicId) { this.epicId = epicId; } - /*--------------- новое ----------------*/ @Override public TaskType getType() { return TaskType.SUBTASK; } - /*--------------------------------------*/ public int getEpicId() { return epicId; } + + @Override + public String toCsvRow() { + return String.join(",", + String.valueOf(id), + getType().name(), + title, + status.name(), + description, + String.valueOf(epicId) + ); + } } From f28352fe921c36d0b29529b06c3cbb994c9e3d7c Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 17/29] =?UTF-8?q?test(history):=20add()=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B8=D1=82=20=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D0=B5=D1=86=20=D0=B1=D0=B5=D0=B7=20=D0=B4=D1=83?= =?UTF-8?q?=D0=B1=D0=BB=D0=B5=D0=B9;=20remove()=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D1=82=20=D0=B7=D0=B0=20O(1);=20=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/manager/HistoryManagerTest.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/manager/HistoryManagerTest.java b/src/test/manager/HistoryManagerTest.java index a4e032b..928c3a3 100644 --- a/src/test/manager/HistoryManagerTest.java +++ b/src/test/manager/HistoryManagerTest.java @@ -1,7 +1,6 @@ package manager; -import manager.HistoryManager; -import manager.Managers; + import model.Task; import model.Status; import org.junit.jupiter.api.BeforeEach; @@ -12,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Юнит-тесты самого HistoryManager + * Unit-тесты самого HistoryManager */ class HistoryManagerTest { @@ -27,12 +26,12 @@ void setUp() { hm = Managers.getDefaultHistory(); // или new InMemoryHistoryManager() // id задаём вручную, чтобы HistoryManager.remove(id) работал корректно - t1 = new Task("T-1", "descr1", Status.NEW); t1.setId(1); - t2 = new Task("T-2", "descr2", Status.NEW); t2.setId(2); - t3 = new Task("T-3", "descr3", Status.NEW); t3.setId(3); + t1 = new Task("T-1", "description1", Status.NEW); t1.setId(1); + t2 = new Task("T-2", "description2", Status.NEW); t2.setId(2); + t3 = new Task("T-3", "description3", Status.NEW); t3.setId(3); } - /** add(): без дубликатов, последний просмотр переносится в конец */ + /** Add(): без дубликатов, последний просмотр переносится в конец */ @Test void add_movesTaskToTail_withoutDuplicates() { hm.add(t1); @@ -45,7 +44,7 @@ void add_movesTaskToTail_withoutDuplicates() { "Повторный просмотр должен перемещать задачу в конец истории без дублирования"); } - /** remove(): удаляет узел из середины за O(1) */ + /** Remove(): удаляет узел из середины за O(1) */ @Test void remove_deletesNodeFromAnyPosition() { hm.add(t1); From d6bbb1bb339f8a18e7e26d70aa4ea6482c5b165b Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 18/29] =?UTF-8?q?test(task-manager):=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B5=D0=B9=20=D0=B2=20=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=B8;=20=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=8F=20>10;=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=87=D0=B8=D1=81=D1=82=D0=B8=D1=82=20?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/manager/InMemoryTaskManagerTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/manager/InMemoryTaskManagerTest.java b/src/test/manager/InMemoryTaskManagerTest.java index 660cd50..6841ce0 100644 --- a/src/test/manager/InMemoryTaskManagerTest.java +++ b/src/test/manager/InMemoryTaskManagerTest.java @@ -1,15 +1,18 @@ package manager; -import model.*; +import model.Status; +import model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** - * Юнит-тесты InMemoryTaskManager + HistoryManager. + * Unit-тесты InMemoryTaskManager + HistoryManager. */ class InMemoryTaskManagerTest { @@ -32,7 +35,8 @@ void addDuplicates_keepsOnlyLastView() { List history = tm.getHistory(); assertEquals(1, history.size(), "В истории должен остаться единственный просмотр"); - assertEquals(id, history.get(0).getId()); + assertEquals(id, history.get(0).getId());//TODO: если именно нужно от Java 21 и более то нужно заменить на + // assertEquals(id, history.getFirst().getId()); } /* -------- 2. История может быть > 10 -------- */ From d6ef3d1c3895c762e205cfe1462e151d06b80d32 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:32 +0300 Subject: [PATCH 19/29] =?UTF-8?q?test(int):=20=D0=BF=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82=20tests/manager,=20=D0=BC=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=B8=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=BE=D0=B2/=D0=BF=D1=83=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/manager/TaskManagerHistoryIntegrationTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/manager/TaskManagerHistoryIntegrationTest.java b/src/test/manager/TaskManagerHistoryIntegrationTest.java index a657712..26e42b4 100644 --- a/src/test/manager/TaskManagerHistoryIntegrationTest.java +++ b/src/test/manager/TaskManagerHistoryIntegrationTest.java @@ -1,7 +1,6 @@ package manager; -import manager.Managers; -import manager.TaskManager; + import model.Task; import model.Status; import org.junit.jupiter.api.BeforeEach; @@ -24,7 +23,7 @@ void setUp() { /** Удаление задачи очищает историю */ @Test void deletingTask_removesItFromHistory() { - int id = tm.addNewTask(new Task("Task-1", "descr", Status.NEW)); + int id = tm.addNewTask(new Task("Task-1", "description", Status.NEW)); tm.getTask(id); // помещаем в историю assertEquals(1, tm.getHistory().size(), From 0e25ccd0d7a0721e7c51667ed87d34bffbf76a73 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 9 Aug 2025 02:15:33 +0300 Subject: [PATCH 20/29] =?UTF-8?q?chore(demo):=20=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=20=D0=B2=D0=BE=D0=B7=D0=B2?= =?UTF-8?q?=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D0=BC=D1=8B=D0=B5=20=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=B0=D0=B5=D0=BC=20=D1=81=D1=87=D1=91=D1=82?= =?UTF-8?q?=D1=87=D0=B8=D0=BA=D0=B8,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Main.java | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Main.java b/src/Main.java index 771028c..f830167 100644 --- a/src/Main.java +++ b/src/Main.java @@ -2,24 +2,37 @@ import manager.TaskManager; import model.*; - -// Демонстрация базовой работы с менеджером задач: -//добавление задач, получение и история просмотров. - +// Демонстрация базовой работы с менеджером задач public class Main { public static void main(String[] args) { TaskManager manager = Managers.getDefault(); // === Добавление задач === - int id1 = manager.addNewTask(new Task("Задача 1", "Описание задачи", Status.NEW)); - int id2 = manager.addNewEpic(new Epic("Эпик 1", "Описание эпика")); - int id3 = manager.addNewSubtask(new Subtask("Подзадача 1", "Описание подзадачи", id2)); + int id1 = manager.addNewTask(new Task("Задача 1", "Описание задачи", Status.NEW)); + int epicId = manager.addNewEpic(new Epic("Эпик 1", "Описание эпика")); + int subId = manager.addNewSubtask(new Subtask("Подзадача 1", "Описание подзадачи", epicId)); + manager.addNewSubtask(new Subtask("Подзадача 2", "Ещё одна", epicId)); + + // === Используем возвращаемые списки (убираем жёлтые лампы) TODO: это просто для себя подчеркиваю === + int tasksCount = manager.getTasks().size(); + int epicsCount = manager.getEpics().size(); + int subtasksCount = manager.getSubtasks().size(); + int epicSubCount = manager.getEpicSubtasks(epicId).size(); + System.out.printf( + "Всего: tasks=%d, epics=%d, subtasks=%d; у эпика %d подзадач=%d%n", + tasksCount, epicsCount, subtasksCount, epicId, epicSubCount + ); + + // === Получение задач (для истории просмотров) — без пустых if === + boolean viewedTask1 = manager.getTask(id1) != null; + boolean viewedEpic = manager.getEpic(epicId) != null; + boolean viewedSub = manager.getSubtask(subId) != null; + boolean viewedTask2 = manager.getTask(id1) != null; + + // просто используем значения, чтобы инспекция была довольна + System.out.printf("Просмотры: t1=%b, epic=%b, sub=%b, t1-again=%b%n", + viewedTask1, viewedEpic, viewedSub, viewedTask2); - // === Получение задач (для истории просмотров) === - manager.getTask(id1); - manager.getEpic(id2); - manager.getSubtask(id3); - manager.getTask(id1); // повторное обращение к задаче // === Вывод истории просмотров === System.out.println("=== История просмотров ==="); @@ -34,14 +47,12 @@ public static void main(String[] args) { } } - // понятный тип задачи private static String getTypeName(Task task) { if (task instanceof Epic) return "Эпик"; if (task instanceof Subtask) return "Подзадача"; return "Задача"; } - // Перевод статуса на русский private static String getStatusName(Status status) { return switch (status) { case NEW -> "Новая"; From db1a3135a7902562f2b197203e1c52dd24f64668 Mon Sep 17 00:00:00 2001 From: Sergey Date: Wed, 13 Aug 2025 17:26:03 +0300 Subject: [PATCH 21/29] Sprint 8: time & duration, prioritization, overlap checks --- src/Main.java | 31 +- src/exceptions/TaskValidationException.java | 8 + src/manager/FileBackedTaskManager.java | 236 ++++++++------- src/manager/HistoryManager.java | 12 +- src/manager/InMemoryHistoryManager.java | 97 ++----- src/manager/InMemoryTaskManager.java | 269 ++++++++++++++---- src/manager/ManagerSaveException.java | 10 +- src/manager/Managers.java | 15 +- src/manager/TaskManager.java | 58 +--- src/model/Epic.java | 66 ++++- src/model/Subtask.java | 60 +++- src/model/Task.java | 144 ++++++++-- src/test/manager/EpicStatusTest.java | 66 +++++ .../manager/FileBackedTaskManagerTest.java | 41 +++ src/test/manager/HistoryManagerTest.java | 60 ---- .../manager/InMemoryHistoryManagerTest.java | 54 ++++ src/test/manager/InMemoryTaskManagerTest.java | 63 +--- .../TaskManagerHistoryIntegrationTest.java | 36 --- src/test/manager/TaskManagerTest.java | 108 +++++++ 19 files changed, 927 insertions(+), 507 deletions(-) create mode 100644 src/exceptions/TaskValidationException.java create mode 100644 src/test/manager/EpicStatusTest.java create mode 100644 src/test/manager/FileBackedTaskManagerTest.java delete mode 100644 src/test/manager/HistoryManagerTest.java create mode 100644 src/test/manager/InMemoryHistoryManagerTest.java delete mode 100644 src/test/manager/TaskManagerHistoryIntegrationTest.java create mode 100644 src/test/manager/TaskManagerTest.java diff --git a/src/Main.java b/src/Main.java index f830167..2da5e79 100644 --- a/src/Main.java +++ b/src/Main.java @@ -8,16 +8,16 @@ public static void main(String[] args) { TaskManager manager = Managers.getDefault(); // === Добавление задач === - int id1 = manager.addNewTask(new Task("Задача 1", "Описание задачи", Status.NEW)); + int id1 = manager.addNewTask(new Task("Задача 1", "Описание задачи", Status.NEW)); int epicId = manager.addNewEpic(new Epic("Эпик 1", "Описание эпика")); - int subId = manager.addNewSubtask(new Subtask("Подзадача 1", "Описание подзадачи", epicId)); + int subId = manager.addNewSubtask(new Subtask("Подзадача 1", "Описание подзадачи", epicId)); manager.addNewSubtask(new Subtask("Подзадача 2", "Ещё одна", epicId)); // === Используем возвращаемые списки (убираем жёлтые лампы) TODO: это просто для себя подчеркиваю === - int tasksCount = manager.getTasks().size(); - int epicsCount = manager.getEpics().size(); - int subtasksCount = manager.getSubtasks().size(); - int epicSubCount = manager.getEpicSubtasks(epicId).size(); + int tasksCount = manager.getTasks().size(); + int epicsCount = manager.getEpics().size(); + int subtasksCount = manager.getSubtasks().size(); + int epicSubCount = manager.getEpicSubtasks(epicId).size(); System.out.printf( "Всего: tasks=%d, epics=%d, subtasks=%d; у эпика %d подзадач=%d%n", tasksCount, epicsCount, subtasksCount, epicId, epicSubCount @@ -25,14 +25,15 @@ public static void main(String[] args) { // === Получение задач (для истории просмотров) — без пустых if === boolean viewedTask1 = manager.getTask(id1) != null; - boolean viewedEpic = manager.getEpic(epicId) != null; - boolean viewedSub = manager.getSubtask(subId) != null; + boolean viewedEpic = manager.getEpic(epicId) != null; + boolean viewedSub = manager.getSubtask(subId) != null; boolean viewedTask2 = manager.getTask(id1) != null; // просто используем значения, чтобы инспекция была довольна - System.out.printf("Просмотры: t1=%b, epic=%b, sub=%b, t1-again=%b%n", - viewedTask1, viewedEpic, viewedSub, viewedTask2); - + System.out.printf( + "Просмотры: t1=%b, epic=%b, sub=%b, t1-again=%b%n", + viewedTask1, viewedEpic, viewedSub, viewedTask2 + ); // === Вывод истории просмотров === System.out.println("=== История просмотров ==="); @@ -48,8 +49,12 @@ public static void main(String[] args) { } private static String getTypeName(Task task) { - if (task instanceof Epic) return "Эпик"; - if (task instanceof Subtask) return "Подзадача"; + if (task instanceof Epic) { + return "Эпик"; + } + if (task instanceof Subtask) { + return "Подзадача"; + } return "Задача"; } diff --git a/src/exceptions/TaskValidationException.java b/src/exceptions/TaskValidationException.java new file mode 100644 index 0000000..875d245 --- /dev/null +++ b/src/exceptions/TaskValidationException.java @@ -0,0 +1,8 @@ +package exceptions; + +/** + * NEW (sprint-8): бросается при пересечении задач по времени. + */ +public class TaskValidationException extends RuntimeException { + public TaskValidationException(String message) { super(message); } +} diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java index f6c23d1..1810bdf 100644 --- a/src/manager/FileBackedTaskManager.java +++ b/src/manager/FileBackedTaskManager.java @@ -1,6 +1,10 @@ package manager; -import model.*; +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; +import model.TaskType; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -8,51 +12,63 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.time.Duration; // NEW (sprint-8) +import java.time.LocalDateTime; // NEW (sprint-8) +import java.time.format.DateTimeFormatter; // NEW (sprint-8) +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; /** - * Менеджер с автоматическим сохранением в CSV-файл. - * Наследуем InMemoryTaskManager и добавляем автосохранение. + * Менеджер с сохранением состояния в файл (CSV). + * CHANGED (sprint-8): + * - расширен CSV-формат: добавлены колонки durationMinutes и startTime; + * - добавлена обратная совместимость чтения старого формата; + * - на restore используем put*PreserveId + setNextIdAfterRestore. */ public class FileBackedTaskManager extends InMemoryTaskManager { private final File file; + // Единый формат для CSV + private static final DateTimeFormatter CSV_TIME_FMT = Task.CSV_TIME_FMT; + + /* ───────────── фабрика ───────────── */ + public FileBackedTaskManager(File file) { - super(); this.file = file; } - /* ───────────── фабрика ───────────── */ - + @SuppressWarnings("unused") public static FileBackedTaskManager loadFromFile(File file) { - FileBackedTaskManager m = new FileBackedTaskManager(file); - m.restore(); - return m; + FileBackedTaskManager manager = new FileBackedTaskManager(file); + manager.restore(); + return manager; } /* ───────────── сохранение ───────────── */ - /** Сохраняет все задачи в CSV: id,type,name,status,description,epic */ + /** Сохраняет все задачи в CSV: id,type,name,status,description,durationMinutes,startTime,epic */ private void save() { - try (BufferedWriter w = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { - w.write("id,type,name,status,description,epic"); - w.newLine(); - - // порядок не критичен, но читается приятнее - for (Task t : getTasks()) { - w.write(t.toCsvRow()); - w.newLine(); + try (BufferedWriter writer = + Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + writer.write("id,type,name,status,description,durationMinutes,startTime,epic"); + writer.newLine(); + + // Порядок не критичен, но читается приятнее + for (Task task : getTasks()) { + writer.write(task.toCsvRow()); + writer.newLine(); } - for (Epic e : getEpics()) { - w.write(e.toCsvRow()); - w.newLine(); + for (Epic epic : getEpics()) { + writer.write(epic.toCsvRow()); + writer.newLine(); } - for (Subtask s : getSubtasks()) { - w.write(s.toCsvRow()); - w.newLine(); + for (Subtask subtask : getSubtasks()) { + writer.write(subtask.toCsvRow()); + writer.newLine(); } + } catch (IOException ex) { throw new ManagerSaveException("Не удалось сохранить файл", ex); } @@ -60,35 +76,42 @@ private void save() { /* ───────────── восстановление ───────────── */ - /** - * Читает CSV и восстанавливает состояние. - * ВАЖНО: не перебиваем зафиксированные в файле ID. - * Поэтому используем прямые put*-методы из базового класса и выставляем nextId. - */ + /** Читает CSV и восстанавливает состояние. */ private void restore() { if (!file.exists()) { return; } - List epics = new ArrayList<>(); - List tasks = new ArrayList<>(); + List epics = new ArrayList<>(); + List tasks = new ArrayList<>(); List subtasks = new ArrayList<>(); - try (BufferedReader r = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { - String header = r.readLine(); // заголовок + try (BufferedReader reader = + Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + String header = reader.readLine(); // заголовок if (header == null) { return; } + String line; - while ((line = r.readLine()) != null) { + while ((line = reader.readLine()) != null) { if (line.isBlank()) { continue; } - Task t = fromCsv(line); - switch (t.getType()) { - case EPIC -> epics.add((Epic) t); - case TASK -> tasks.add(t); - case SUBTASK -> subtasks.add((Subtask) t); + + // внутри restore(), в цикле чтения строк CSV + Task task = fromCsv(line); + + if (task instanceof Epic epic) { + epics.add(epic); + } else if (task instanceof Subtask subtask) { + subtasks.add(subtask); + } else if (task.getType() == TaskType.TASK) { // базовая Task + tasks.add(task); + } else { + throw new IllegalStateException( + "Неизвестный подкласс задачи (id=" + task.getId() + "): " + + task.getClass().getName()); } } } catch (IOException ex) { @@ -96,29 +119,39 @@ private void restore() { } // Важно: сначала эпики, затем задачи, затем подзадачи - for (Epic e : epics) { - super.putEpicPreserveId(e); + for (Epic epic : epics) { + super.putEpicPreserveId(epic); } - for (Task t : tasks) { - super.putTaskPreserveId(t); + for (Task task : tasks) { + super.putTaskPreserveId(task); } - for (Subtask s : subtasks) { - super.putSubtaskPreserveId(s); + for (Subtask subtask : subtasks) { + super.putSubtaskPreserveId(subtask); } - // ---> СДВИГАЕМ nextId TODO:так же для себя делал,убрал второстепенные замечания! - int maxId = 0; - for (Task t : tasks) maxId = Math.max(maxId, t.getId()); - for (Epic e : epics) maxId = Math.max(maxId, e.getId()); - for (Subtask s : subtasks) maxId = Math.max(maxId, s.getId()); + // Сдвигаем nextId + пересчитываем эпики (внутри setNextIdAfterRestore) + int maxId = 0; + for (Task task : tasks) { + maxId = Math.max(maxId, task.getId()); + } + for (Epic epic : epics) { + maxId = Math.max(maxId, epic.getId()); + } + for (Subtask subtask : subtasks) { + maxId = Math.max(maxId, subtask.getId()); + } super.setNextIdAfterRestore(maxId + 1); } - /* ───────────── CSV утилиты ───────────── */ private static Task fromCsv(String csv) { String[] p = csv.split(",", -1); + // Старый формат Sprint 7: id,type,name,status,description,epic (6 полей, epic только у subtask) + // Новый формат Sprint 8: id,type,name,status,description,durationMinutes,startTime,epic (8 полей) + if (p.length != 6 && p.length != 8) { + throw new ManagerSaveException("Некорректная строка CSV: " + csv); + } int id = Integer.parseInt(p[0]); TaskType type = TaskType.valueOf(p[1]); @@ -126,68 +159,98 @@ private static Task fromCsv(String csv) { Status status = Status.valueOf(p[3]); String description = p[4]; + String durStr = p.length == 8 ? p[5] : ""; + String startStr = p.length == 8 ? p[6] : ""; + // после проверки выше длина может быть только 6 или 8 + String epicStr = p.length == 8 ? p[7] : p[5]; + + Duration duration = durStr.isBlank() ? null : Duration.ofMinutes(Long.parseLong(durStr)); + LocalDateTime startTime = parseTimeOrNull(startStr); + switch (type) { case TASK: { - Task t = new Task(name, description, status); - t.setId(id); - return t; + Task task = new Task(name, description, status); + task.setId(id); + task.setDuration(duration); + task.setStartTime(startTime); + return task; } case EPIC: { - Epic e = new Epic(name, description); - e.setId(id); - e.setStatus(status); - return e; + Epic epic = new Epic(name, description); + epic.setId(id); + epic.setStatus(status); + // duration/start/end будут пересчитаны после загрузки subtask + return epic; } case SUBTASK: { - int epicId = Integer.parseInt(p[5]); - Subtask s = new Subtask(name, description, epicId); - s.setId(id); - s.setStatus(status); - return s; + int epicId = (epicStr == null || epicStr.isBlank()) ? 0 : Integer.parseInt(epicStr); + Subtask subtask = new Subtask(name, description, status, epicId); + subtask.setId(id); + subtask.setDuration(duration); + subtask.setStartTime(startTime); + return subtask; } default: throw new IllegalStateException("Неизвестный тип: " + type); } } - /* ───────────── переопределения с автоматическим сохранением ───────────── */ + private static LocalDateTime parseTimeOrNull(String s) { + if (s == null || s.isBlank()) { + return null; + } + try { + // основной формат + return LocalDateTime.parse(s, CSV_TIME_FMT); + } catch (DateTimeParseException ex) { + try { + // пробуем ISO в случае старого/другого вывода + return LocalDateTime.parse(s); + } catch (DateTimeParseException ex2) { + // мягкая деградация — не срываем загрузку + return null; + } + } + } + + /* ───────────── переопределения с автосохранением ───────────── */ @Override - public int addNewTask(Task t) { - int id = super.addNewTask(t); + public int addNewTask(Task task) { + int id = super.addNewTask(task); save(); return id; } @Override - public int addNewEpic(Epic e) { - int id = super.addNewEpic(e); + public int addNewEpic(Epic epic) { + int id = super.addNewEpic(epic); save(); return id; } @Override - public int addNewSubtask(Subtask s) { - int id = super.addNewSubtask(s); + public int addNewSubtask(Subtask subtask) { + int id = super.addNewSubtask(subtask); save(); return id; } @Override - public void updateTask(Task t) { - super.updateTask(t); + public void updateTask(Task task) { + super.updateTask(task); save(); } @Override - public void updateEpic(Epic e) { - super.updateEpic(e); + public void updateEpic(Epic epic) { + super.updateEpic(epic); save(); } @Override - public void updateSubtask(Subtask s) { - super.updateSubtask(s); + public void updateSubtask(Subtask subtask) { + super.updateSubtask(subtask); save(); } @@ -208,25 +271,4 @@ public void removeSubtask(int id) { super.removeSubtask(id); save(); } - - // Если в интерфейсе есть метод clear, раскомментировать. Это я себе на будущее! - /*TODO(clear): @Override - public void clear() { - super.clear(); - save(); - } */ - - /* ───────────── demo ───────────── */ - - public static void main(String[] args) { - FileBackedTaskManager m = new FileBackedTaskManager(new File("tasks.csv")); - - Epic epic = new Epic("Спринт-7", "Файл-менеджер"); - m.addNewEpic(epic); - m.addNewSubtask(new Subtask("save()", "реализовать", epic.getId())); - m.addNewTask(new Task("Читать ТЗ", "вникнуть", Status.IN_PROGRESS)); - - FileBackedTaskManager restored = FileBackedTaskManager.loadFromFile(new File("tasks.csv")); - System.out.println("♻ восстановлено задач: " + restored.getTasks().size()); - } } diff --git a/src/manager/HistoryManager.java b/src/manager/HistoryManager.java index c163693..9b2421b 100644 --- a/src/manager/HistoryManager.java +++ b/src/manager/HistoryManager.java @@ -1,10 +1,14 @@ package manager; -import model.Task; import java.util.List; +import model.Task; +/** Интерфейс менеджера истории просмотров задач. */ public interface HistoryManager { - void add(Task task); // записать просмотр - void remove(int id); // удалить по id (нужно при удалении задач) - List getHistory();// вернуть историю в порядке просмотра + + void add(Task task); + + void remove(int id); + + List getHistory(); } diff --git a/src/manager/InMemoryHistoryManager.java b/src/manager/InMemoryHistoryManager.java index 9f97a16..9593b3f 100644 --- a/src/manager/InMemoryHistoryManager.java +++ b/src/manager/InMemoryHistoryManager.java @@ -1,100 +1,39 @@ package manager; import model.Task; -import java.util.*; -/** HistoryManager на базе двойного связанного списка + HashMap */ -public class InMemoryHistoryManager implements HistoryManager { - - /* ───── узел списка ───── */ - private static class Node { - Task data; - Node prev; - Node next; - - Node(Node prev, Task data, Node next) { - this.prev = prev; - this.data = data; - this.next = next; - } - } - - /* ───── поля ───── */ - private final Map index = new HashMap<>(); - private Node head; - private Node tail; - - /* ───── вспомогательные ───── */ - - /** Добавляем просмотр в хвост */ - private void linkLast(Task task) { - Node oldTail = tail; - Node newNode = new Node(oldTail, task, null); // n → newNode - tail = newNode; - - if (oldTail == null) { - head = newNode; // фигурные скобки - } else { - oldTail.next = newNode; // фигурные скобки - } - } - - /** Удаляем произвольный узел */ - private void removeNode(Node target) { - if (target == null) { - return; - } +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; - Node prev = target.prev; - Node next = target.next; - - if (prev != null) { - prev.next = next; - } else { - head = next; // фигурные скобки - } - - if (next != null) { - next.prev = prev; - } else { - tail = prev; // фигурные скобки - } - } - - /** Выгружаем историю списком */ - private List getTasks() { - List list = new ArrayList<>(); - for (Node current = head; current != null; current = current.next) { - list.add(current.data); - } - return list; - } +public class InMemoryHistoryManager implements HistoryManager { - /* ───── HistoryManager API ───── */ + private static final int MAX = 10; + private final LinkedHashMap order = new LinkedHashMap<>(); @Override public void add(Task task) { if (task == null) { - return; // фигурные скобки + return; + } + int id = task.getId(); + // Дедупликация. + order.remove(id); + order.put(id, task); + // Ограничиваем размер. + while (order.size() > MAX) { + Integer firstKey = order.keySet().iterator().next(); + order.remove(firstKey); } - - /* если id уже есть — убираем старый узел */ - Node duplicate = index.remove(task.getId()); - removeNode(duplicate); - - /* вносим новый просмотр */ - linkLast(task); - index.put(task.getId(), tail); } @Override public void remove(int id) { - Node node = index.remove(id); - removeNode(node); + order.remove(id); } @Override public List getHistory() { - return getTasks(); + return new ArrayList<>(order.values()); } } diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java index 8dc000e..c33afdc 100644 --- a/src/manager/InMemoryTaskManager.java +++ b/src/manager/InMemoryTaskManager.java @@ -1,11 +1,22 @@ package manager; +import exceptions.TaskValidationException; // NEW (sprint-8) import model.*; +import java.time.Duration; // NEW (sprint-8) +import java.time.LocalDateTime; // NEW (sprint-8) import java.util.*; +import java.util.stream.Collectors; /** * InMemoryTaskManager хранит задачи в памяти и ведет историю. + * + *

CHANGED (sprint-8): + * - приоритизация через TreeSet (startTime); + * - проверка пересечений при add/update Task/Subtask; + * - эпики получают расчётные duration/start/end от подзадач; + * - добавлены protected put*-методы и setNextIdAfterRestore для FileBacked; + * - часть циклов переписана на stream API. */ public class InMemoryTaskManager implements TaskManager { @@ -15,28 +26,52 @@ public class InMemoryTaskManager implements TaskManager { protected final Map subtasks = new HashMap<>(); /* ---------- история ---------- */ - private final HistoryManager historyManager = Managers.getDefaultHistory(); + private final HistoryManager historyManager = new InMemoryHistoryManager(); - /* ---------- генератор ID ---------- */ + /* ---------- ID ---------- */ protected int nextId = 1; private int generateId() { return nextId++; } + /* ---------- приоритизация (sprint-8) ---------- */ + // Задачи без startTime сюда не добавляем (по ТЗ). + private final Comparator PRIORITY_CMP = + Comparator.comparing(Task::getStartTime, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparingInt(Task::getId); + + private final NavigableSet prioritized = new TreeSet<>(PRIORITY_CMP); + + private void trackPrioritized(Task task) { + if (task != null && task.getStartTime() != null) { + prioritized.remove(task); + prioritized.add(task); + } else { + prioritized.remove(task); + } + } + /* ---------- создание ---------- */ + @Override public int addNewTask(Task task) { - task.setId(generateId()); - tasks.put(task.getId(), task); - return task.getId(); + // NEW (sprint-8): валидация пересечений + validateNoOverlaps(task, null); + int id = generateId(); + task.setId(id); + tasks.put(id, task); + trackPrioritized(task); + return id; } @Override public int addNewEpic(Epic epic) { - epic.setId(generateId()); - epics.put(epic.getId(), epic); - return epic.getId(); + int id = generateId(); + epic.setId(id); + epics.put(id, epic); + // у эпика вычисляемые поля — посчитаются, когда появятся subtask + return id; } @Override @@ -45,48 +80,74 @@ public int addNewSubtask(Subtask subtask) { if (epic == null) { throw new IllegalArgumentException("Эпик не найден"); } - subtask.setId(generateId()); - subtasks.put(subtask.getId(), subtask); - epic.addSubtaskId(subtask.getId()); - return subtask.getId(); + validateNoOverlaps(subtask, null); + int id = generateId(); + subtask.setId(id); + subtasks.put(id, subtask); + epic.addSubtaskId(id); + trackPrioritized(subtask); + recalcEpic(epic.getId()); + return id; } /* ---------- обновление ---------- */ + @Override public void updateTask(Task task) { - if (tasks.containsKey(task.getId())) { - tasks.put(task.getId(), task); + if (!tasks.containsKey(task.getId())) { + return; } + validateNoOverlaps(task, task.getId()); + tasks.put(task.getId(), task); + trackPrioritized(task); } @Override public void updateEpic(Epic epic) { - if (epics.containsKey(epic.getId())) { - epics.put(epic.getId(), epic); + if (!epics.containsKey(epic.getId())) { + return; } + // статус/время эпика пересчитывается от subtask — но позволим обновить title/description + Epic exist = epics.get(epic.getId()); + exist.setTitle(epic.getTitle()); + exist.setDescription(epic.getDescription()); + // статус руками не трогаем + recalcEpic(exist.getId()); } @Override - public void updateSubtask(Subtask s) { - if (subtasks.containsKey(s.getId())) { - subtasks.put(s.getId(), s); + public void updateSubtask(Subtask subtask) { + if (!subtasks.containsKey(subtask.getId())) { + return; } + validateNoOverlaps(subtask, subtask.getId()); + subtasks.put(subtask.getId(), subtask); + trackPrioritized(subtask); + recalcEpic(subtask.getEpicId()); } /* ---------- удаление ---------- */ + @Override public void removeTask(int id) { - tasks.remove(id); - historyManager.remove(id); + Task removed = tasks.remove(id); + if (removed != null) { + prioritized.remove(removed); + historyManager.remove(id); + } } @Override public void removeEpic(int id) { Epic epic = epics.remove(id); if (epic != null) { + // удаляем все подзадачи эпика for (int sid : epic.getSubtaskIds()) { - subtasks.remove(sid); - historyManager.remove(sid); + Subtask s = subtasks.remove(sid); + if (s != null) { + prioritized.remove(s); + historyManager.remove(sid); + } } historyManager.remove(id); } @@ -100,11 +161,16 @@ public void removeSubtask(int id) { if (epic != null) { epic.getSubtaskIds().remove((Integer) id); } + prioritized.remove(s); + historyManager.remove(id); + if (epic != null) { + recalcEpic(epic.getId()); + } } - historyManager.remove(id); } /* ---------- получение + история ---------- */ + @Override public Task getTask(int id) { Task t = tasks.get(id); @@ -133,6 +199,7 @@ public Subtask getSubtask(int id) { } /* ---------- списки ---------- */ + @Override public List getTasks() { return new ArrayList<>(tasks.values()); @@ -150,17 +217,13 @@ public List getSubtasks() { @Override public List getEpicSubtasks(int epicId) { - List result = new ArrayList<>(); - Epic epic = epics.get(epicId); - if (epic != null) { - for (int id : epic.getSubtaskIds()) { - Subtask s = subtasks.get(id); - if (s != null) { - result.add(s); - } - } - } - return result; + // NEW (sprint-8): Stream API вместо временного списка + return epics.containsKey(epicId) + ? epics.get(epicId).getSubtaskIds().stream() + .map(subtasks::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + : List.of(); } @Override @@ -168,37 +231,129 @@ public List getHistory() { return historyManager.getHistory(); } - /* ---------- защищённые хуки для восстановления из файла ---------- */ + /* ---------- prioritized (sprint-8) ---------- */ - /** Кладем задачу с уже заданным id (не трогаем историю, TODO: без увеличения nextId). */ - protected void putTaskPreserveId(Task task) { - tasks.put(task.getId(), task); - bumpNextId(task.getId()); + @Override + public List getPrioritizedTasks() { + // ожидается частый вызов → O(n) + return new ArrayList<>(prioritized); } - /** Кладем эпик с уже заданным id. */ - protected void putEpicPreserveId(Epic epic) { - epics.put(epic.getId(), epic); - bumpNextId(epic.getId()); + /* ---------- расчёт эпика (status/duration/start/end) ---------- */ + + private void recalcEpic(int epicId) { + Epic epic = epics.get(epicId); + if (epic == null) { + return; + } + + List subs = + epic.getSubtaskIds().stream().map(subtasks::get).filter(Objects::nonNull).toList(); + + // статус + if (subs.isEmpty()) { + epic.setStatus(Status.NEW); + } else { + boolean allNew = subs.stream().allMatch(s -> s.getStatus() == Status.NEW); + boolean allDone = subs.stream().allMatch(s -> s.getStatus() == Status.DONE); + if (allNew) { + epic.setStatus(Status.NEW); + } else if (allDone) { + epic.setStatus(Status.DONE); + } else { + epic.setStatus(Status.IN_PROGRESS); + } + } + + // duration = сумма минут (null считаем как 0) + long minutes = + subs.stream() + .map(Subtask::getDuration) + .filter(Objects::nonNull) + .mapToLong(Duration::toMinutes) + .sum(); + epic.setCalculatedDuration(minutes == 0 ? null : Duration.ofMinutes(minutes)); + + // start = минимальный start subtask; end = макс end + LocalDateTime start = + subs.stream() + .map(Subtask::getStartTime) + .filter(Objects::nonNull) + .min(LocalDateTime::compareTo) + .orElse(null); + + LocalDateTime end = + subs.stream() + .map(Subtask::getEndTime) + .filter(Objects::nonNull) + .max(LocalDateTime::compareTo) + .orElse(null); + + epic.setCalculatedStart(start); + epic.setCalculatedEnd(end); } - /** Кладем Subtask с уже заданным id и привязываем к эпику. */ - protected void putSubtaskPreserveId(Subtask subtask) { - subtasks.put(subtask.getId(), subtask); - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - epic.addSubtaskId(subtask.getId()); + /* ---------- пересечения (sprint-8) ---------- */ + + private void validateNoOverlaps(Task candidate, Integer selfId) { + // Не проверяем, если нет времени или длительности. + if (candidate.getStartTime() == null || candidate.getDuration() == null) { + return; } - bumpNextId(subtask.getId()); + + boolean intersect = + prioritized.stream() + .filter(t -> selfId == null || t.getId() != selfId) + .anyMatch(t -> isOverlap(candidate, t)); + + if (intersect) { + throw new TaskValidationException("Задача пересекается по времени с другой"); + } + } + + // Пересечение отрезков: [A.start, A.end) и [B.start, B.end) + private static boolean isOverlap(Task a, Task b) { + LocalDateTime as = a.getStartTime(); + LocalDateTime bs = b.getStartTime(); + if (as == null || bs == null) { + return false; + } + var ad = a.getDuration(); + var bd = b.getDuration(); + if (ad == null || bd == null) { + return false; + } + LocalDateTime ae = a.getEndTime(); + LocalDateTime be = b.getEndTime(); + // пересечение при строгом наложении (границы, касающиеся впритык, допустимы) + return as.isBefore(be) && bs.isBefore(ae); } - private void bumpNextId(int usedId) { - if (usedId >= nextId) { - nextId = usedId + 1; + + /* ---------- поддержка FileBacked (preserve id / nextId) ---------- */ + + // Восстановление с сохранением id (используется FileBackedTaskManager.restore()) + protected void putTaskPreserveId(Task t) { + tasks.put(t.getId(), t); + trackPrioritized(t); + } + + protected void putEpicPreserveId(Epic e) { + epics.put(e.getId(), e); + // пересчёт сделаем после загрузки всех subtask + } + + protected void putSubtaskPreserveId(Subtask s) { + subtasks.put(s.getId(), s); + Epic epic = epics.get(s.getEpicId()); + if (epic != null) { + epic.addSubtaskId(s.getId()); } + trackPrioritized(s); } - /** Вызывается после восстановления, чтобы новые id шли дальше. */ - protected void setNextIdAfterRestore(int nextId) { - this.nextId = Math.max(this.nextId, nextId); + protected void setNextIdAfterRestore(int next) { + this.nextId = Math.max(this.nextId, next); + // После полного restore пересчитаем эпики: + epics.keySet().forEach(this::recalcEpic); } } diff --git a/src/manager/ManagerSaveException.java b/src/manager/ManagerSaveException.java index 2e38139..68d0e6e 100644 --- a/src/manager/ManagerSaveException.java +++ b/src/manager/ManagerSaveException.java @@ -1,10 +1,12 @@ package manager; -/** - * Наша обёртка над IOException, чтобы не менять сигнатуры интерфейса. - */ public class ManagerSaveException extends RuntimeException { + + public ManagerSaveException(String message) { + super(message); + } + public ManagerSaveException(String message, Throwable cause) { super(message, cause); } -} \ No newline at end of file +} diff --git a/src/manager/Managers.java b/src/manager/Managers.java index a42e7c7..d08561c 100644 --- a/src/manager/Managers.java +++ b/src/manager/Managers.java @@ -1,13 +1,16 @@ package manager; -//Упрощает инициализацию в точке входа (Main) и тестах. -//Используется для создания реализаций {TaskManager} и {HistoryManager}. -public class Managers { - //Возвращает реализацию менеджера задач, работающую в памяти. + +public final class Managers { + + private Managers() {} + public static TaskManager getDefault() { return new InMemoryTaskManager(); } -//Возвращает реализацию менеджера истории просмотров, работающую в памяти. + + // NEW для истории + @SuppressWarnings("unused") public static HistoryManager getDefaultHistory() { return new InMemoryHistoryManager(); } -} \ No newline at end of file +} diff --git a/src/manager/TaskManager.java b/src/manager/TaskManager.java index 6c0c8c3..94bb687 100644 --- a/src/manager/TaskManager.java +++ b/src/manager/TaskManager.java @@ -8,92 +8,56 @@ /** * Интерфейс менеджера задач. - * Определяет операции для создания, изменения, удаления и получения - * задач всех типов (Task, Epic, Subtask), а также получения истории просмотров. + * (из Sprint 7 + NEW методы Sprint 8) */ public interface TaskManager { /* ===================== Создание ===================== */ - - /** - * Добавить обычную задачу. - * @param task задача - * @return присвоенный идентификатор - */ int addNewTask(Task task); - /** - * Добавить эпик. - * @param epic эпик - * @return присвоенный идентификатор - */ int addNewEpic(Epic epic); - /** - * Добавить подзадачу. - * @param subtask подзадача - * @return присвоенный идентификатор - */ int addNewSubtask(Subtask subtask); /* ===================== Обновление ===================== */ - - /** - * Обновить обычную задачу (по её id внутри объекта). - * Если задачи с таким id нет — обновление игнорируется. - */ void updateTask(Task task); - /** - * Обновить эпик (по его id внутри объекта). - * Если эпика с таким id нет — обновление игнорируется. - */ void updateEpic(Epic epic); - /** - * Обновить подзадачу (по её id внутри объекта). - * Если подзадачи с таким id нет — обновление игнорируется. - */ void updateSubtask(Subtask subtask); /* ===================== Удаление ===================== */ - - /** Удалить обычную задачу по id. */ void removeTask(int id); - /** Удалить эпик по id (его подзадачи тоже удаляются). */ void removeEpic(int id); - /** Удалить подзадачу по id. */ void removeSubtask(int id); /* ===================== Получение (одна) ===================== */ - - /** Получить задачу по id (добавляется в историю просмотров). */ - Task getTask(int id); - /** Получить эпик по id (добавляется в историю просмотров). */ + Epic getEpic(int id); - /** Получить подзадачу по id (добавляется в историю просмотров). */ Subtask getSubtask(int id); /* ===================== Получение (списки) ===================== */ - - /** Все обычные задачи. */ List getTasks(); - /** Все эпики. */ List getEpics(); - /** Все подзадачи. */ List getSubtasks(); - /** Подзадачи конкретного эпика. */ List getEpicSubtasks(int epicId); /* ===================== История ===================== */ - - /** Последние просмотренные задачи (до 10, без дублей). */ List getHistory(); + + /* ===================== Prioritized (sprint-8) ===================== */ + + /** + * NEW (sprint-8): задачи и подзадачи в порядке приоритета по startTime. + * Эпики не включаем (их время расчётное). + * Задачи без startTime не учитываются. + */ + List getPrioritizedTasks(); } diff --git a/src/model/Epic.java b/src/model/Epic.java index 4eecf5f..cde60c0 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,14 +1,26 @@ package model; +import java.time.Duration; // NEW (sprint-8) +import java.time.LocalDateTime; // NEW (sprint-8) import java.util.ArrayList; import java.util.List; -/** Эпик — задача, содержащая подзадачи. */ +/** + * Эпик объединяет подзадачи. + * CHANGED (sprint-8): + * - duration и startTime/ endTime — расчётные поля (как status); + * - добавлены сеттеры-пакетные для пересчёта менеджером. + */ +@SuppressWarnings("DuplicatedCode") public class Epic extends Task { + private final List subtaskIds = new ArrayList<>(); + // NEW (sprint-8) — расчётные поля + private LocalDateTime endTime; + public Epic(String title, String description) { - super(title, description, Status.NEW); + super(title, description); } @Override @@ -16,16 +28,54 @@ public TaskType getType() { return TaskType.EPIC; } - /** Возвращает копию списка идентификаторов подзадач. */ + /* ---------- подзадачи ---------- */ + public List getSubtaskIds() { - return new ArrayList<>(subtaskIds); + return subtaskIds; } - /** Добавляет id подзадачи (эпик не может ссылаться сам на себя). */ public void addSubtaskId(int id) { - if (id == this.id) { - throw new IllegalArgumentException("Эпик не может быть собственным Subtask"); - } subtaskIds.add(id); } + + /* ---------- переопределения расчётных полей ---------- */ + + // стало (public): + public void setCalculatedDuration(Duration duration) { + this.duration = duration; + } + + public void setCalculatedStart(LocalDateTime start) { + this.startTime = start; + } + + public void setCalculatedEnd(LocalDateTime end) { + this.endTime = end; + } + + @Override + public LocalDateTime getEndTime() { + return endTime; + } + + @Override + public String toCsvRow() { + String dur = duration == null ? "" : String.valueOf(duration.toMinutes()); + String st = startTime == null ? "" : startTime.format(CSV_TIME_FMT); + return String.join( + ",", + String.valueOf(id), + getType().name(), + escape(title), + status.name(), + escape(description), + dur, + st, + "" // epic + ); + } + + private static String escape(String s) { + return s == null ? "" : s; + } } diff --git a/src/model/Subtask.java b/src/model/Subtask.java index df813ed..4d3dfed 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -1,34 +1,72 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * Подзадача, привязанная к эпику. + * + *

CHANGED (sprint-8): унаследованы duration/startTime/getEndTime от Task. + */ public class Subtask extends Task { - private final int epicId; + + private int epicId; public Subtask(String title, String description, int epicId) { - super(title, description, Status.NEW); - if (epicId <= 0) { - throw new IllegalArgumentException("Неверный epicId"); - } + super(title, description); this.epicId = epicId; } - @Override - public TaskType getType() { - return TaskType.SUBTASK; + public Subtask(String title, String description, Status status, int epicId) { + super(title, description, status); + this.epicId = epicId; } public int getEpicId() { return epicId; } + @SuppressWarnings("unused") + public void setEpicId(int epicId) { + this.epicId = epicId; + } + + @Override + public TaskType getType() { + return TaskType.SUBTASK; + } + @Override public String toCsvRow() { - return String.join(",", + String dur = duration == null ? "" : String.valueOf(duration.toMinutes()); + String st = startTime == null ? "" : startTime.format(CSV_TIME_FMT); + return String.join( + ",", String.valueOf(id), getType().name(), - title, + escape(title), status.name(), - description, + escape(description), + dur, + st, String.valueOf(epicId) ); } + + private static String escape(String s) { + return s == null ? "" : s; + } + + // Удобные fluent-сеттеры TODO:(по желанию) + @SuppressWarnings("unused") + public Subtask withStart(LocalDateTime start) { + this.startTime = start; + return this; + } + + @SuppressWarnings("unused") + public Subtask withDuration(Duration d) { + this.duration = d; + return this; + } } diff --git a/src/model/Task.java b/src/model/Task.java index fcf9174..642e3f8 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,26 +1,55 @@ package model; +import java.time.Duration; // NEW (sprint-8) +import java.time.LocalDateTime; // NEW (sprint-8) +import java.time.format.DateTimeFormatter; import java.util.Objects; -/** Базовая задача. */ +/** + * Базовая задача. + *

CHANGED (sprint-8): + * - добавлены поля duration и startTime; + * - добавлен getEndTime(); + * - расширена CSV-строка (toCsvRow) на durationMinutes и startTime. + */ +@SuppressWarnings("DuplicatedCode") public class Task { + protected int id; protected String title; protected String description; - protected int id; - protected Status status; + protected Status status = Status.NEW; - public Task(String title, String description, Status status) { + // NEW (sprint-8) + protected Duration duration; // оценка длительности (минуты) + protected LocalDateTime startTime; // когда начать + + // CSV-формат времени. Сохраняем человеко читаемо. + public static final DateTimeFormatter CSV_TIME_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + // NEW (sprint-8): удобный конструктор под тесты/инициализацию + public Task( + String title, + String description, + java.time.Duration duration, + java.time.LocalDateTime startTime) { + this(title, description); + this.duration = duration; + this.startTime = startTime; + } + + public Task(String title, String description) { this.title = title; this.description = description; - this.status = status; } - /** Тип задачи. */ - public TaskType getType() { - return TaskType.TASK; + public Task(String title, String description, Status status) { + this.title = title; + this.description = description; + this.status = status; } - /* ========= геттеры/сеттеры ========= */ + /* ---------- геттеры/сеттеры ---------- */ public int getId() { return id; @@ -30,14 +59,26 @@ public void setId(int id) { this.id = id; } + public TaskType getType() { + return TaskType.TASK; + } + public String getTitle() { return title; } + public void setTitle(String title) { + this.title = title; + } + public String getDescription() { return description; } + public void setDescription(String description) { + this.description = description; + } + public Status getStatus() { return status; } @@ -46,42 +87,93 @@ public void setStatus(Status status) { this.status = status; } - /* ========= CSV-представление (для FileBackedTaskManager) ========= */ + // NEW (sprint-8) + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + // NEW (sprint-8) + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + // NEW (sprint-8): вычисляем завершение как start + duration + public LocalDateTime getEndTime() { + if (startTime == null || duration == null) { + return null; + } + return startTime.plus(duration); + } + + /* ---------- CSV ---------- */ + + /** + * Возвращает CSV-строку в формате: + * id,type,name,status,description,durationMinutes,startTime,epic + * Для Task поле epic — пустое. + */ public String toCsvRow() { - // у обычной задачи поле epic пустое - return String.join(",", - String.valueOf(getId()), + String dur = duration == null ? "" : String.valueOf(duration.toMinutes()); + String st = startTime == null ? "" : startTime.format(CSV_TIME_FMT); + return String.join( + ",", + String.valueOf(id), getType().name(), - getTitle(), - getStatus().name(), - getDescription(), - "" + escape(title), + status.name(), + escape(description), + dur, + st, + "" // epic ); } + // экранирование запятых + private static String escape(String s) { + return s == null ? "" : s; + } + + /* ---------- equals/hashCode ---------- */ + @Override public boolean equals(Object o) { if (this == o) { return true; } - // pattern matching TODO: — убирает предупреждение «Variable 'task' can be replaced with pattern variable» - if (!(o instanceof Task other)) { + if (!(o instanceof Task task)) { return false; } - return id == other.id; + return id == task.id + && Objects.equals(title, task.title) + && Objects.equals(description, task.description) + && status == task.status + && Objects.equals(duration, task.duration) + && Objects.equals(startTime, task.startTime) + && getType() == task.getType(); } @Override public int hashCode() { - return Objects.hash(id); + return Objects.hash(id, title, description, status, duration, startTime, getType()); } @Override public String toString() { - return getClass().getSimpleName() + "{" + - "id=" + id + - ", title='" + title + '\'' + - ", status=" + status + - '}'; + return getType() + + "{" + + "id=" + id + + ", title='" + title + '\'' + + ", status=" + status + + ", duration=" + (duration == null ? "null" : duration.toMinutes() + "m") + + ", startTime=" + startTime + + '}'; } } diff --git a/src/test/manager/EpicStatusTest.java b/src/test/manager/EpicStatusTest.java new file mode 100644 index 0000000..edebe56 --- /dev/null +++ b/src/test/manager/EpicStatusTest.java @@ -0,0 +1,66 @@ +package manager; + +import java.io.File; +import model.Epic; +import model.Status; +import model.Subtask; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class EpicStatusTest { + + // Проверяем правила статуса эпика на InMemory + @Test + void epicStatusRules_inMemory() { + assertEpicStatusRules(new InMemoryTaskManager()); + } + + // И то же самое на FileBacked + @Test + void epicStatusRules_fileBacked() throws Exception { + File tmp = File.createTempFile("tasks", ".csv"); + tmp.deleteOnExit(); + assertEpicStatusRules(new FileBackedTaskManager(tmp)); + } + + // Общая проверка правил из ТЗ: + // a) без подзадач → NEW + // b) все подзадачи NEW → NEW + // c) все подзадачи DONE → DONE + // d) NEW + DONE → IN_PROGRESS + // e) есть хотя бы одна IN_PROGRESS → IN_PROGRESS + private void assertEpicStatusRules(TaskManager manager) { + int epicId = manager.addNewEpic(new Epic("E", "desc")); + + // a) без подзадач + assertEquals(Status.NEW, manager.getEpic(epicId).getStatus()); + + int s1 = manager.addNewSubtask(new Subtask("s1", "", epicId)); + int s2 = manager.addNewSubtask(new Subtask("s2", "", epicId)); + + // b) все NEW + assertEquals(Status.NEW, manager.getEpic(epicId).getStatus()); + + // c) все DONE + var u1 = manager.getSubtask(s1); + u1.setStatus(Status.DONE); + manager.updateSubtask(u1); + + var u2 = manager.getSubtask(s2); + u2.setStatus(Status.DONE); + manager.updateSubtask(u2); + + assertEquals(Status.DONE, manager.getEpic(epicId).getStatus()); + + // d) NEW + DONE → IN_PROGRESS + u1.setStatus(Status.NEW); + manager.updateSubtask(u1); + assertEquals(Status.IN_PROGRESS, manager.getEpic(epicId).getStatus()); + + // e) есть хотя бы одна IN_PROGRESS → IN_PROGRESS + u2.setStatus(Status.IN_PROGRESS); + manager.updateSubtask(u2); + assertEquals(Status.IN_PROGRESS, manager.getEpic(epicId).getStatus()); + } +} diff --git a/src/test/manager/FileBackedTaskManagerTest.java b/src/test/manager/FileBackedTaskManagerTest.java new file mode 100644 index 0000000..06ccafa --- /dev/null +++ b/src/test/manager/FileBackedTaskManagerTest.java @@ -0,0 +1,41 @@ +package manager; + +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +class FileBackedTaskManagerTest extends TaskManagerTest { + + private File tmp; + + @BeforeEach + void initFile() throws Exception { + // можно оставить — не мешает + tmp = File.createTempFile("tasks", ".csv"); + tmp.deleteOnExit(); + } + + @AfterEach + void cleanup() { + if (tmp != null) { + if (!tmp.delete()) { + // на Windows файл может быть занят — удалим при выходе + tmp.deleteOnExit(); + } + } + } + + @Override + protected FileBackedTaskManager createManager() { + if (tmp == null) { // <-- гарантия при вызове из super.setUp() + try { + tmp = File.createTempFile("tasks", ".csv"); + tmp.deleteOnExit(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new FileBackedTaskManager(tmp); + } +} diff --git a/src/test/manager/HistoryManagerTest.java b/src/test/manager/HistoryManagerTest.java deleted file mode 100644 index 928c3a3..0000000 --- a/src/test/manager/HistoryManagerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package manager; - - -import model.Task; -import model.Status; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit-тесты самого HistoryManager - */ -class HistoryManagerTest { - - private HistoryManager hm; - - private Task t1; - private Task t2; - private Task t3; - - @BeforeEach - void setUp() { - hm = Managers.getDefaultHistory(); // или new InMemoryHistoryManager() - - // id задаём вручную, чтобы HistoryManager.remove(id) работал корректно - t1 = new Task("T-1", "description1", Status.NEW); t1.setId(1); - t2 = new Task("T-2", "description2", Status.NEW); t2.setId(2); - t3 = new Task("T-3", "description3", Status.NEW); t3.setId(3); - } - - /** Add(): без дубликатов, последний просмотр переносится в конец */ - @Test - void add_movesTaskToTail_withoutDuplicates() { - hm.add(t1); - hm.add(t2); - hm.add(t3); - hm.add(t2); // повторный просмотр t2 - - List history = hm.getHistory(); - assertEquals(List.of(t1, t3, t2), history, - "Повторный просмотр должен перемещать задачу в конец истории без дублирования"); - } - - /** Remove(): удаляет узел из середины за O(1) */ - @Test - void remove_deletesNodeFromAnyPosition() { - hm.add(t1); - hm.add(t2); - hm.add(t3); - - hm.remove(2); // удаляем t2 (из середины списка) - - List history = hm.getHistory(); - assertEquals(List.of(t1, t3), history, - "После удаления задачи из середины истории в списке должны остаться t1 и t3"); - } -} diff --git a/src/test/manager/InMemoryHistoryManagerTest.java b/src/test/manager/InMemoryHistoryManagerTest.java new file mode 100644 index 0000000..ed94f45 --- /dev/null +++ b/src/test/manager/InMemoryHistoryManagerTest.java @@ -0,0 +1,54 @@ +package manager; + +import model.Task; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class InMemoryHistoryManagerTest { + + @Test + void emptyHistory_ok() { + InMemoryHistoryManager h = new InMemoryHistoryManager(); + assertTrue(h.getHistory().isEmpty()); + } + + @Test + void noDuplicates_limit10() { + InMemoryHistoryManager h = new InMemoryHistoryManager(); + for (int i = 0; i < 12; i++) { + Task t = new Task("T" + i, "", Duration.ofMinutes(1), LocalDateTime.now()); + t.setId(i + 1); + h.add(t); + h.add(t); + } + assertEquals(10, h.getHistory().size()); + } + + @Test + void remove_edges() { + InMemoryHistoryManager h = new InMemoryHistoryManager(); + + Task a = new Task("A", "", Duration.ofMinutes(1), LocalDateTime.now()); + a.setId(1); + + Task b = new Task("B", "", Duration.ofMinutes(1), LocalDateTime.now()); + b.setId(2); + + Task c = new Task("C", "", Duration.ofMinutes(1), LocalDateTime.now()); + c.setId(3); + + h.add(a); + h.add(b); + h.add(c); + + h.remove(1); // начало + h.remove(2); // середина (после удаления 1 останутся [b, c]) + h.remove(3); // конец + + assertTrue(h.getHistory().isEmpty()); + } +} diff --git a/src/test/manager/InMemoryTaskManagerTest.java b/src/test/manager/InMemoryTaskManagerTest.java index 6841ce0..002850a 100644 --- a/src/test/manager/InMemoryTaskManagerTest.java +++ b/src/test/manager/InMemoryTaskManagerTest.java @@ -1,64 +1,9 @@ package manager; -import model.Status; -import model.Task; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +class InMemoryTaskManagerTest extends TaskManagerTest { -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - - -/** - * Unit-тесты InMemoryTaskManager + HistoryManager. - */ -class InMemoryTaskManagerTest { - - private TaskManager tm; - - @BeforeEach - void setUp() { - tm = Managers.getDefault(); // InMemoryTaskManager - } - - /* -------- 1. Дубликаты не сохраняются -------- */ - @Test - void addDuplicates_keepsOnlyLastView() { - int id = tm.addNewTask(new Task("T", "d", Status.NEW)); - - tm.getTask(id); - tm.getTask(id); - tm.getTask(id); - - List history = tm.getHistory(); - assertEquals(1, history.size(), - "В истории должен остаться единственный просмотр"); - assertEquals(id, history.get(0).getId());//TODO: если именно нужно от Java 21 и более то нужно заменить на - // assertEquals(id, history.getFirst().getId()); - } - - /* -------- 2. История может быть > 10 -------- */ - @Test - void historyCanGrowMoreThanTen() { - for (int i = 0; i < 20; i++) { - int id = tm.addNewTask(new Task("task-" + i, "", Status.NEW)); - tm.getTask(id); - } - assertEquals(20, tm.getHistory().size(), - "История должна содержать все 20 просмотров"); - } - - /* -------- 3. Удаление чистит историю -------- */ - @Test - void deletingTask_removesItFromHistory() { - int id = tm.addNewTask(new Task("X", "", Status.NEW)); - tm.getTask(id); - - tm.removeTask(id); - - assertTrue(tm.getHistory().isEmpty(), - "После удаления задачи записи о ней в истории быть не должно"); + @Override + protected InMemoryTaskManager createManager() { + return new InMemoryTaskManager(); } } diff --git a/src/test/manager/TaskManagerHistoryIntegrationTest.java b/src/test/manager/TaskManagerHistoryIntegrationTest.java deleted file mode 100644 index 26e42b4..0000000 --- a/src/test/manager/TaskManagerHistoryIntegrationTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package manager; - - -import model.Task; -import model.Status; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Интеграционный тест: TaskManager ↔ HistoryManager - */ -class TaskManagerHistoryIntegrationTest { - - private TaskManager tm; - - @BeforeEach - void setUp() { - tm = Managers.getDefault(); // new InMemoryTaskManager() - } - - /** Удаление задачи очищает историю */ - @Test - void deletingTask_removesItFromHistory() { - int id = tm.addNewTask(new Task("Task-1", "description", Status.NEW)); - - tm.getTask(id); // помещаем в историю - assertEquals(1, tm.getHistory().size(), - "После просмотра история должна содержать одну запись"); - - tm.removeTask(id); // удаляем задачу - assertTrue(tm.getHistory().isEmpty(), - "После удаления задачи история должна быть пустой"); - } -} diff --git a/src/test/manager/TaskManagerTest.java b/src/test/manager/TaskManagerTest.java new file mode 100644 index 0000000..8e8e776 --- /dev/null +++ b/src/test/manager/TaskManagerTest.java @@ -0,0 +1,108 @@ +package manager; + +import model.*; +import org.junit.jupiter.api.BeforeEach; +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 abstract class TaskManagerTest { + + protected T manager; + + protected abstract T createManager(); + + @BeforeEach + void setUp() { + manager = createManager(); + } + + @Test + void epicStatusRules() { + int epicId = manager.addNewEpic(new Epic("E", "d")); + + // a) все NEW + int s1 = manager.addNewSubtask(new Subtask("s1", "", epicId)); + int s2 = manager.addNewSubtask(new Subtask("s2", "", epicId)); + assertEquals(Status.NEW, manager.getEpic(epicId).getStatus()); + + // b) все DONE + Subtask us1 = manager.getSubtask(s1); + us1.setStatus(Status.DONE); + manager.updateSubtask(us1); + + Subtask us2 = manager.getSubtask(s2); + us2.setStatus(Status.DONE); + manager.updateSubtask(us2); + + assertEquals(Status.DONE, manager.getEpic(epicId).getStatus()); + + // c) NEW + DONE => IN_PROGRESS + us1.setStatus(Status.NEW); + manager.updateSubtask(us1); + assertEquals(Status.IN_PROGRESS, manager.getEpic(epicId).getStatus()); + + // d) есть IN_PROGRESS => IN_PROGRESS + us2.setStatus(Status.IN_PROGRESS); + manager.updateSubtask(us2); + assertEquals(Status.IN_PROGRESS, manager.getEpic(epicId).getStatus()); + } + + @Test + void prioritizedAndNoIntersections() { + // две задачи подряд без пересечения + Task t1 = new Task("t1", ""); + t1.setStartTime(LocalDateTime.of(2025, 1, 1, 10, 0)); + t1.setDuration(Duration.ofMinutes(30)); + + Task t2 = new Task("t2", ""); + t2.setStartTime(LocalDateTime.of(2025, 1, 1, 10, 30)); + t2.setDuration(Duration.ofMinutes(30)); + + manager.addNewTask(t1); + manager.addNewTask(t2); + + List pr = manager.getPrioritizedTasks(); + assertEquals(2, pr.size()); + assertEquals("t1", pr.get(0).getTitle()); + assertEquals("t2", pr.get(1).getTitle()); + + // пересечение должно падать + Task t3 = new Task("t3", ""); + t3.setStartTime(LocalDateTime.of(2025, 1, 1, 10, 15)); + t3.setDuration(Duration.ofMinutes(30)); + assertThrows(RuntimeException.class, () -> manager.addNewTask(t3)); + } + + @Test + void epicTimeIsCalculatedFromSubtasks() { + int epicId = manager.addNewEpic(new Epic("E", "")); + + Subtask s1 = new Subtask("a", "", epicId); + s1.setStartTime(LocalDateTime.of(2025, 1, 1, 9, 0)); + s1.setDuration(Duration.ofMinutes(30)); + int s1id = manager.addNewSubtask(s1); + + Subtask s2 = new Subtask("b", "", epicId); + s2.setStartTime(LocalDateTime.of(2025, 1, 1, 10, 0)); + s2.setDuration(Duration.ofMinutes(90)); + int s2id = manager.addNewSubtask(s2); + assertTrue(s2id > 0); // используем переменную, чтобы не было предупреждения + + Epic e = manager.getEpic(epicId); + assertEquals(LocalDateTime.of(2025, 1, 1, 9, 0), e.getStartTime()); + assertEquals(LocalDateTime.of(2025, 1, 1, 11, 30), e.getEndTime()); + assertEquals(120, e.getDuration().toMinutes()); // 30 + 90 + + // удалил одну — пересчёт + manager.removeSubtask(s1id); + e = manager.getEpic(epicId); + assertEquals(LocalDateTime.of(2025, 1, 1, 10, 0), e.getStartTime()); + assertEquals(LocalDateTime.of(2025, 1, 1, 11, 30), e.getEndTime()); + assertEquals(90, e.getDuration().toMinutes()); + } +} From 554dd8cfbff64a34acc6c94b8aaf309cfdd00a06 Mon Sep 17 00:00:00 2001 From: Sergey Date: Wed, 13 Aug 2025 18:13:17 +0300 Subject: [PATCH 22/29] tests(prioritized): exclude epics and tasks without startTime from prioritized list --- .../manager/FileBackedLegacyFormatTest.java | 4 ++ src/test/manager/FileBackedRoundTripTest.java | 4 ++ src/test/manager/PrioritizedViewTest.java | 39 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/test/manager/FileBackedLegacyFormatTest.java create mode 100644 src/test/manager/FileBackedRoundTripTest.java create mode 100644 src/test/manager/PrioritizedViewTest.java diff --git a/src/test/manager/FileBackedLegacyFormatTest.java b/src/test/manager/FileBackedLegacyFormatTest.java new file mode 100644 index 0000000..74c7bbf --- /dev/null +++ b/src/test/manager/FileBackedLegacyFormatTest.java @@ -0,0 +1,4 @@ +package manager; + +public class FileBackedLegacyFormatTest { +} diff --git a/src/test/manager/FileBackedRoundTripTest.java b/src/test/manager/FileBackedRoundTripTest.java new file mode 100644 index 0000000..98133cd --- /dev/null +++ b/src/test/manager/FileBackedRoundTripTest.java @@ -0,0 +1,4 @@ +package manager; + +public class FileBackedRoundTripTest { +} diff --git a/src/test/manager/PrioritizedViewTest.java b/src/test/manager/PrioritizedViewTest.java new file mode 100644 index 0000000..0eaf1de --- /dev/null +++ b/src/test/manager/PrioritizedViewTest.java @@ -0,0 +1,39 @@ +package manager; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import model.Epic; +import model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PrioritizedViewTest { + + private InMemoryTaskManager manager; + + @BeforeEach + void setUp() { + manager = new InMemoryTaskManager(); + } + + @Test + void prioritized_excludesEpicsAndNullStart() { + int epicId = manager.addNewEpic(new Epic("E", "d")); + + Task noStart = new Task("noStart", ""); + noStart.setDuration(Duration.ofMinutes(15)); + manager.addNewTask(noStart); + + Task withStart = new Task("withStart", ""); + withStart.setDuration(Duration.ofMinutes(10)); + withStart.setStartTime(LocalDateTime.of(2025, 1, 1, 10, 0)); + manager.addNewTask(withStart); + + List pr = manager.getPrioritizedTasks(); + assertEquals(1, pr.size(), "В приоритизации должны быть только задачи с startTime (без эпиков)"); + assertEquals("withStart", pr.get(0).getTitle()); + } +} From fa7f086647a9806975d9575fcb2621749d419ea2 Mon Sep 17 00:00:00 2001 From: Sergey Date: Wed, 13 Aug 2025 18:14:44 +0300 Subject: [PATCH 23/29] tests: add prioritized and file-backed coverage PrioritizedViewTest: --- .../manager/FileBackedLegacyFormatTest.java | 22 +++++++- src/test/manager/FileBackedRoundTripTest.java | 54 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/test/manager/FileBackedLegacyFormatTest.java b/src/test/manager/FileBackedLegacyFormatTest.java index 74c7bbf..298e002 100644 --- a/src/test/manager/FileBackedLegacyFormatTest.java +++ b/src/test/manager/FileBackedLegacyFormatTest.java @@ -1,4 +1,24 @@ package manager; -public class FileBackedLegacyFormatTest { +import static org.junit.jupiter.api.Assertions.*; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.Test; + +class FileBackedLegacyFormatTest { + + @Test + void legacySprint7Format_isAccepted() throws Exception { + File tmp = File.createTempFile("tasks", ".csv"); + tmp.deleteOnExit(); + String legacy = + "id,type,name,status,description,epic\n" + + "1,TASK,t,NEW,d,\n"; + Files.writeString(tmp.toPath(), legacy, StandardCharsets.UTF_8); + + FileBackedTaskManager m = FileBackedTaskManager.loadFromFile(tmp); + assertEquals(1, m.getTasks().size()); + assertEquals("t", m.getTasks().get(0).getTitle()); + } } diff --git a/src/test/manager/FileBackedRoundTripTest.java b/src/test/manager/FileBackedRoundTripTest.java index 98133cd..b60f59a 100644 --- a/src/test/manager/FileBackedRoundTripTest.java +++ b/src/test/manager/FileBackedRoundTripTest.java @@ -1,4 +1,56 @@ package manager; -public class FileBackedRoundTripTest { +import static org.junit.jupiter.api.Assertions.*; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.time.LocalDateTime; +import model.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FileBackedRoundTripTest { + + private File tmp; + + @BeforeEach + void init() throws Exception { + tmp = File.createTempFile("tasks", ".csv"); + tmp.deleteOnExit(); + } + + @AfterEach + void cleanup() { + if (tmp != null && !tmp.delete()) { + tmp.deleteOnExit(); + } + } + + @Test + void saveAndLoad_doesNotThrow() { + assertDoesNotThrow(() -> { + FileBackedTaskManager m1 = new FileBackedTaskManager(tmp); + Task t = new Task("t", "d"); + t.setDuration(Duration.ofMinutes(30)); + t.setStartTime(LocalDateTime.of(2025, 1, 1, 10, 0)); + m1.addNewTask(t); // триггерит save() + + FileBackedTaskManager m2 = FileBackedTaskManager.loadFromFile(tmp); + assertEquals(1, m2.getTasks().size()); + assertEquals("t", m2.getTasks().get(0).getTitle()); + }); + } + + @Test + void malformedCsv_throwsManagerSaveException() throws Exception { + // некорректная строка (ни 6, ни 8 колонок) + String bad = + "id,type,name,status,description,durationMinutes,startTime,epic\n" + + "1,TASK,t,NEW,d,10,2025-01-01 10:00,,"; // лишняя запятая => 9 колонок + Files.writeString(tmp.toPath(), bad, StandardCharsets.UTF_8); + + assertThrows(ManagerSaveException.class, () -> FileBackedTaskManager.loadFromFile(tmp)); + } } From 5de5c43ae5e2ad77fb8286a8835dd8d1200e45be Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 14 Aug 2025 16:20:50 +0300 Subject: [PATCH 24/29] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=D1=80=D1=83=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D0=B9=20escape()=20=D0=B8=D0=B7=20Task;=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=20util.CsvUtil?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/CsvUtils.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/util/CsvUtils.java diff --git a/src/util/CsvUtils.java b/src/util/CsvUtils.java new file mode 100644 index 0000000..4a42234 --- /dev/null +++ b/src/util/CsvUtils.java @@ -0,0 +1,4 @@ +package util; + +public class CsvUtils { +} From 4b01ed4cf6923f335900f5725ce0a30c37dfe815 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 14 Aug 2025 16:26:41 +0300 Subject: [PATCH 25/29] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3(epic):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D1=87=D1=91=D1=82=20=D1=8D=D0=BF=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=20=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=85=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/InMemoryTaskManager.java | 44 ++-------------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java index c33afdc..8afccf2 100644 --- a/src/manager/InMemoryTaskManager.java +++ b/src/manager/InMemoryTaskManager.java @@ -3,7 +3,6 @@ import exceptions.TaskValidationException; // NEW (sprint-8) import model.*; -import java.time.Duration; // NEW (sprint-8) import java.time.LocalDateTime; // NEW (sprint-8) import java.util.*; import java.util.stream.Collectors; @@ -250,47 +249,8 @@ private void recalcEpic(int epicId) { List subs = epic.getSubtaskIds().stream().map(subtasks::get).filter(Objects::nonNull).toList(); - // статус - if (subs.isEmpty()) { - epic.setStatus(Status.NEW); - } else { - boolean allNew = subs.stream().allMatch(s -> s.getStatus() == Status.NEW); - boolean allDone = subs.stream().allMatch(s -> s.getStatus() == Status.DONE); - if (allNew) { - epic.setStatus(Status.NEW); - } else if (allDone) { - epic.setStatus(Status.DONE); - } else { - epic.setStatus(Status.IN_PROGRESS); - } - } - - // duration = сумма минут (null считаем как 0) - long minutes = - subs.stream() - .map(Subtask::getDuration) - .filter(Objects::nonNull) - .mapToLong(Duration::toMinutes) - .sum(); - epic.setCalculatedDuration(minutes == 0 ? null : Duration.ofMinutes(minutes)); - - // start = минимальный start subtask; end = макс end - LocalDateTime start = - subs.stream() - .map(Subtask::getStartTime) - .filter(Objects::nonNull) - .min(LocalDateTime::compareTo) - .orElse(null); - - LocalDateTime end = - subs.stream() - .map(Subtask::getEndTime) - .filter(Objects::nonNull) - .max(LocalDateTime::compareTo) - .orElse(null); - - epic.setCalculatedStart(start); - epic.setCalculatedEnd(end); + // TODO(review sprint-8): пересчёт эпика вынесен в Epic.recalcFromSubtasks — один проход. + epic.recalcFromSubtasks(subs); } /* ---------- пересечения (sprint-8) ---------- */ From 1b61ab14464abbc27c88470fc32942105a3c44db Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 14 Aug 2025 16:26:54 +0300 Subject: [PATCH 26/29] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3(Epic):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20recalcFromSubtasks;=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D1=87=D1=91=D1=82=D0=BD=D1=8B=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20=D1=81=D0=B5=D1=82=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Epic.java | 87 ++++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index cde60c0..114e56d 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,22 +1,17 @@ package model; -import java.time.Duration; // NEW (sprint-8) -import java.time.LocalDateTime; // NEW (sprint-8) +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import util.CsvUtils; /** * Эпик объединяет подзадачи. - * CHANGED (sprint-8): - * - duration и startTime/ endTime — расчётные поля (как status); - * - добавлены сеттеры-пакетные для пересчёта менеджером. */ -@SuppressWarnings("DuplicatedCode") public class Epic extends Task { private final List subtaskIds = new ArrayList<>(); - - // NEW (sprint-8) — расчётные поля private LocalDateTime endTime; public Epic(String title, String description) { @@ -28,8 +23,6 @@ public TaskType getType() { return TaskType.EPIC; } - /* ---------- подзадачи ---------- */ - public List getSubtaskIds() { return subtaskIds; } @@ -38,19 +31,58 @@ public void addSubtaskId(int id) { subtaskIds.add(id); } - /* ---------- переопределения расчётных полей ---------- */ - - // стало (public): - public void setCalculatedDuration(Duration duration) { - this.duration = duration; - } - - public void setCalculatedStart(LocalDateTime start) { - this.startTime = start; - } - - public void setCalculatedEnd(LocalDateTime end) { - this.endTime = end; + // TODO(review sprint-8): пересчёт status/duration/start/end за один проход по сабтаскам. + public void recalcFromSubtasks(List subs) { + if (subs == null || subs.isEmpty()) { + this.status = Status.NEW; + this.duration = null; + this.startTime = null; + this.endTime = null; + return; + } + + boolean allNew = true; + boolean allDone = true; + + long totalMinutes = 0L; + LocalDateTime minStart = null; + LocalDateTime maxEnd = null; + + for (Subtask s : subs) { + Status st = s.getStatus(); + if (st != Status.NEW) { + allNew = false; + } + if (st != Status.DONE) { + allDone = false; + } + + Duration d = s.getDuration(); + if (d != null) { + totalMinutes += d.toMinutes(); + } + + LocalDateTime stTime = s.getStartTime(); + if (stTime != null && (minStart == null || stTime.isBefore(minStart))) { + minStart = stTime; + } + LocalDateTime enTime = s.getEndTime(); + if (enTime != null && (maxEnd == null || enTime.isAfter(maxEnd))) { + maxEnd = enTime; + } + } + + if (allNew) { + this.status = Status.NEW; + } else if (allDone) { + this.status = Status.DONE; + } else { + this.status = Status.IN_PROGRESS; + } + + this.duration = (totalMinutes == 0) ? null : Duration.ofMinutes(totalMinutes); + this.startTime = minStart; + this.endTime = maxEnd; } @Override @@ -58,6 +90,7 @@ public LocalDateTime getEndTime() { return endTime; } + // TODO(review sprint-8): CSV-escape вынесен в util.CsvUtils (убрано дублирование). @Override public String toCsvRow() { String dur = duration == null ? "" : String.valueOf(duration.toMinutes()); @@ -66,16 +99,12 @@ public String toCsvRow() { ",", String.valueOf(id), getType().name(), - escape(title), + CsvUtils.escape(title), status.name(), - escape(description), + CsvUtils.escape(description), dur, st, "" // epic ); } - - private static String escape(String s) { - return s == null ? "" : s; - } } From e769ed398c919eb2bec0d0cd4c361612af3f279a Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 14 Aug 2025 16:26:59 +0300 Subject: [PATCH 27/29] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3(Subtask):=20CSV-escape=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=20CsvUtils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Subtask.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/model/Subtask.java b/src/model/Subtask.java index 4d3dfed..9a7070a 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.time.LocalDateTime; +import util.CsvUtils; // TODO(review sprint-8): используем утилиту для CSV-экранирования /** * Подзадача, привязанная к эпику. @@ -36,6 +37,7 @@ public TaskType getType() { return TaskType.SUBTASK; } + // TODO(review sprint-8): экранирование перенесено в CsvUtils, чтобы не дублировать метод. @Override public String toCsvRow() { String dur = duration == null ? "" : String.valueOf(duration.toMinutes()); @@ -44,20 +46,16 @@ public String toCsvRow() { ",", String.valueOf(id), getType().name(), - escape(title), + CsvUtils.escape(title), status.name(), - escape(description), + CsvUtils.escape(description), dur, st, String.valueOf(epicId) ); } - private static String escape(String s) { - return s == null ? "" : s; - } - - // Удобные fluent-сеттеры TODO:(по желанию) + // Удобные fluent-сеттеры @SuppressWarnings("unused") public Subtask withStart(LocalDateTime start) { this.startTime = start; From 2d75a7897bfe599bf298a6186dcc0b8c9e2ceb56 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 14 Aug 2025 16:27:03 +0300 Subject: [PATCH 28/29] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3(file):=20=D0=BD=D0=B5=D0=B9?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B3=20=D0=B8=20=D1=83=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D1=8B=20CSV;=20maxId=20=D0=B7=D0=B0=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=20=D0=BF=D1=80=D0=BE=D1=85=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manager/FileBackedTaskManager.java | 62 ++++++++------------------ 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java index 1810bdf..0197f69 100644 --- a/src/manager/FileBackedTaskManager.java +++ b/src/manager/FileBackedTaskManager.java @@ -5,6 +5,7 @@ import model.Subtask; import model.Task; import model.TaskType; +import util.CsvUtils; // TODO(review): парсинг времени вынесен в утилиту import java.io.BufferedReader; import java.io.BufferedWriter; @@ -12,10 +13,9 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.time.Duration; // NEW (sprint-8) -import java.time.LocalDateTime; // NEW (sprint-8) +import java.time.Duration; // NEW (sprint-8) +import java.time.LocalDateTime; // NEW (sprint-8) import java.time.format.DateTimeFormatter; // NEW (sprint-8) -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; @@ -86,6 +86,8 @@ private void restore() { List tasks = new ArrayList<>(); List subtasks = new ArrayList<>(); + int maxId = 0; // TODO(review): считаем maxId за один проход при чтении файла + try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { String header = reader.readLine(); // заголовок @@ -99,8 +101,8 @@ private void restore() { continue; } - // внутри restore(), в цикле чтения строк CSV Task task = fromCsv(line); + maxId = Math.max(maxId, task.getId()); // TODO(review): обновляем maxId на лету if (task instanceof Epic epic) { epics.add(epic); @@ -129,43 +131,33 @@ private void restore() { super.putSubtaskPreserveId(subtask); } - // Сдвигаем nextId + пересчитываем эпики (внутри setNextIdAfterRestore) - int maxId = 0; - for (Task task : tasks) { - maxId = Math.max(maxId, task.getId()); - } - for (Epic epic : epics) { - maxId = Math.max(maxId, epic.getId()); - } - for (Subtask subtask : subtasks) { - maxId = Math.max(maxId, subtask.getId()); - } + // TODO(review): без дополнительных циклов — используем maxId, посчитанный при чтении super.setNextIdAfterRestore(maxId + 1); } /* ───────────── CSV утилиты ───────────── */ private static Task fromCsv(String csv) { - String[] p = csv.split(",", -1); + String[] taskParts = csv.split(",", -1); // TODO(review): не используем односимвольные имена // Старый формат Sprint 7: id,type,name,status,description,epic (6 полей, epic только у subtask) // Новый формат Sprint 8: id,type,name,status,description,durationMinutes,startTime,epic (8 полей) - if (p.length != 6 && p.length != 8) { + if (taskParts.length != 6 && taskParts.length != 8) { throw new ManagerSaveException("Некорректная строка CSV: " + csv); } - int id = Integer.parseInt(p[0]); - TaskType type = TaskType.valueOf(p[1]); - String name = p[2]; - Status status = Status.valueOf(p[3]); - String description = p[4]; + int id = Integer.parseInt(taskParts[0]); + TaskType type = TaskType.valueOf(taskParts[1]); + String name = taskParts[2]; + Status status = Status.valueOf(taskParts[3]); + String description = taskParts[4]; - String durStr = p.length == 8 ? p[5] : ""; - String startStr = p.length == 8 ? p[6] : ""; + String durStr = taskParts.length == 8 ? taskParts[5] : ""; + String startStr = taskParts.length == 8 ? taskParts[6] : ""; // после проверки выше длина может быть только 6 или 8 - String epicStr = p.length == 8 ? p[7] : p[5]; + String epicStr = taskParts.length == 8 ? taskParts[7] : taskParts[5]; Duration duration = durStr.isBlank() ? null : Duration.ofMinutes(Long.parseLong(durStr)); - LocalDateTime startTime = parseTimeOrNull(startStr); + LocalDateTime startTime = CsvUtils.parseTimeOrNull(startStr, CSV_TIME_FMT); // TODO(review): парсинг времени из утилиты switch (type) { case TASK: { @@ -195,24 +187,6 @@ private static Task fromCsv(String csv) { } } - private static LocalDateTime parseTimeOrNull(String s) { - if (s == null || s.isBlank()) { - return null; - } - try { - // основной формат - return LocalDateTime.parse(s, CSV_TIME_FMT); - } catch (DateTimeParseException ex) { - try { - // пробуем ISO в случае старого/другого вывода - return LocalDateTime.parse(s); - } catch (DateTimeParseException ex2) { - // мягкая деградация — не срываем загрузку - return null; - } - } - } - /* ───────────── переопределения с автосохранением ───────────── */ @Override From b76bc3199ecf905bf946f3b19da8926ff6e92262 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 14 Aug 2025 16:27:08 +0300 Subject: [PATCH 29/29] =?UTF-8?q?feat(util):=20CsvUtils.escape=20=D0=B8=20?= =?UTF-8?q?CsvUtils.parseTimeOrNull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/CsvUtils.java | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/util/CsvUtils.java b/src/util/CsvUtils.java index 4a42234..ae16816 100644 --- a/src/util/CsvUtils.java +++ b/src/util/CsvUtils.java @@ -1,4 +1,28 @@ package util; -public class CsvUtils { +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +public final class CsvUtils { + private CsvUtils() {} + + public static String escape(String s) { + return s == null ? "" : s; + } + + public static LocalDateTime parseTimeOrNull(String s, DateTimeFormatter fmt) { + if (s == null || s.isBlank()) { + return null; + } + try { + return LocalDateTime.parse(s, fmt); + } catch (DateTimeParseException ex) { + try { + return LocalDateTime.parse(s); // ISO fallback + } catch (DateTimeParseException ignored) { + return null; + } + } + } }