diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c801215 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.java] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +max_line_length = 100 +insert_final_newline = true diff --git a/library/gson-2.11.0.jar b/library/gson-2.11.0.jar new file mode 100644 index 0000000..18e59c8 Binary files /dev/null and b/library/gson-2.11.0.jar differ diff --git a/src/Main.java b/src/Main.java index 2da5e79..174596c 100644 --- a/src/Main.java +++ b/src/Main.java @@ -1,68 +1,59 @@ import manager.Managers; import manager.TaskManager; -import model.*; +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; -// Демонстрация базовой работы с менеджером задач public class Main { - public static void main(String[] args) { - TaskManager manager = Managers.getDefault(); - - // === Добавление задач === - 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 - ); - - // === Вывод истории просмотров === - System.out.println("=== История просмотров ==="); - for (Task task : manager.getHistory()) { - System.out.printf( - "%s (ID: %d)\nЗаголовок: %s\nСтатус: %s\n---\n", - getTypeName(task), - task.getId(), - task.getTitle(), - getStatusName(task.getStatus()) - ); - } + public static void main(String[] args) { + TaskManager manager = Managers.getDefault(); + + // Добавление задач + 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)); + + // Краткая сводка + 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); + + // Получения для истории просмотров + manager.getTask(id1); + manager.getEpic(epicId); + manager.getSubtask(subId); + manager.getTask(id1); // повторно + + // Вывод истории просмотров + System.out.println("=== История просмотров ==="); + for (Task task : manager.getHistory()) { + System.out.printf( + "%s (ID: %d)\nЗаголовок: %s\nСтатус: %s\n---\n", + getTypeName(task), task.getId(), task.getTitle(), getStatusName(task.getStatus())); } + } - private static String getTypeName(Task task) { - if (task instanceof Epic) { - return "Эпик"; - } - if (task instanceof Subtask) { - return "Подзадача"; - } - return "Задача"; + private static String getTypeName(Task task) { + if (task instanceof Epic) { + return "Эпик"; } - - private static String getStatusName(Status status) { - return switch (status) { - case NEW -> "Новая"; - case IN_PROGRESS -> "В процессе"; - case DONE -> "Выполнена"; - }; + if (task instanceof Subtask) { + return "Подзадача"; } + return "Задача"; + } + + private static String getStatusName(Status status) { + return switch (status) { + case NEW -> "Новая"; + case IN_PROGRESS -> "В процессе"; + case DONE -> "Выполнена"; + }; + } } diff --git a/src/exceptions/NotFoundException.java b/src/exceptions/NotFoundException.java new file mode 100644 index 0000000..9fb983a --- /dev/null +++ b/src/exceptions/NotFoundException.java @@ -0,0 +1,8 @@ +package exceptions; + +/** sprint-9: сигнализирует об отсутствии сущности в менеджере. */ +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/exceptions/TaskValidationException.java b/src/exceptions/TaskValidationException.java index 875d245..1479e0f 100644 --- a/src/exceptions/TaskValidationException.java +++ b/src/exceptions/TaskValidationException.java @@ -1,8 +1,8 @@ package exceptions; -/** - * NEW (sprint-8): бросается при пересечении задач по времени. - */ +/** NEW (sprint-8): бросается при пересечении задач по времени. */ public class TaskValidationException extends RuntimeException { - public TaskValidationException(String message) { super(message); } + public TaskValidationException(String message) { + super(message); + } } diff --git a/src/http/BaseHttpHandler.java b/src/http/BaseHttpHandler.java new file mode 100644 index 0000000..fcad664 --- /dev/null +++ b/src/http/BaseHttpHandler.java @@ -0,0 +1,49 @@ +package http; // sprint 9 + +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** Общая база для HTTP-обработчиков. Содержит вспомогательные методы. Sprint 9 */ +public abstract class BaseHttpHandler { + + protected String readBody(HttpExchange exchange) throws IOException { // sprint 9 + try (InputStream is = exchange.getRequestBody()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + protected void sendJson(HttpExchange exchange, int status, String json) + throws IOException { // sprint 9 + byte[] resp = json.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json;charset=utf-8"); + exchange.sendResponseHeaders(status, resp.length); + exchange.getResponseBody().write(resp); + exchange.close(); + } + + protected void sendOk(HttpExchange exchange, String json) throws IOException { // 200 + sendJson(exchange, 200, json); + } + + protected void sendCreated(HttpExchange exchange) throws IOException { // 201 + sendJson(exchange, 201, "\"created\""); + } + + protected void sendNotFound(HttpExchange exchange, String message) throws IOException { // 404 + sendJson(exchange, 404, "{\"error\":\"" + escape(message) + "\"}"); + } + + protected void sendHasOverlaps(HttpExchange exchange, String message) throws IOException { // 406 + sendJson(exchange, 406, "{\"error\":\"" + escape(message) + "\"}"); + } + + protected void sendServerError(HttpExchange exchange, String message) throws IOException { // 500 + sendJson(exchange, 500, "{\"error\":\"" + escape(message) + "\"}"); + } + + private String escape(String s) { + return s == null ? "" : s.replace("\"", "\\\""); + } +} diff --git a/src/http/EpicsHandler.java b/src/http/EpicsHandler.java new file mode 100644 index 0000000..6fa0bd9 --- /dev/null +++ b/src/http/EpicsHandler.java @@ -0,0 +1,116 @@ +package http; + +import static http.HttpUtil.isNewId; +import static http.HttpUtil.parseIdOrNull; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import exceptions.NotFoundException; +import java.io.IOException; +import java.net.URI; +import manager.TaskManager; +import model.Epic; + +/** /epics, /epics/{id}, /epics/{id}/subtasks */ +public class EpicsHandler extends BaseHttpHandler implements HttpHandler { + + private final TaskManager manager; + private final Gson gson; + + public EpicsHandler(TaskManager manager, Gson gson) { + this.manager = manager; + this.gson = gson; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + URI uri = exchange.getRequestURI(); + String[] parts = uri.getPath().split("/"); + + switch (method) { + case "GET" -> handleGet(exchange, parts); + case "POST" -> handlePost(exchange); + case "DELETE" -> handleDelete(exchange, parts); + default -> sendServerError(exchange, "Unsupported method"); + } + } catch (Exception e) { + sendServerError(exchange, e.getMessage() == null ? e.toString() : e.getMessage()); + } + } + + private void handleGet(HttpExchange exchange, String[] parts) throws IOException { + if (parts.length == 2) { // /epics + sendOk(exchange, gson.toJson(manager.getEpics())); + return; + } + if (parts.length == 3) { // /epics/{id} + Integer id = parseIdOrNull(parts[2]); + if (id == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + sendOk(exchange, gson.toJson(manager.getEpic(id))); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + return; + } + if (parts.length == 4 && "subtasks".equals(parts[3])) { // /epics/{id}/subtasks + Integer epicId = parseIdOrNull(parts[2]); + if (epicId == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + // вызов getEpic для явного 404, затем выдаём список + manager.getEpic(epicId); + sendOk(exchange, gson.toJson(manager.getEpicSubtasks(epicId))); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + return; + } + sendNotFound(exchange, "incorrect path"); + } + + private void handlePost(HttpExchange exchange) throws IOException { + String body = readBody(exchange); + Epic epic = gson.fromJson(body, Epic.class); + if (epic == null) { + sendServerError(exchange, "empty body"); + return; + } + try { + if (isNewId(epic.getId())) { + manager.addNewEpic(epic); + } else { + manager.updateEpic(epic); // NotFound → 404 + } + sendCreated(exchange); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + } + + private void handleDelete(HttpExchange exchange, String[] parts) throws IOException { + if (parts.length != 3) { + sendNotFound(exchange, "incorrect path"); + return; + } + Integer id = parseIdOrNull(parts[2]); + if (id == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + manager.removeEpic(id); // NotFound → 404 + sendOk(exchange, "\"deleted\""); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + } +} diff --git a/src/http/HistoryHandler.java b/src/http/HistoryHandler.java new file mode 100644 index 0000000..c9659b4 --- /dev/null +++ b/src/http/HistoryHandler.java @@ -0,0 +1,32 @@ +package http; // sprint 9 + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import manager.TaskManager; + +/** GET /history — вернуть историю просмотров. sprint 9 */ +public class HistoryHandler extends BaseHttpHandler implements HttpHandler { + + private final TaskManager manager; + private final Gson gson; + + public HistoryHandler(TaskManager manager, Gson gson) { + this.manager = manager; + this.gson = gson; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendServerError(exchange, "Unsupported method"); + return; + } + try { + sendOk(exchange, gson.toJson(manager.getHistory())); + } catch (Exception e) { + sendServerError(exchange, e.getMessage() == null ? e.toString() : e.getMessage()); + } + } +} diff --git a/src/http/HttpTaskServer.java b/src/http/HttpTaskServer.java new file mode 100644 index 0000000..fe37194 --- /dev/null +++ b/src/http/HttpTaskServer.java @@ -0,0 +1,98 @@ +package http; // sprint 9 + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import manager.Managers; +import manager.TaskManager; + +public class HttpTaskServer { + + public static final int DEFAULT_PORT = 8080; // sprint9 + + private HttpServer server; // sprint9: был final → теперь ленивое создание + private final TaskManager manager; + private final Gson gson; + private final int port; // sprint9: порт хранится в поле + + public HttpTaskServer(TaskManager manager) { // для прод-режима + this(manager, DEFAULT_PORT); // sprint9 + } + + public HttpTaskServer(TaskManager manager, int port) { // sprint9: для тестов/кастомного порта + this.manager = manager; + this.port = port; + this.gson = buildGson(); + } + + public void start() throws IOException { + if (server != null) { + return; // уже запущен + } + server = HttpServer.create(new InetSocketAddress(port), 0); // sprint9 + // регистрируем контексты при запуске (а не в конструкторе) + server.createContext("/tasks", new TasksHandler(this.manager, this.gson)); + server.createContext("/subtasks", new SubtasksHandler(this.manager, this.gson)); + server.createContext("/epics", new EpicsHandler(this.manager, this.gson)); + server.createContext("/history", new HistoryHandler(this.manager, this.gson)); + server.createContext("/prioritized", new PrioritizedHandler(this.manager, this.gson)); + + server.start(); + System.out.println("HTTP Task Server started on port " + getPort()); // sprint9 + } + + public void stop() { + if (server != null) { + server.stop(0); + server = null; // sprint9 + System.out.println("HTTP Task Server stopped"); + } + } + + // sprint9: фактический порт (если был 0, ОС подставила свободный) + public int getPort() { + return server == null ? port : server.getAddress().getPort(); + } + + public static Gson getGson() { + return buildGson(); + } + + private static Gson buildGson() { + return new GsonBuilder() + .registerTypeAdapter( + LocalDateTime.class, + (com.google.gson.JsonSerializer) + (src, t, ctx) -> + new com.google.gson.JsonPrimitive( + src.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))) + .registerTypeAdapter( + LocalDateTime.class, + (com.google.gson.JsonDeserializer) + (json, t, ctx) -> + LocalDateTime.parse(json.getAsString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + .registerTypeAdapter( + Duration.class, + (com.google.gson.JsonSerializer) + (src, t, ctx) -> new com.google.gson.JsonPrimitive(src.toMillis())) + .registerTypeAdapter( + Duration.class, + (com.google.gson.JsonDeserializer) + (json, t, ctx) -> Duration.ofMillis(json.getAsLong())) + .create(); + } + + /** Запуск сервера из консоли. */ + public static void main(String[] args) throws IOException { + TaskManager manager = Managers.getDefault(); + System.out.println("HTTP Task Server started"); + System.out.println("manager = " + manager.getClass().getName()); + System.out.println("working dir = " + new java.io.File(".").getAbsolutePath()); + new HttpTaskServer(manager, DEFAULT_PORT).start(); // sprint9 + } +} diff --git a/src/http/HttpUtil.java b/src/http/HttpUtil.java new file mode 100644 index 0000000..c7449a3 --- /dev/null +++ b/src/http/HttpUtil.java @@ -0,0 +1,20 @@ +package http; + +/** sprint-9: общие мелкие утилиты для HTTP-обработчиков. */ +public final class HttpUtil { + private HttpUtil() {} + + /** Новый объект, если id == null или 0. */ + public static boolean isNewId(Integer id) { + return id == null || id == 0; + } + + /** Парсер id без исключений. Возвращает null при ошибке. */ + public static Integer parseIdOrNull(String s) { + try { + return Integer.valueOf(s); + } catch (Exception ignored) { + return null; + } + } +} diff --git a/src/http/PrioritizedHandler.java b/src/http/PrioritizedHandler.java new file mode 100644 index 0000000..a11c034 --- /dev/null +++ b/src/http/PrioritizedHandler.java @@ -0,0 +1,32 @@ +package http; // sprint 9 + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import manager.TaskManager; + +/** GET /prioritized — задачи по приоритету. sprint 9 */ +public class PrioritizedHandler extends BaseHttpHandler implements HttpHandler { + + private final TaskManager manager; + private final Gson gson; + + public PrioritizedHandler(TaskManager manager, Gson gson) { + this.manager = manager; + this.gson = gson; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendServerError(exchange, "Unsupported method"); + return; + } + try { + sendOk(exchange, gson.toJson(manager.getPrioritizedTasks())); + } catch (Exception e) { + sendServerError(exchange, e.getMessage() == null ? e.toString() : e.getMessage()); + } + } +} diff --git a/src/http/SubtasksHandler.java b/src/http/SubtasksHandler.java new file mode 100644 index 0000000..04d9912 --- /dev/null +++ b/src/http/SubtasksHandler.java @@ -0,0 +1,102 @@ +package http; + +import static http.HttpUtil.isNewId; +import static http.HttpUtil.parseIdOrNull; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import exceptions.NotFoundException; +import exceptions.TaskValidationException; +import java.io.IOException; +import java.net.URI; +import manager.TaskManager; +import model.Subtask; + +public class SubtasksHandler extends BaseHttpHandler implements HttpHandler { + + private final TaskManager manager; + private final Gson gson; + + public SubtasksHandler(TaskManager manager, Gson gson) { + this.manager = manager; + this.gson = gson; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + URI uri = exchange.getRequestURI(); + String[] parts = uri.getPath().split("/"); // ["", "subtasks", ...] + switch (method) { + case "GET" -> handleGet(exchange, parts); + case "POST" -> handlePost(exchange); + case "DELETE" -> handleDelete(exchange, parts); + default -> sendServerError(exchange, "Unsupported method " + method); + } + } catch (Exception e) { + sendServerError(exchange, e.getMessage() == null ? e.toString() : e.getMessage()); + } + } + + private void handleGet(HttpExchange exchange, String[] parts) throws IOException { + if (parts.length == 2) { // /subtasks + sendOk(exchange, gson.toJson(manager.getSubtasks())); + return; + } + if (parts.length == 3) { // /subtasks/{id} + Integer id = parseIdOrNull(parts[2]); + if (id == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + sendOk(exchange, gson.toJson(manager.getSubtask(id))); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + return; + } + sendNotFound(exchange, "incorrect path"); + } + + private void handlePost(HttpExchange exchange) throws IOException { + String body = readBody(exchange); + Subtask incoming = gson.fromJson(body, Subtask.class); + if (incoming == null) { + sendServerError(exchange, "empty body"); + return; + } + try { + if (isNewId(incoming.getId())) { + manager.addNewSubtask(incoming); // бросит NotFound, если epic не найден + } else { + manager.updateSubtask(incoming); // NotFound → 404 + } + sendCreated(exchange); + } catch (TaskValidationException overlap) { + sendHasOverlaps(exchange, overlap.getMessage()); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + } + + private void handleDelete(HttpExchange exchange, String[] parts) throws IOException { + if (parts.length != 3) { + sendNotFound(exchange, "incorrect path"); + return; + } + Integer id = parseIdOrNull(parts[2]); + if (id == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + manager.removeSubtask(id); // NotFound → 404 + sendOk(exchange, "\"deleted\""); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + } +} diff --git a/src/http/TasksHandler.java b/src/http/TasksHandler.java new file mode 100644 index 0000000..3e227f8 --- /dev/null +++ b/src/http/TasksHandler.java @@ -0,0 +1,103 @@ +package http; + +import static http.HttpUtil.isNewId; +import static http.HttpUtil.parseIdOrNull; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import exceptions.NotFoundException; +import exceptions.TaskValidationException; +import java.io.IOException; +import java.net.URI; +import manager.TaskManager; +import model.Task; + +/** /tasks и /tasks/{id} */ +public class TasksHandler extends BaseHttpHandler implements HttpHandler { + + private final TaskManager manager; + private final Gson gson; + + public TasksHandler(TaskManager manager, Gson gson) { + this.manager = manager; + this.gson = gson; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + URI uri = exchange.getRequestURI(); + String[] parts = uri.getPath().split("/"); // ["", "tasks", ...] + switch (method) { + case "GET" -> handleGet(exchange, parts); + case "POST" -> handlePost(exchange); + case "DELETE" -> handleDelete(exchange, parts); + default -> sendServerError(exchange, "Unsupported method " + method); + } + } catch (Exception e) { + sendServerError(exchange, e.getMessage() == null ? e.toString() : e.getMessage()); + } + } + + private void handleGet(HttpExchange exchange, String[] parts) throws IOException { + if (parts.length == 2) { // /tasks + sendOk(exchange, gson.toJson(manager.getTasks())); + return; + } + if (parts.length == 3) { // /tasks/{id} + Integer id = parseIdOrNull(parts[2]); + if (id == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + sendOk(exchange, gson.toJson(manager.getTask(id))); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + return; + } + sendNotFound(exchange, "incorrect path"); + } + + private void handlePost(HttpExchange exchange) throws IOException { + String body = readBody(exchange); + Task incoming = gson.fromJson(body, Task.class); + if (incoming == null) { + sendServerError(exchange, "empty body"); + return; + } + try { + if (isNewId(incoming.getId())) { + manager.addNewTask(incoming); + } else { + manager.updateTask(incoming); // NotFound → 404 + } + sendCreated(exchange); + } catch (TaskValidationException overlap) { + sendHasOverlaps(exchange, overlap.getMessage()); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + } + + private void handleDelete(HttpExchange exchange, String[] parts) throws IOException { + if (parts.length != 3) { + sendNotFound(exchange, "incorrect path"); + return; + } + Integer id = parseIdOrNull(parts[2]); + if (id == null) { + sendNotFound(exchange, "incorrect id"); + return; + } + try { + manager.removeTask(id); // NotFound → 404 + sendOk(exchange, "\"deleted\""); + } catch (NotFoundException nf) { + sendNotFound(exchange, nf.getMessage()); + } + } +} diff --git a/src/manager/FileBackedTaskManager.java b/src/manager/FileBackedTaskManager.java index 0197f69..cca276a 100644 --- a/src/manager/FileBackedTaskManager.java +++ b/src/manager/FileBackedTaskManager.java @@ -1,248 +1,252 @@ package manager; -import model.Epic; -import model.Status; -import model.Subtask; -import model.Task; -import model.TaskType; -import util.CsvUtils; // TODO(review): парсинг времени вынесен в утилиту - 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.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.util.ArrayList; import java.util.List; +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; +import model.TaskType; +import util.CsvUtils; // TODO(review): парсинг времени вынесен в утилиту /** - * Менеджер с сохранением состояния в файл (CSV). - * CHANGED (sprint-8): - * - расширен CSV-формат: добавлены колонки durationMinutes и startTime; - * - добавлена обратная совместимость чтения старого формата; - * - на restore используем put*PreserveId + setNextIdAfterRestore. + * Менеджер с сохранением состояния в файл (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) { - this.file = file; + private final File file; + + // Единый формат для CSV + private static final DateTimeFormatter CSV_TIME_FMT = Task.CSV_TIME_FMT; + + // фабрика + + public FileBackedTaskManager(File file) { + this.file = file; + } + + @SuppressWarnings("unused") + public static FileBackedTaskManager loadFromFile(File file) { + FileBackedTaskManager manager = new FileBackedTaskManager(file); + manager.restore(); + return manager; + } + + // сохранение + + /** Сохраняет все задачи в CSV: id,type,name,status,description,durationMinutes,startTime,epic */ + private void save() { + 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 epic : getEpics()) { + writer.write(epic.toCsvRow()); + writer.newLine(); + } + for (Subtask subtask : getSubtasks()) { + writer.write(subtask.toCsvRow()); + writer.newLine(); + } + + } catch (IOException ex) { + throw new ManagerSaveException("Не удалось сохранить файл", ex); } + } - @SuppressWarnings("unused") - public static FileBackedTaskManager loadFromFile(File file) { - FileBackedTaskManager manager = new FileBackedTaskManager(file); - manager.restore(); - return manager; - } + // восстановление - /* ───────────── сохранение ───────────── */ - - /** Сохраняет все задачи в CSV: id,type,name,status,description,durationMinutes,startTime,epic */ - private void save() { - 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 epic : getEpics()) { - writer.write(epic.toCsvRow()); - writer.newLine(); - } - for (Subtask subtask : getSubtasks()) { - writer.write(subtask.toCsvRow()); - writer.newLine(); - } - - } catch (IOException ex) { - throw new ManagerSaveException("Не удалось сохранить файл", ex); - } + /** Читает CSV и восстанавливает состояние. */ + private void restore() { + if (!file.exists()) { + return; } - /* ───────────── восстановление ───────────── */ + List epics = new ArrayList<>(); + List tasks = new ArrayList<>(); + List subtasks = new ArrayList<>(); - /** Читает CSV и восстанавливает состояние. */ - private void restore() { - if (!file.exists()) { - return; - } + int maxId = 0; // TODO(review): считаем maxId за один проход при чтении файла - List epics = new ArrayList<>(); - 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(); // заголовок - if (header == null) { - return; - } - - String line; - while ((line = reader.readLine()) != null) { - if (line.isBlank()) { - continue; - } - - Task task = fromCsv(line); - maxId = Math.max(maxId, task.getId()); // TODO(review): обновляем maxId на лету - - 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) { - throw new ManagerSaveException("Не удалось прочитать файл", ex); - } + try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + String header = reader.readLine(); // заголовок + if (header == null) { + return; + } - // Важно: сначала эпики, затем задачи, затем подзадачи - for (Epic epic : epics) { - super.putEpicPreserveId(epic); + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) { + continue; } - for (Task task : tasks) { - super.putTaskPreserveId(task); - } - for (Subtask subtask : subtasks) { - super.putSubtaskPreserveId(subtask); - } - - // TODO(review): без дополнительных циклов — используем maxId, посчитанный при чтении - super.setNextIdAfterRestore(maxId + 1); - } - - /* ───────────── CSV утилиты ───────────── */ - private static Task fromCsv(String csv) { - 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 (taskParts.length != 6 && taskParts.length != 8) { - throw new ManagerSaveException("Некорректная строка CSV: " + csv); - } - - 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 = taskParts.length == 8 ? taskParts[5] : ""; - String startStr = taskParts.length == 8 ? taskParts[6] : ""; - // после проверки выше длина может быть только 6 или 8 - String epicStr = taskParts.length == 8 ? taskParts[7] : taskParts[5]; - - Duration duration = durStr.isBlank() ? null : Duration.ofMinutes(Long.parseLong(durStr)); - LocalDateTime startTime = CsvUtils.parseTimeOrNull(startStr, CSV_TIME_FMT); // TODO(review): парсинг времени из утилиты - - switch (type) { - case TASK: { - Task task = new Task(name, description, status); - task.setId(id); - task.setDuration(duration); - task.setStartTime(startTime); - return task; - } - case EPIC: { - Epic epic = new Epic(name, description); - epic.setId(id); - epic.setStatus(status); - // duration/start/end будут пересчитаны после загрузки subtask - return epic; - } - case SUBTASK: { - 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); + Task task = fromCsv(line); + maxId = Math.max(maxId, task.getId()); // TODO(review): обновляем maxId на лету + + 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) { + throw new ManagerSaveException("Не удалось прочитать файл", ex); } - /* ───────────── переопределения с автосохранением ───────────── */ - - @Override - public int addNewTask(Task task) { - int id = super.addNewTask(task); - save(); - return id; + // Важно: сначала эпики, затем задачи, затем подзадачи + for (Epic epic : epics) { + super.putEpicPreserveId(epic); } - - @Override - public int addNewEpic(Epic epic) { - int id = super.addNewEpic(epic); - save(); - return id; + for (Task task : tasks) { + super.putTaskPreserveId(task); } - - @Override - public int addNewSubtask(Subtask subtask) { - int id = super.addNewSubtask(subtask); - save(); - return id; + for (Subtask subtask : subtasks) { + super.putSubtaskPreserveId(subtask); } - @Override - public void updateTask(Task task) { - super.updateTask(task); - save(); - } + // TODO(review): без дополнительных циклов — используем maxId, посчитанный при чтении + super.setNextIdAfterRestore(maxId + 1); + } - @Override - public void updateEpic(Epic epic) { - super.updateEpic(epic); - save(); - } + // CSV утилиты - @Override - public void updateSubtask(Subtask subtask) { - super.updateSubtask(subtask); - save(); + private static Task fromCsv(String csv) { + 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 (taskParts.length != 6 && taskParts.length != 8) { + throw new ManagerSaveException("Некорректная строка CSV: " + csv); } - @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(); + 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 = taskParts.length == 8 ? taskParts[5] : ""; + String startStr = taskParts.length == 8 ? taskParts[6] : ""; + // после проверки выше длина может быть только 6 или 8 + String epicStr = taskParts.length == 8 ? taskParts[7] : taskParts[5]; + + Duration duration = durStr.isBlank() ? null : Duration.ofMinutes(Long.parseLong(durStr)); + LocalDateTime startTime = + CsvUtils.parseTimeOrNull( + startStr, CSV_TIME_FMT); // TODO(review): парсинг времени из утилиты + + switch (type) { + case TASK: + { + Task task = new Task(name, description, status); + task.setId(id); + task.setDuration(duration); + task.setStartTime(startTime); + return task; + } + case EPIC: + { + Epic epic = new Epic(name, description); + epic.setId(id); + epic.setStatus(status); + // duration/start/end будут пересчитаны после загрузки subtask + return epic; + } + case SUBTASK: + { + 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); } + } + + // переопределения с автосохранением + + @Override + public int addNewTask(Task task) { + int id = super.addNewTask(task); + save(); + return id; + } + + @Override + public int addNewEpic(Epic epic) { + int id = super.addNewEpic(epic); + save(); + return id; + } + + @Override + public int addNewSubtask(Subtask subtask) { + int id = super.addNewSubtask(subtask); + save(); + return id; + } + + @Override + public void updateTask(Task task) { + super.updateTask(task); + save(); + } + + @Override + public void updateEpic(Epic epic) { + super.updateEpic(epic); + save(); + } + + @Override + public void updateSubtask(Subtask subtask) { + super.updateSubtask(subtask); + save(); + } + + @Override + public void 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(); + } } diff --git a/src/manager/HistoryManager.java b/src/manager/HistoryManager.java index 9b2421b..6c5cecf 100644 --- a/src/manager/HistoryManager.java +++ b/src/manager/HistoryManager.java @@ -6,9 +6,9 @@ /** Интерфейс менеджера истории просмотров задач. */ public interface HistoryManager { - void add(Task task); + void add(Task task); - void remove(int id); + void remove(int id); - List getHistory(); + List getHistory(); } diff --git a/src/manager/InMemoryHistoryManager.java b/src/manager/InMemoryHistoryManager.java index 9593b3f..58d18d4 100644 --- a/src/manager/InMemoryHistoryManager.java +++ b/src/manager/InMemoryHistoryManager.java @@ -1,39 +1,38 @@ package manager; -import model.Task; - import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import model.Task; public class InMemoryHistoryManager implements HistoryManager { - private static final int MAX = 10; - private final LinkedHashMap order = new LinkedHashMap<>(); + private static final int MAX = 10; + private final LinkedHashMap order = new LinkedHashMap<>(); - @Override - public void add(Task task) { - if (task == null) { - 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); - } + @Override + public void add(Task task) { + if (task == null) { + return; } - - @Override - public void remove(int id) { - order.remove(id); + int id = task.getId(); + // Дедупликация. + order.remove(id); + order.put(id, task); + // Ограничиваем размер. + while (order.size() > MAX) { + Integer firstKey = order.keySet().iterator().next(); + order.remove(firstKey); } + } - @Override - public List getHistory() { - return new ArrayList<>(order.values()); - } + @Override + public void remove(int id) { + order.remove(id); + } + + @Override + public List getHistory() { + return new ArrayList<>(order.values()); + } } diff --git a/src/manager/InMemoryTaskManager.java b/src/manager/InMemoryTaskManager.java index 8afccf2..c926ec4 100644 --- a/src/manager/InMemoryTaskManager.java +++ b/src/manager/InMemoryTaskManager.java @@ -1,319 +1,312 @@ package manager; +import exceptions.NotFoundException; import exceptions.TaskValidationException; // NEW (sprint-8) -import model.*; - import java.time.LocalDateTime; // NEW (sprint-8) import java.util.*; import java.util.stream.Collectors; +import model.*; /** * InMemoryTaskManager хранит задачи в памяти и ведет историю. * - *

CHANGED (sprint-8): - * - приоритизация через TreeSet (startTime); - * - проверка пересечений при add/update Task/Subtask; - * - эпики получают расчётные duration/start/end от подзадач; - * - добавлены protected put*-методы и setNextIdAfterRestore для FileBacked; - * - часть циклов переписана на stream API. + *

CHANGED (sprint-8): - приоритизация через TreeSet (startTime); - проверка пересечений при + * add/update Task/Subtask; - эпики получают расчётные duration/start/end от подзадач; - добавлены + * protected put*-методы и setNextIdAfterRestore для FileBacked; - часть циклов переписана на stream + * API. + * + *

CHANGED (sprint-9): - устранены дубли в prioritized: удаление по id перед переиндексацией. */ public class InMemoryTaskManager implements TaskManager { - /* ---------- хранилища ---------- */ - protected final Map tasks = new HashMap<>(); - protected final Map epics = new HashMap<>(); - protected final Map subtasks = new HashMap<>(); - - /* ---------- история ---------- */ - private final HistoryManager historyManager = new InMemoryHistoryManager(); + /* хранилища */ + protected final Map tasks = new HashMap<>(); + protected final Map epics = new HashMap<>(); + protected final Map subtasks = new HashMap<>(); - /* ---------- ID ---------- */ - protected int nextId = 1; + /* история */ + private final HistoryManager historyManager = new InMemoryHistoryManager(); - private int generateId() { - return nextId++; - } + /* ID */ + protected int nextId = 1; - /* ---------- приоритизация (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); - } - } + private int generateId() { + return nextId++; + } - /* ---------- создание ---------- */ - - @Override - public int addNewTask(Task task) { - // 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) { - int id = generateId(); - epic.setId(id); - epics.put(id, epic); - // у эпика вычисляемые поля — посчитаются, когда появятся subtask - return id; - } + /* приоритизация (sprint-8) */ + // Задачи без startTime сюда не добавляем (по ТЗ). + private final Comparator PRIORITY_CMP = + Comparator.comparing(Task::getStartTime, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparingInt(Task::getId); - @Override - public int addNewSubtask(Subtask subtask) { - Epic epic = epics.get(subtask.getEpicId()); - if (epic == null) { - throw new IllegalArgumentException("Эпик не найден"); - } - validateNoOverlaps(subtask, null); - int id = generateId(); - subtask.setId(id); - subtasks.put(id, subtask); - epic.addSubtaskId(id); - trackPrioritized(subtask); - recalcEpic(epic.getId()); - return id; - } + private final NavigableSet prioritized = new TreeSet<>(PRIORITY_CMP); - /* ---------- обновление ---------- */ + // sprint-9: удаляем из приоритета все версии по id (независимо от старого startTime) + private void prioritizedRemoveById(int id) { // sprint-9 + prioritized.removeIf(t -> t.getId() == id); + } - @Override - public void updateTask(Task task) { - if (!tasks.containsKey(task.getId())) { - return; - } - validateNoOverlaps(task, task.getId()); - tasks.put(task.getId(), task); - trackPrioritized(task); + // sprint-9: переиндексация элемента в приоритете + private void prioritizedReindex(Task task) { // sprint-9 + if (task == null) { + return; } - - @Override - public void updateEpic(Epic 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()); + prioritizedRemoveById(task.getId()); + if (task.getStartTime() != null) { + prioritized.add(task); } - - @Override - 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 int addNewTask(Task task) { + validateNoOverlaps(task, null); + int id = generateId(); + task.setId(id); + tasks.put(id, task); + prioritizedReindex(task); // sprint-9 + return id; + } + + @Override + public int addNewEpic(Epic epic) { + int id = generateId(); + epic.setId(id); + epics.put(id, epic); + return id; + } + + @Override + public int addNewSubtask(Subtask subtask) { + Epic epic = epics.get(subtask.getEpicId()); + if (epic == null) { + throw new NotFoundException("epic " + subtask.getEpicId() + " not found"); } - - /* ---------- удаление ---------- */ - - @Override - public void removeTask(int id) { - Task removed = tasks.remove(id); - if (removed != null) { - prioritized.remove(removed); - historyManager.remove(id); - } + validateNoOverlaps(subtask, null); + int id = generateId(); + subtask.setId(id); + subtasks.put(id, subtask); + epic.addSubtaskId(id); + prioritizedReindex(subtask); // sprint-9 + recalcEpic(epic.getId()); + return id; + } + + /* обновление */ + + @Override + public void updateTask(Task task) { + if (!tasks.containsKey(task.getId())) { + throw new NotFoundException("task " + task.getId() + " not found"); } - - @Override - public void removeEpic(int id) { - Epic epic = epics.remove(id); - if (epic != null) { - // удаляем все подзадачи эпика - for (int sid : epic.getSubtaskIds()) { - Subtask s = subtasks.remove(sid); - if (s != null) { - prioritized.remove(s); - historyManager.remove(sid); - } - } - historyManager.remove(id); - } + validateNoOverlaps(task, task.getId()); + tasks.put(task.getId(), task); + prioritizedReindex(task); // sprint-9 + } + + @Override + public void updateEpic(Epic epic) { + Epic exist = epics.get(epic.getId()); + if (exist == null) { + throw new NotFoundException("epic " + epic.getId() + " not found"); } - - @Override - public void removeSubtask(int id) { - Subtask s = subtasks.remove(id); - if (s != null) { - Epic epic = epics.get(s.getEpicId()); - if (epic != null) { - epic.getSubtaskIds().remove((Integer) id); - } - prioritized.remove(s); - historyManager.remove(id); - if (epic != null) { - recalcEpic(epic.getId()); - } - } + // статус/время эпика пересчитывается от subtask — но позволим обновить title/description + exist.setTitle(epic.getTitle()); + exist.setDescription(epic.getDescription()); + recalcEpic(exist.getId()); + } + + @Override + public void updateSubtask(Subtask subtask) { + if (!subtasks.containsKey(subtask.getId())) { + throw new NotFoundException("subtask " + subtask.getId() + " not found"); } - - /* ---------- получение + история ---------- */ - - @Override - public Task getTask(int id) { - Task t = tasks.get(id); - if (t != null) { - historyManager.add(t); - } - return t; + validateNoOverlaps(subtask, subtask.getId()); + subtasks.put(subtask.getId(), subtask); + prioritizedReindex(subtask); // sprint-9 + recalcEpic(subtask.getEpicId()); + } + + /* удаление */ + + @Override + public void removeTask(int id) { + if (!tasks.containsKey(id)) { + throw new NotFoundException("task " + id + " not found"); } - - @Override - public Epic getEpic(int id) { - Epic e = epics.get(id); - if (e != null) { - historyManager.add(e); - } - return e; + tasks.remove(id); + prioritizedRemoveById(id); // sprint-9 + historyManager.remove(id); + } + + @Override + public void removeEpic(int id) { + Epic epic = epics.remove(id); + if (epic == null) { + throw new NotFoundException("epic " + id + " not found"); } - - @Override - public Subtask getSubtask(int id) { - Subtask s = subtasks.get(id); - if (s != null) { - historyManager.add(s); - } - return s; + for (int sid : epic.getSubtaskIds()) { + subtasks.remove(sid); + prioritizedRemoveById(sid); // sprint-9 + historyManager.remove(sid); } - - /* ---------- списки ---------- */ - - @Override - public List getTasks() { - return new ArrayList<>(tasks.values()); + historyManager.remove(id); + } + + @Override + public void removeSubtask(int id) { + Subtask s = subtasks.remove(id); + if (s == null) { + throw new NotFoundException("subtask " + id + " not found"); } - - @Override - public List getEpics() { - return new ArrayList<>(epics.values()); + Epic epic = epics.get(s.getEpicId()); + if (epic != null) { + epic.removeSubtaskId(id); + recalcEpic(epic.getId()); } + prioritizedRemoveById(id); // sprint-9 + historyManager.remove(id); + } - @Override - public List getSubtasks() { - return new ArrayList<>(subtasks.values()); - } + /* получение + история */ - @Override - public List getEpicSubtasks(int epicId) { - // 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 + public Task getTask(int id) { + Task t = tasks.get(id); + if (t == null) { + throw new NotFoundException("task " + id + " not found"); } - - @Override - public List getHistory() { - return historyManager.getHistory(); + historyManager.add(t); + return t; + } + + @Override + public Epic getEpic(int id) { + Epic e = epics.get(id); + if (e == null) { + throw new NotFoundException("epic " + id + " not found"); } - - /* ---------- prioritized (sprint-8) ---------- */ - - @Override - public List getPrioritizedTasks() { - // ожидается частый вызов → O(n) - return new ArrayList<>(prioritized); + historyManager.add(e); + return e; + } + + @Override + public Subtask getSubtask(int id) { + Subtask s = subtasks.get(id); + if (s == null) { + throw new NotFoundException("subtask " + id + " not found"); } - - /* ---------- расчёт эпика (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(); - - // TODO(review sprint-8): пересчёт эпика вынесен в Epic.recalcFromSubtasks — один проход. - epic.recalcFromSubtasks(subs); + 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 getEpicSubtasks(int epicId) { + return epics.containsKey(epicId) + ? epics.get(epicId).getSubtaskIds().stream() + .map(subtasks::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + : List.of(); + } + + @Override + public List getHistory() { + return historyManager.getHistory(); + } + + /* prioritized */ + + @Override + public List getPrioritizedTasks() { + return new ArrayList<>(prioritized); + } + + /* расчёт эпика (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(); + epic.recalcFromSubtasks(subs); + } - /* ---------- пересечения (sprint-8) ---------- */ - - private void validateNoOverlaps(Task candidate, Integer selfId) { - // Не проверяем, если нет времени или длительности. - if (candidate.getStartTime() == null || candidate.getDuration() == null) { - return; - } + /* пересечения (sprint-8) */ - boolean intersect = - prioritized.stream() - .filter(t -> selfId == null || t.getId() != selfId) - .anyMatch(t -> isOverlap(candidate, t)); - - if (intersect) { - throw new TaskValidationException("Задача пересекается по времени с другой"); - } + private void validateNoOverlaps(Task candidate, Integer selfId) { + if (candidate.getStartTime() == null || candidate.getDuration() == null) { + return; } - - // Пересечение отрезков: [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); + boolean intersect = + prioritized.stream() + .filter(t -> selfId == null || t.getId() != selfId) + .anyMatch(t -> isOverlap(candidate, t)); + if (intersect) { + throw new TaskValidationException("Задача пересекается по времени с другой"); } - - /* ---------- поддержка FileBacked (preserve id / nextId) ---------- */ - - // Восстановление с сохранением id (используется FileBackedTaskManager.restore()) - protected void putTaskPreserveId(Task t) { - tasks.put(t.getId(), t); - trackPrioritized(t); + } + + // Пересечение отрезков: [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; } - - protected void putEpicPreserveId(Epic e) { - epics.put(e.getId(), e); - // пересчёт сделаем после загрузки всех subtask + var ad = a.getDuration(); + var bd = b.getDuration(); + if (ad == null || bd == null) { + return false; } - - 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); + LocalDateTime ae = a.getEndTime(); + LocalDateTime be = b.getEndTime(); + return as.isBefore(be) && bs.isBefore(ae); + } + + /* поддержка FileBacked (preserve id / nextId) */ + + protected void putTaskPreserveId(Task t) { + tasks.put(t.getId(), t); + prioritizedReindex(t); // sprint-9 + } + + protected void putEpicPreserveId(Epic e) { + epics.put(e.getId(), e); + } + + protected void putSubtaskPreserveId(Subtask s) { + subtasks.put(s.getId(), s); + Epic epic = epics.get(s.getEpicId()); + if (epic != null) { + epic.addSubtaskId(s.getId()); } + prioritizedReindex(s); // sprint-9 + } - protected void setNextIdAfterRestore(int next) { - this.nextId = Math.max(this.nextId, next); - // После полного restore пересчитаем эпики: - epics.keySet().forEach(this::recalcEpic); - } + protected void setNextIdAfterRestore(int next) { + this.nextId = Math.max(this.nextId, next); + epics.keySet().forEach(this::recalcEpic); + } } diff --git a/src/manager/ManagerSaveException.java b/src/manager/ManagerSaveException.java index 68d0e6e..704ea7c 100644 --- a/src/manager/ManagerSaveException.java +++ b/src/manager/ManagerSaveException.java @@ -2,11 +2,11 @@ public class ManagerSaveException extends RuntimeException { - public ManagerSaveException(String message) { - super(message); - } + public ManagerSaveException(String message) { + super(message); + } - public ManagerSaveException(String message, Throwable cause) { - super(message, cause); - } + public ManagerSaveException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/manager/Managers.java b/src/manager/Managers.java index d08561c..1033f00 100644 --- a/src/manager/Managers.java +++ b/src/manager/Managers.java @@ -1,16 +1,33 @@ package manager; +import java.io.File; + +/** Фабрики менеджеров. */ public final class Managers { - private Managers() {} + private Managers() {} + + /** + * sprint9: по умолчанию используем файловый менеджер, чтобы данные сохранялись в tasks.csv в + * рабочей директории запуска. + */ + public static TaskManager getDefault() { // sprint9 + return new FileBackedTaskManager(new File("tasks.csv")); // sprint9 + } + + /** sprint9: явная фабрика для InMemory — удобно для тестов. */ + public static TaskManager getInMemoryTaskManager() { // sprint9 + return new InMemoryTaskManager(); // sprint9 + } - public static TaskManager getDefault() { - return new InMemoryTaskManager(); - } + /** sprint9: файловый менеджер с настраиваемым путём (если хранить в data/tasks.csv и т.п.). */ + public static TaskManager getFileBackedTaskManager(File file) { // sprint9 + return new FileBackedTaskManager(file); // sprint9 + } - // NEW для истории - @SuppressWarnings("unused") - public static HistoryManager getDefaultHistory() { - return new InMemoryHistoryManager(); - } + // Оставляем как было: менеджер истории по умолчанию — in-memory. + @SuppressWarnings("unused") + public static HistoryManager getDefaultHistory() { + return new InMemoryHistoryManager(); + } } diff --git a/src/manager/TaskManager.java b/src/manager/TaskManager.java index 94bb687..e8965d0 100644 --- a/src/manager/TaskManager.java +++ b/src/manager/TaskManager.java @@ -1,63 +1,54 @@ package manager; +import java.util.List; import model.Epic; import model.Subtask; import model.Task; -import java.util.List; - -/** - * Интерфейс менеджера задач. - * (из Sprint 7 + NEW методы Sprint 8) - */ +/** Интерфейс менеджера задач (Sprint 7 + новые методы Sprint 8). */ public interface TaskManager { - /* ===================== Создание ===================== */ - int addNewTask(Task task); - - int addNewEpic(Epic epic); + // Создание + int addNewTask(Task task); - int addNewSubtask(Subtask subtask); + int addNewEpic(Epic epic); - /* ===================== Обновление ===================== */ - void updateTask(Task task); + int addNewSubtask(Subtask subtask); - void updateEpic(Epic epic); + // Обновление + void updateTask(Task task); - void updateSubtask(Subtask subtask); + void updateEpic(Epic epic); - /* ===================== Удаление ===================== */ - void removeTask(int id); + void updateSubtask(Subtask subtask); - void removeEpic(int id); + // Удаление + void removeTask(int id); - void removeSubtask(int id); + void removeEpic(int id); - /* ===================== Получение (одна) ===================== */ - Task getTask(int id); + void removeSubtask(int id); - Epic getEpic(int id); + // Получение (одна) + Task getTask(int id); - Subtask getSubtask(int id); + Epic getEpic(int id); - /* ===================== Получение (списки) ===================== */ - List getTasks(); + Subtask getSubtask(int id); - List getEpics(); + // Получение (списки) + List getTasks(); - List getSubtasks(); + List getEpics(); - List getEpicSubtasks(int epicId); + List getSubtasks(); - /* ===================== История ===================== */ - List getHistory(); + List getEpicSubtasks(int epicId); - /* ===================== Prioritized (sprint-8) ===================== */ + // История + List getHistory(); - /** - * NEW (sprint-8): задачи и подзадачи в порядке приоритета по startTime. - * Эпики не включаем (их время расчётное). - * Задачи без startTime не учитываются. - */ - List getPrioritizedTasks(); + // Prioritized (sprint-8) + /** Задачи и подзадачи по приоритету startTime; эпики не включаются. */ + List getPrioritizedTasks(); } diff --git a/src/model/Epic.java b/src/model/Epic.java index 114e56d..d262156 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -6,105 +6,126 @@ import java.util.List; import util.CsvUtils; -/** - * Эпик объединяет подзадачи. - */ +/** Эпик объединяет подзадачи. */ public class Epic extends Task { - private final List subtaskIds = new ArrayList<>(); - private LocalDateTime endTime; + // sprint9: БЫЛО: `private final List subtaskIds = new ArrayList<>();` + // Gson создаёт объект, обходя конструктор/инициализацию полей → в рантайме это поле могло быть + // null. + // Делаю ленивую инициализацию через геттер/хелперы. + private List subtaskIds; // TODO:sprint9: убрал final и инициализацию здесь - public Epic(String title, String description) { - super(title, description); - } + private LocalDateTime endTime; - @Override - public TaskType getType() { - return TaskType.EPIC; - } + public Epic(String title, String description) { + super(title, description); + } - public List getSubtaskIds() { - return subtaskIds; - } + @Override + public TaskType getType() { + return TaskType.EPIC; + } - public void addSubtaskId(int id) { - subtaskIds.add(id); + // sprint9: гарантируем НЕ-null. Возвращаем МУТАБЕЛЬНЫЙ список, чтобы не ломать существующий код + // менеджера. + public List getSubtaskIds() { + if (subtaskIds == null) { // sprint9 + subtaskIds = new ArrayList<>(); // sprint9 + } + return subtaskIds; // sprint9 + } + + public void addSubtaskId(int id) { + // sprint9: защищаемся от null и дублей + List ids = getSubtaskIds(); // sprint9 + if (!ids.contains(id)) { // sprint9 + ids.add(id); // sprint9 + } + } + + public void removeSubtaskId(int id) { // sprint9: безопасное удаление + List ids = getSubtaskIds(); // sprint9 + ids.remove((Integer) id); // sprint9 + } + + public void clearSubtaskIds() { // sprint9 + getSubtaskIds().clear(); // sprint9 + } + + // 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; } - // 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; + 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; + } } - @Override - public LocalDateTime getEndTime() { - return endTime; + if (allNew) { + this.status = Status.NEW; + } else if (allDone) { + this.status = Status.DONE; + } else { + this.status = Status.IN_PROGRESS; } - // TODO(review sprint-8): CSV-escape вынесен в util.CsvUtils (убрано дублирование). - @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(), - CsvUtils.escape(title), - status.name(), - CsvUtils.escape(description), - dur, - st, - "" // epic + this.duration = (totalMinutes == 0) ? null : Duration.ofMinutes(totalMinutes); + this.startTime = minStart; + this.endTime = maxEnd; + } + + @Override + 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()); + String st = startTime == null ? "" : startTime.format(CSV_TIME_FMT); + return String.join( + ",", + String.valueOf(id), + getType().name(), + CsvUtils.escape(title), + status.name(), + CsvUtils.escape(description), + dur, + st, + "" // epic ); - } + } } diff --git a/src/model/Status.java b/src/model/Status.java index cbae870..6aeedf6 100644 --- a/src/model/Status.java +++ b/src/model/Status.java @@ -1,7 +1,7 @@ package model; public enum Status { - NEW, - IN_PROGRESS, - DONE -} \ No newline at end of file + NEW, + IN_PROGRESS, + DONE +} diff --git a/src/model/Subtask.java b/src/model/Subtask.java index 9a7070a..da9269a 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -11,60 +11,83 @@ */ public class Subtask extends Task { - private int epicId; + private int epicId; - public Subtask(String title, String description, int epicId) { - super(title, description); - this.epicId = epicId; - } + public Subtask(String title, String description, int epicId) { + super(title, description); + this.epicId = epicId; + } - public Subtask(String title, String description, Status status, int epicId) { - super(title, description, status); - this.epicId = epicId; - } + public Subtask(String title, String description, Status status, int epicId) { + super(title, description, status); + this.epicId = epicId; + } - public int getEpicId() { - return epicId; - } + // sprint-9: удобный конструктор для тестов/инициализации c временем и длительностью + public Subtask( + String title, + String description, + Status status, + Duration duration, + LocalDateTime startTime, + int epicId) { // sprint-9 + super(title, description, status); // sprint-9 + this.duration = duration; // sprint-9 + this.startTime = startTime; // sprint-9 + this.epicId = epicId; // sprint-9 + } - @SuppressWarnings("unused") - public void setEpicId(int epicId) { - this.epicId = epicId; - } + // sprint-9: сокращённый вариант — статус по умолчанию NEW + public Subtask( + String title, + String description, + Duration duration, + LocalDateTime startTime, + int epicId) { // sprint-9 + this(title, description, Status.NEW, duration, startTime, epicId); // sprint-9 + } - @Override - public TaskType getType() { - return TaskType.SUBTASK; - } + public int getEpicId() { + return epicId; + } - // TODO(review sprint-8): экранирование перенесено в CsvUtils, чтобы не дублировать метод. - @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(), - CsvUtils.escape(title), - status.name(), - CsvUtils.escape(description), - dur, - st, - String.valueOf(epicId) - ); - } + @SuppressWarnings("unused") + public void setEpicId(int epicId) { + this.epicId = epicId; + } - // Удобные fluent-сеттеры - @SuppressWarnings("unused") - public Subtask withStart(LocalDateTime start) { - this.startTime = start; - return this; - } + @Override + public TaskType getType() { + return TaskType.SUBTASK; + } - @SuppressWarnings("unused") - public Subtask withDuration(Duration d) { - this.duration = d; - return this; - } + // TODO(review sprint-8): экранирование перенесено в CsvUtils, чтобы не дублировать метод. + @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(), + CsvUtils.escape(title), + status.name(), + CsvUtils.escape(description), + dur, + st, + String.valueOf(epicId)); + } + + // Удобные fluent-сеттеры + @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 642e3f8..5a0b7db 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,179 +1,179 @@ package model; -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; import java.util.Objects; +import util.CsvUtils; // sprint-9: используем общую утилиту для CSV-escape /** * Базовая задача. - *

CHANGED (sprint-8): - * - добавлены поля duration и startTime; - * - добавлен getEndTime(); - * - расширена CSV-строка (toCsvRow) на durationMinutes и startTime. + * + *

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 Status status = Status.NEW; - - // 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; - } - - public Task(String title, String description, Status status) { - this.title = title; - this.description = description; - this.status = status; - } - - /* ---------- геттеры/сеттеры ---------- */ - - public int getId() { - return id; - } - - 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; - } - - public void setStatus(Status status) { - this.status = status; - } - - // 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() { - 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 + protected int id; + protected String title; + protected String description; + protected Status status = Status.NEW; + + // 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; + } + + public Task(String title, String description, Status status) { + this.title = title; + this.description = description; + this.status = status; + } + + /* ---------- геттеры/сеттеры ---------- */ + + public int getId() { + return id; + } + + 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; + } + + public void setStatus(Status status) { + this.status = status; + } + + // 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() { + 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(), + CsvUtils.escape(title), // sprint-9: общий util вместо локального escape + status.name(), + CsvUtils.escape(description), // sprint-9: общий util вместо локального escape + 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; - } - if (!(o instanceof Task task)) { - return false; - } - 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, title, description, status, duration, startTime, getType()); - } - - @Override - public String toString() { - return getType() - + "{" - + "id=" + id - + ", title='" + title + '\'' - + ", status=" + status - + ", duration=" + (duration == null ? "null" : duration.toMinutes() + "m") - + ", startTime=" + startTime - + '}'; - } + } + + /* ---------- equals/hashCode ---------- */ + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Task task)) { + return false; + } + 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, title, description, status, duration, startTime, getType()); + } + + @Override + public String toString() { + return getType() + + "{" + + "id=" + + id + + ", title='" + + title + + '\'' + + ", status=" + + status + + ", duration=" + + (duration == null ? "null" : duration.toMinutes() + "m") + + ", startTime=" + + startTime + + '}'; + } } diff --git a/src/model/TaskType.java b/src/model/TaskType.java index fbdbeb1..1b5db4a 100644 --- a/src/model/TaskType.java +++ b/src/model/TaskType.java @@ -2,7 +2,7 @@ /** Тип задачи: нужен для CSV сериализация. */ public enum TaskType { - TASK, - EPIC, - SUBTASK + TASK, + EPIC, + SUBTASK } diff --git a/src/test/java/InMemoryTaskManagerTest.java b/src/test/java/InMemoryTaskManagerTest.java new file mode 100644 index 0000000..fafb9ab --- /dev/null +++ b/src/test/java/InMemoryTaskManagerTest.java @@ -0,0 +1,10 @@ +import manager.InMemoryTaskManager; +import manager.TaskManagerTest; + +class InMemoryTaskManagerTest extends TaskManagerTest { + + @Override + protected InMemoryTaskManager createManager() { + return new InMemoryTaskManager(); + } +} diff --git a/src/test/java/http/BaseHttpTest.java b/src/test/java/http/BaseHttpTest.java new file mode 100644 index 0000000..d58397e --- /dev/null +++ b/src/test/java/http/BaseHttpTest.java @@ -0,0 +1,60 @@ +package http; + +import com.google.gson.Gson; +import java.io.IOException; +import java.net.URI; +import java.net.http.*; +import java.time.Duration; +import manager.InMemoryTaskManager; +import manager.TaskManager; + +public abstract class BaseHttpTest { + protected TaskManager manager; // null в «внешнем» режиме + protected HttpTaskServer server; // не создаём, если работаем с внешним + protected Gson gson; + protected String baseUrl; + protected boolean external; + + protected void setUpBase() throws IOException { + gson = HttpTaskServer.getGson(); + String prop = System.getProperty("BASE_URL"); // -DBASE_URL=http://localhost:8080 + external = prop != null && !prop.isBlank(); + + if (external && isReachable(prop)) { + baseUrl = prop; + System.out.println("[tests] Using EXTERNAL server: " + baseUrl); + return; + } + + manager = new InMemoryTaskManager(); + server = new HttpTaskServer(manager, 0); // порт 0 → свободный + server.start(); + baseUrl = "http://localhost:" + server.getPort(); + external = false; + System.out.println("[tests] Using LOCAL server: " + baseUrl); + } + + protected void tearDownBase() { + if (!external && server != null) { + server.stop(); + server = null; + System.out.println("[tests] Local server stopped"); + } + } + + private static boolean isReachable(String baseUrl) { + try { + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofMillis(800)).build(); + HttpRequest req = + HttpRequest.newBuilder(URI.create(baseUrl + "/tasks")) + .timeout(Duration.ofMillis(1200)) + .GET() + .build(); + HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.discarding()); + return resp.statusCode() > 0; + } catch (Exception e) { + System.out.println("[tests] BASE_URL not reachable, fallback to local: " + e); + return false; + } + } +} diff --git a/src/test/java/http/HttpTaskServerEpicsTest.java b/src/test/java/http/HttpTaskServerEpicsTest.java new file mode 100644 index 0000000..b34a0ae --- /dev/null +++ b/src/test/java/http/HttpTaskServerEpicsTest.java @@ -0,0 +1,52 @@ +package http; + +import static http.TestHttpUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import java.io.IOException; +import model.Epic; +import model.Status; +import org.junit.jupiter.api.*; + +class HttpTaskServerEpicsTest extends BaseHttpTest { + + private Gson gson; + + @BeforeEach + void setUp() throws IOException { + setUpBase(); + gson = HttpTaskServer.getGson(); + } + + @AfterEach + void tearDown() { + tearDownBase(); + } + + @Test + void epic_crud_and_subtasks_emptyList() throws Exception { + Epic e = new Epic("Epic A", "big"); + e.setStatus(Status.NEW); + assertEquals(201, post(baseUrl, "/epics", gson.toJson(e)).statusCode()); + + int id = firstIdFromArray(get(baseUrl, "/epics").body()); + assertEquals(200, get(baseUrl, "/epics/" + id).statusCode()); + + // update title/description + Epic upd = new Epic("Epic A*", "bigger"); + upd.setId(id); + upd.setStatus(Status.NEW); + assertEquals(201, post(baseUrl, "/epics", gson.toJson(upd)).statusCode()); + + // subtasks of epic -> [] + String subs = get(baseUrl, "/epics/" + id + "/subtasks").body(); + JsonArray arr = JsonParser.parseString(subs).getAsJsonArray(); + assertEquals(0, arr.size()); + + assertEquals(200, delete(baseUrl, "/epics/" + id).statusCode()); + assertEquals(404, get(baseUrl, "/epics/" + id).statusCode()); + } +} diff --git a/src/test/java/http/HttpTaskServerHistoryPrioritizedTest.java b/src/test/java/http/HttpTaskServerHistoryPrioritizedTest.java new file mode 100644 index 0000000..afbbe0f --- /dev/null +++ b/src/test/java/http/HttpTaskServerHistoryPrioritizedTest.java @@ -0,0 +1,95 @@ +package http; + +import static http.TestHttpUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.*; + +class HttpTaskServerHistoryPrioritizedTest extends BaseHttpTest { + + private Gson gson; + + private static final LocalDateTime BASE = LocalDateTime.of(2035, 1, 1, 0, 0); + + @BeforeEach + void setUp() throws IOException { + setUpBase(); + gson = HttpTaskServer.getGson(); + } + + @AfterEach + void tearDown() { + tearDownBase(); + } + + @Test + void history_and_prioritized_ok() throws Exception { + // task t1 10:00 + Task t1 = new Task("T1", "D", Status.NEW); + t1.setDuration(Duration.ofMinutes(10)); + t1.setStartTime(BASE.withHour(10)); + assertEquals(201, post(baseUrl, "/tasks", gson.toJson(t1)).statusCode()); + + // task t2 11:00 + Task t2 = new Task("T2", "D", Status.NEW); + t2.setDuration(Duration.ofMinutes(10)); + t2.setStartTime(BASE.withHour(11)); + assertEquals(201, post(baseUrl, "/tasks", gson.toJson(t2)).statusCode()); + + // epic + sub 12:00 + Epic e = new Epic("E", ""); + e.setStatus(Status.NEW); + assertEquals(201, post(baseUrl, "/epics", gson.toJson(e)).statusCode()); + int epicId = firstIdFromArray(get(baseUrl, "/epics").body()); + + Subtask s = new Subtask("S", "", Status.NEW, Duration.ofMinutes(20), BASE.withHour(12), epicId); + assertEquals(201, post(baseUrl, "/subtasks", gson.toJson(s)).statusCode()); + + // дергаем /tasks/{id}, /epics/{id}, /subtasks/{id} -> попадут в историю + int t1Id = idByIndex(get(baseUrl, "/tasks").body(), 0); + int t2Id = idByIndex(get(baseUrl, "/tasks").body(), 1); + int sId = firstIdFromArray(get(baseUrl, "/subtasks").body()); + + assertEquals(200, get(baseUrl, "/tasks/" + t1Id).statusCode()); + assertEquals(200, get(baseUrl, "/epics/" + epicId).statusCode()); + assertEquals(200, get(baseUrl, "/subtasks/" + sId).statusCode()); + + // history should be non-empty (contains 3 entries) + JsonArray hist = JsonParser.parseString(get(baseUrl, "/history").body()).getAsJsonArray(); + assertTrue(hist.size() >= 3); + + // prioritized — строго по возрастанию startTime: 10:00, 11:00, 12:00 + JsonArray pr = JsonParser.parseString(get(baseUrl, "/prioritized").body()).getAsJsonArray(); + assertTrue(pr.size() >= 3); + long first = startMillis(pr.get(0)); + long second = startMillis(pr.get(1)); + long third = startMillis(pr.get(2)); + assertTrue(first <= second && second <= third, "prioritized must be sorted by startTime"); + } + + /* helpers (не дублируют TestHttpUtils) */ + + private static long startMillis(JsonElement el) { + String s = el.getAsJsonObject().get("startTime").getAsString(); + return java.time.LocalDateTime.parse(s) + .atZone(java.time.ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + } + + private static int idByIndex(String json, int idx) { + JsonArray arr = JsonParser.parseString(json).getAsJsonArray(); + return arr.get(idx).getAsJsonObject().get("id").getAsInt(); + } +} diff --git a/src/test/java/http/HttpTaskServerSubtasksTest.java b/src/test/java/http/HttpTaskServerSubtasksTest.java new file mode 100644 index 0000000..4f3580c --- /dev/null +++ b/src/test/java/http/HttpTaskServerSubtasksTest.java @@ -0,0 +1,79 @@ +package http; + +import static http.TestHttpUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import model.Epic; +import model.Status; +import model.Subtask; +import org.junit.jupiter.api.*; + +class HttpTaskServerSubtasksTest extends BaseHttpTest { + + @BeforeEach + void setUp() throws IOException { + setUpBase(); // gson уже инициализирован в BaseHttpTest + } + + @AfterEach + void tearDown() { + tearDownBase(); + } + + @Test + void subtask_crud_and_overlap406() throws Exception { + // epic + Epic e = new Epic("E", ""); + e.setStatus(Status.NEW); + assertEquals(201, post(baseUrl, "/epics", gson.toJson(e)).statusCode()); + int epicId = firstIdFromArray(get(baseUrl, "/epics").body()); + + // create subtask + Subtask s = + new Subtask( + "S1", + "", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2035, 1, 1, 12, 0), + epicId); + assertEquals(201, post(baseUrl, "/subtasks", gson.toJson(s)).statusCode()); + int sid = firstIdFromArray(get(baseUrl, "/subtasks").body()); + + // get by id + assertEquals(200, get(baseUrl, "/subtasks/" + sid).statusCode()); + + // list epic's subtasks -> 1 + JsonArray arr = + JsonParser.parseString(get(baseUrl, "/epics/" + epicId + "/subtasks").body()) + .getAsJsonArray(); + assertEquals(1, arr.size()); + + // overlap + Subtask clash = + new Subtask( + "S2", + "", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2035, 1, 1, 12, 5), + epicId); + assertEquals(406, post(baseUrl, "/subtasks", gson.toJson(clash)).statusCode()); + + // update existing + s.setId(sid); + s.setStatus(Status.IN_PROGRESS); + s.setStartTime(LocalDateTime.of(2035, 1, 1, 12, 30)); + s.setDuration(Duration.ofMinutes(20)); + assertEquals(201, post(baseUrl, "/subtasks", gson.toJson(s)).statusCode()); + + // delete + assertEquals(200, delete(baseUrl, "/subtasks/" + sid).statusCode()); + assertEquals(404, get(baseUrl, "/subtasks/" + sid).statusCode()); + } +} diff --git a/src/test/java/http/HttpTaskServerTasksTest.java b/src/test/java/http/HttpTaskServerTasksTest.java new file mode 100644 index 0000000..e347222 --- /dev/null +++ b/src/test/java/http/HttpTaskServerTasksTest.java @@ -0,0 +1,51 @@ +package http; + +import static http.TestHttpUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import model.Status; +import model.Task; +import org.junit.jupiter.api.*; + +class HttpTaskServerTasksTest extends BaseHttpTest { + + @BeforeEach + void setUp() throws IOException { + setUpBase(); // BaseHttpTest заполняет protected gson, baseUrl и т.д. + } + + @AfterEach + void tearDown() { + tearDownBase(); + } + + @Test + void addGetUpdateDelete_Task_ok_and_overlap406() throws Exception { + Task t1 = new Task("T1", "D1", Status.NEW); + t1.setDuration(Duration.ofMinutes(10)); + t1.setStartTime(LocalDateTime.of(2035, 1, 1, 10, 0)); + assertEquals(201, post(baseUrl, "/tasks", gson.toJson(t1)).statusCode()); + + int id = firstIdFromArray(get(baseUrl, "/tasks").body()); + assertTrue(id > 0); + + assertEquals(200, get(baseUrl, "/tasks/" + id).statusCode()); + + t1.setId(id); + t1.setStatus(Status.IN_PROGRESS); + t1.setStartTime(LocalDateTime.of(2035, 1, 1, 10, 30)); + t1.setDuration(Duration.ofMinutes(15)); + assertEquals(201, post(baseUrl, "/tasks", gson.toJson(t1)).statusCode()); + + Task clash = new Task("Clash", "overlap", Status.NEW); + clash.setDuration(Duration.ofMinutes(20)); + clash.setStartTime(LocalDateTime.of(2035, 1, 1, 10, 35)); + assertEquals(406, post(baseUrl, "/tasks", gson.toJson(clash)).statusCode()); + + assertEquals(200, delete(baseUrl, "/tasks/" + id).statusCode()); + assertEquals(404, get(baseUrl, "/tasks/" + id).statusCode()); + } +} diff --git a/src/test/java/http/PrioritizedUniquenessTest.java b/src/test/java/http/PrioritizedUniquenessTest.java new file mode 100644 index 0000000..0dc7675 --- /dev/null +++ b/src/test/java/http/PrioritizedUniquenessTest.java @@ -0,0 +1,73 @@ +package http; + +import static http.TestHttpUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import model.Epic; +import model.Status; +import model.Subtask; +import org.junit.jupiter.api.*; + +class PrioritizedUniquenessTest extends BaseHttpTest { + + private static final LocalDateTime BASE = LocalDateTime.of(2035, 1, 1, 0, 0); + private Gson gson; + + @BeforeEach + void setUp() throws IOException { + setUpBase(); + gson = HttpTaskServer.getGson(); + } + + @AfterEach + void tearDown() { + tearDownBase(); + } + + @Test + void updateSubtaskDoesNotDuplicateInPrioritized() throws Exception { + // 1) epic + Epic e = new Epic("E", ""); + e.setStatus(Status.NEW); + assertEquals(201, post(baseUrl, "/epics", gson.toJson(e)).statusCode()); + + int epicId = + external + ? firstIdFromArray(get(baseUrl, "/epics").body()) + : manager.getEpics().get(0).getId(); + + // 2) sub 12:00 + Subtask s = new Subtask("S", "", Status.NEW, Duration.ofMinutes(10), BASE.withHour(12), epicId); + assertEquals(201, post(baseUrl, "/subtasks", gson.toJson(s)).statusCode()); + + int subId = + external + ? firstIdFromArray(get(baseUrl, "/subtasks").body()) + : manager.getSubtasks().get(0).getId(); + + // 3) update same sub to 12:30 + s.setId(subId); + s.setStatus(Status.IN_PROGRESS); + s.setStartTime(BASE.withHour(12).withMinute(30)); + s.setDuration(Duration.ofMinutes(15)); + assertEquals(201, post(baseUrl, "/subtasks", gson.toJson(s)).statusCode()); + + // 4) /prioritized — ровно одно вхождение id + String prBody = get(baseUrl, "/prioritized").body(); + JsonArray arr = JsonParser.parseString(prBody).getAsJsonArray(); + int occurrences = 0; + for (int i = 0; i < arr.size(); i++) { + var obj = arr.get(i).getAsJsonObject(); + if (obj.has("id") && obj.get("id").getAsInt() == subId) { + occurrences++; + } + } + assertEquals(1, occurrences); + } +} diff --git a/src/test/java/http/TestHttpUtils.java b/src/test/java/http/TestHttpUtils.java new file mode 100644 index 0000000..ee5f64a --- /dev/null +++ b/src/test/java/http/TestHttpUtils.java @@ -0,0 +1,53 @@ +package http; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.IOException; +import java.net.URI; +import java.net.http.*; + +/** sprint-9: общие HTTP-хелперы для тестов (без копипасты в каждом тесте). */ +public final class TestHttpUtils { + + private TestHttpUtils() {} + + public static HttpResponse get(String baseUrl, String path) + throws IOException, InterruptedException { + return client() + .send( + HttpRequest.newBuilder(URI.create(baseUrl + path)).GET().build(), + HttpResponse.BodyHandlers.ofString()); + } + + public static HttpResponse post(String baseUrl, String path, String body) + throws IOException, InterruptedException { + return client() + .send( + HttpRequest.newBuilder(URI.create(baseUrl + path)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(), + HttpResponse.BodyHandlers.ofString()); + } + + public static HttpResponse delete(String baseUrl, String path) + throws IOException, InterruptedException { + return client() + .send( + HttpRequest.newBuilder(URI.create(baseUrl + path)).DELETE().build(), + HttpResponse.BodyHandlers.ofString()); + } + + /** Наивно берёт id первого элемента массива JSON. Удобно для тестов. */ + public static int firstIdFromArray(String json) { + JsonArray arr = JsonParser.parseString(json).getAsJsonArray(); + JsonObject o = arr.get(0).getAsJsonObject(); + return o.get("id").getAsInt(); + } + + /** Единый клиент — не плодим экземпляры. */ + public static HttpClient client() { + return HttpClient.newHttpClient(); + } +} diff --git a/src/test/java/manager/EpicStatusTest.java b/src/test/java/manager/EpicStatusTest.java new file mode 100644 index 0000000..c8a9ef7 --- /dev/null +++ b/src/test/java/manager/EpicStatusTest.java @@ -0,0 +1,67 @@ +package manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; + +import model.Epic; +import model.Status; +import model.Subtask; +import org.junit.jupiter.api.Test; + +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/java/manager/FileBackedLegacyFormatTest.java b/src/test/java/manager/FileBackedLegacyFormatTest.java new file mode 100644 index 0000000..eb8016f --- /dev/null +++ b/src/test/java/manager/FileBackedLegacyFormatTest.java @@ -0,0 +1,24 @@ +package manager; + +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/java/manager/FileBackedRoundTripTest.java b/src/test/java/manager/FileBackedRoundTripTest.java new file mode 100644 index 0000000..e812ba5 --- /dev/null +++ b/src/test/java/manager/FileBackedRoundTripTest.java @@ -0,0 +1,59 @@ +package manager; + +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)); + } +} diff --git a/src/test/java/manager/FileBackedTaskManagerTest.java b/src/test/java/manager/FileBackedTaskManagerTest.java new file mode 100644 index 0000000..9fe7bb1 --- /dev/null +++ b/src/test/java/manager/FileBackedTaskManagerTest.java @@ -0,0 +1,42 @@ +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/java/manager/InMemoryHistoryManagerTest.java b/src/test/java/manager/InMemoryHistoryManagerTest.java new file mode 100644 index 0000000..74df1b5 --- /dev/null +++ b/src/test/java/manager/InMemoryHistoryManagerTest.java @@ -0,0 +1,54 @@ +package manager; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.time.LocalDateTime; + +import model.Task; +import org.junit.jupiter.api.Test; + +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/java/manager/PrioritizedViewTest.java b/src/test/java/manager/PrioritizedViewTest.java new file mode 100644 index 0000000..21b9b3b --- /dev/null +++ b/src/test/java/manager/PrioritizedViewTest.java @@ -0,0 +1,41 @@ +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()); + } +} diff --git a/src/test/java/manager/TaskManagerTest.java b/src/test/java/manager/TaskManagerTest.java new file mode 100644 index 0000000..c973525 --- /dev/null +++ b/src/test/java/manager/TaskManagerTest.java @@ -0,0 +1,108 @@ +package manager; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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()); + } +} diff --git a/src/test/manager/EpicStatusTest.java b/src/test/manager/EpicStatusTest.java deleted file mode 100644 index edebe56..0000000 --- a/src/test/manager/EpicStatusTest.java +++ /dev/null @@ -1,66 +0,0 @@ -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/FileBackedLegacyFormatTest.java b/src/test/manager/FileBackedLegacyFormatTest.java deleted file mode 100644 index 298e002..0000000 --- a/src/test/manager/FileBackedLegacyFormatTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package manager; - -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 deleted file mode 100644 index b60f59a..0000000 --- a/src/test/manager/FileBackedRoundTripTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package manager; - -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)); - } -} diff --git a/src/test/manager/FileBackedTaskManagerTest.java b/src/test/manager/FileBackedTaskManagerTest.java deleted file mode 100644 index 06ccafa..0000000 --- a/src/test/manager/FileBackedTaskManagerTest.java +++ /dev/null @@ -1,41 +0,0 @@ -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/InMemoryHistoryManagerTest.java b/src/test/manager/InMemoryHistoryManagerTest.java deleted file mode 100644 index ed94f45..0000000 --- a/src/test/manager/InMemoryHistoryManagerTest.java +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 002850a..0000000 --- a/src/test/manager/InMemoryTaskManagerTest.java +++ /dev/null @@ -1,9 +0,0 @@ -package manager; - -class InMemoryTaskManagerTest extends TaskManagerTest { - - @Override - protected InMemoryTaskManager createManager() { - return new InMemoryTaskManager(); - } -} diff --git a/src/test/manager/PrioritizedViewTest.java b/src/test/manager/PrioritizedViewTest.java deleted file mode 100644 index 0eaf1de..0000000 --- a/src/test/manager/PrioritizedViewTest.java +++ /dev/null @@ -1,39 +0,0 @@ -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()); - } -} diff --git a/src/test/manager/TaskManagerTest.java b/src/test/manager/TaskManagerTest.java deleted file mode 100644 index 8e8e776..0000000 --- a/src/test/manager/TaskManagerTest.java +++ /dev/null @@ -1,108 +0,0 @@ -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()); - } -} diff --git a/src/util/CsvUtils.java b/src/util/CsvUtils.java index ae16816..1572038 100644 --- a/src/util/CsvUtils.java +++ b/src/util/CsvUtils.java @@ -5,24 +5,24 @@ import java.time.format.DateTimeParseException; public final class CsvUtils { - private CsvUtils() {} + private CsvUtils() {} - public static String escape(String s) { - return s == null ? "" : s; - } + 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; - } - } + 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; + } } + } }