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 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/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 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..7ce16be 100644 --- a/src/manager/InMemoryHistoryManager.java +++ b/src/manager/InMemoryHistoryManager.java @@ -2,22 +2,99 @@ import model.Task; import java.util.*; -//Хранит максимум 10 последних задач. При превышении лимита - // самая старая задача удаляется. + +/** HistoryManager на базе двусвязного списка + HashMap */ 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 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; + } + + 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; + } + + /* ───── HistoryManager API ───── */ @Override public void add(Task task) { - history.addLast(task); - if (history.size() > MAX_HISTORY) { - history.pollFirst(); + 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 node = index.remove(id); + removeNode(node); + } + @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(), + "После удаления задачи история должна быть пустой"); + } +}