diff --git a/Individual Project.jar b/Individual Project.jar new file mode 100644 index 000000000..4f813b367 Binary files /dev/null and b/Individual Project.jar differ diff --git a/docs/README.md b/docs/README.md index 47b9f984f..3d46e4d7b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,141 @@ -# Duke User Guide +# PlopBot User Guide -// Update the title above to match the actual product name +![PlopBot Screenshot](Screenshot.png) -// Product screenshot goes here +PlopBot is a user-friendly task management application that helps you keep track of your to-dos, deadlines, and events. With its simple command-line interface, you can easily add, view, and manage your tasks. -// Product intro goes here -## Adding deadlines +## Adding Tasks -// Describe the action and its outcome. +### 1. Adding To-Dos -// Give examples of usage +To add a simple to-do task: -Example: `keyword (optional arguments)` + todo + +Example: 'todo Buy Groceries' -// A description of the expected outcome goes here +``` +Expected output: + Added: [T][ ] Buy Groceries + You now have 1 tasks in the list. +``` + +### 2. Adding Deadlines + +To add a task with a deadline: + + deadline /by + +Example: 'deadline Submit Report /by 2024-01-01' + +``` +Expected output: + Added: [D][ ] Submit Report (by: 2024-01-01) + You now have 2 tasks in the list. +``` + +Note: Dates can be in the format 'YYYY-MM-DD', or day names like 'Monday', 'Tue', etc. + +### 3. Adding Events + +To add an event: + + event /from /to + +Example: 'event Team meeting /from 2023-06-10 14:00 /to 2023-06-10 15:30' ``` -expected output +Expected output: + Added: [E][ ] Team meeting (from: 2023-06-10 14:00 to: 2023-06-10 15:30) + You now have 3 tasks in the list. ``` -## Feature ABC -// Feature details +## Viewing Tasks + +To view all your tasks, simply write: + + list + +``` +Expected output: + Here are the tasks in your list: + 1.[T][ ] Buy Groceries + 2.[D][ ] Submit Report (by: 2024-01-01) + 3.[E][ ] Team meeting (from: 2023-06-10 14:00 to: 2023-06-10 15:30) +``` -## Feature XYZ +## Marking Tasks as Complete -// Feature details \ No newline at end of file +To mark a task as 'done': + + mark + +Example: 'mark 1' + +``` +Expected output: + I've marked this task as done: + [T][X] Buy Groceries +``` + + +## Unmarking Tasks + +To unmark a task: + + unmark + +Example: 'unmark 1' + +``` +Expected output: + I've unmarked this task: + [T][ ] Buy Groceries +``` + + +## Deleting Tasks + +To delete a task: + + delete + +Example: 'delete 2' + +``` +Expected output: + Removed: [D][ ] Submit Report (by: 2024-01-01) + You now have 2 tasks in the list. +``` + + +## Finding Tasks + +To find tasks containing a keyword: + + find + +Example: 'find meeting' + +``` +Expected output: + Here are the matching tasks in your list: + 1.[E][ ] Team meeting (from: 2023-06-10 14:00 to: 2023-06-10 15:30) +``` + + +## Exiting PlopBot + +To exit the program, simply write one of the following: + + bye + exit + quit + +``` +Expected output: + Thank you for choosing PlopBot. Have a great day! +``` diff --git a/docs/Screenshot.png b/docs/Screenshot.png new file mode 100644 index 000000000..d861db98b Binary files /dev/null and b/docs/Screenshot.png differ diff --git a/out/artifacts/Individual_Project_jar/Individual Project.jar b/out/artifacts/Individual_Project_jar/Individual Project.jar new file mode 100644 index 000000000..4f813b367 Binary files /dev/null and b/out/artifacts/Individual_Project_jar/Individual Project.jar differ diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334c..000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 000000000..49d2c8b51 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: PlopBot + diff --git a/src/main/java/Parser.java b/src/main/java/Parser.java new file mode 100644 index 000000000..1b3abe613 --- /dev/null +++ b/src/main/java/Parser.java @@ -0,0 +1,337 @@ +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAdjusters; + +/** + * Parser class responsible for parsing user input and creating Task objects. + * This class handles the conversion of string commands into structured Task objects, + * including parsing dates and times in various formats. + */ +public class Parser { + private static final String DEADLINE_SEPARATOR = " /by "; + private static final String EVENT_TIME_SEPARATOR = " /from | /to "; + + /** + * Parses a user input string into command components. + * Splits the input into a command type and its details. + * + * @param userInput - The raw input string from the user + * @return - String array where index 0 contains the command type and index 1 contains the details + * @throws PlopBotException - If the input is null, empty, or cannot be parsed + */ + public String[] parseCommand(String userInput) throws PlopBotException { + if (userInput == null || userInput.trim().isEmpty()) { + throw new PlopBotException("Empty command"); + } + return userInput.split(" ", 2); + } + + /** + * Creates a Task object based on the parsed command components. + * Supports creation of todo, deadline, and event tasks. + * + * @param commandParts - Array containing the command type and details + * @return - A new Task object of the appropriate type + * @throws PlopBotException - If the command format is invalid or task creation fails + */ + public Task parseTask(String[] commandParts) throws PlopBotException { + validateCommandParts(commandParts); + String type = commandParts[0]; + String details = commandParts[1]; + + return switch (type) { + case "todo" -> createTodoTask(details); + case "deadline" -> createDeadlineTask(details); + case "event" -> createEventTask(details); + default -> throw new PlopBotException("Unknown task type: " + type); + }; + } + + /** + * Validates that the command parts array contains the required components. + * + * @param commandParts - Array of command components to validate + * @throws PlopBotException - If the array is null or has insufficient components + */ + private void validateCommandParts(String[] commandParts) throws PlopBotException { + if (commandParts == null || commandParts.length < 2) { + throw new PlopBotException("Invalid task format"); + } + } + + /** + * Creates a simple todo task with the given description. + * + * @param details - The description of the todo task + * @return - A new Task object representing a todo task + */ + private Task createTodoTask(String details) { + return new Task(details); + } + + /** + * Creates a deadline task with the given description and deadline date. + * + * @param details - The description and deadline information + * @return - A new Task object representing a deadline task + * @throws PlopBotException - If the deadline format is invalid + */ + private Task createDeadlineTask(String details) throws PlopBotException { + try { + String[] deadlineParts = details.split(DEADLINE_SEPARATOR, 2); + if (deadlineParts.length != 2) { + throw new PlopBotException("Invalid deadline format. Usage: deadline description /by DATE"); + } + return new Task(deadlineParts[0], parseDateString(deadlineParts[1])); + } catch (IllegalArgumentException e) { + throw new PlopBotException("Invalid date format: " + e.getMessage()); + } + } + + /** + * Creates an event task with the given description, start time, and end time. + * + * @param details - The description and event timing information + * @return - A new Task object representing an event task + * @throws PlopBotException - If the event format or timing is invalid + */ + private Task createEventTask(String details) throws PlopBotException { + try { + String[] eventParts = details.split(EVENT_TIME_SEPARATOR); + if (eventParts.length != 3) { + throw new PlopBotException("Invalid event format. Usage: event description /from START_TIME /to END_TIME"); + } + + validateEventTimeParts(eventParts); + return createEventTaskFromParts(eventParts); + } catch (IllegalArgumentException e) { + throw new PlopBotException("Invalid date/time format: " + e.getMessage()); + } + } + + /** + * Validates that event time parts are not empty. + * + * @param eventParts - Array containing event description and time components + * @throws PlopBotException - If either time component is empty + */ + private void validateEventTimeParts(String[] eventParts) throws PlopBotException { + if (eventParts[1].trim().isEmpty()) { + throw new PlopBotException("Start time cannot be empty. Usage: event description /from START_TIME /to END_TIME"); + } + if (eventParts[2].trim().isEmpty()) { + throw new PlopBotException("End time cannot be empty. Usage: event description /from START_TIME /to END_TIME"); + } + } + + /** + * Creates an event task from validated event parts. + * + * @param eventParts - Array containing event description and time components + * @return - A new Task object representing an event task + * @throws PlopBotException - If the time parsing fails or end time is before start time + */ + private Task createEventTaskFromParts(String[] eventParts) throws PlopBotException { + try { + LocalDateTime startTime = parseDateTimeString(eventParts[1], null); + LocalDateTime endTime = parseDateTimeString(eventParts[2], startTime); + validateEventTimes(startTime, endTime); + return new Task(eventParts[0], startTime, endTime); + } catch (DateTimeParseException e) { + throw new PlopBotException("Invalid date/time format. Time must be in format 'YYYY-MM-DD HH:mm' or 'DAY HHam/pm'"); + } + } + + /** + * Validates that the end time is not before the start time. + * + * @param start - Event start time + * @param end - Event end time + * @throws PlopBotException - If end time is before start time + */ + private void validateEventTimes(LocalDateTime start, LocalDateTime end) throws PlopBotException { + if (end.isBefore(start)) { + throw new PlopBotException("End time cannot be before start time"); + } + } + + /** + * Parses a date string into a LocalDate object. + * Supports standard ISO format (YYYY-MM-DD) and relative dates (today, tomorrow, day names). + * + * @param dateString - The date string to parse + * @return - LocalDate object representing the parsed date + * @throws PlopBotException - If the date string is empty or invalid + */ + private static LocalDate parseDateString(String dateString) throws PlopBotException { + if (dateString == null || dateString.trim().isEmpty()) { + throw new PlopBotException("Date cannot be empty"); + } + + try { + return parseStandardDate(dateString.trim()); + } catch (DateTimeParseException e) { + try { + return parseRelativeDate(dateString.trim()); + } catch (IllegalArgumentException ex) { + throw new PlopBotException("Invalid date format. Use 'YYYY-MM-DD', 'today', 'tomorrow', or day name"); + } + } + } + + /** + * Parses a date string in standard ISO format (YYYY-MM-DD). + * + * @param dateString - The date string to parse + * @return - LocalDate object representing the parsed date + */ + private static LocalDate parseStandardDate(String dateString) { + return LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE); + } + + /** + * Parses a relative date string (today, tomorrow, day names). + * + * @param dateString - The relative date string to parse + * @return - LocalDate object representing the parsed date + */ + private static LocalDate parseRelativeDate(String dateString) { + LocalDate now = LocalDate.now(); + String lowercaseDate = dateString.toLowerCase(); + + if (lowercaseDate.equals("today")) return now; + if (lowercaseDate.equals("tomorrow")) return now.plusDays(1); + + return parseDayOfWeek(dateString, now); + } + + /** + * Parses a day of week name into a date. + * + * @param dateString - Day name to parse + * @param now - Reference date for calculating the next occurrence + * @return - LocalDate object representing the next occurrence of the specified day + */ + private static LocalDate parseDayOfWeek(String dateString, LocalDate now) { + try { + DayOfWeek day = DayOfWeek.valueOf(dateString.toUpperCase()); + return now.with(TemporalAdjusters.next(day)); + } catch (IllegalArgumentException e) { + return parseAbbreviatedDay(dateString, now); + } + } + + /** + * Parses an abbreviated day name (Mon, Tue, etc.). + * + * @param dateString - Abbreviated day name to parse + * @param now - Reference date for calculating the next occurrence + * @return - LocalDate object representing the next occurrence of the specified day + * @throws IllegalArgumentException - If the abbreviated day name is invalid + */ + private static LocalDate parseAbbreviatedDay(String dateString, LocalDate now) { + String[] shortDays = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + + for (int i = 0; i < shortDays.length; i++) { + if (shortDays[i].equalsIgnoreCase(dateString)) { + return now.with(TemporalAdjusters.next(DayOfWeek.of(i + 1))); + } + } + throw new IllegalArgumentException("Unable to parse date: " + dateString); + } + + /** + * Parses a date-time string into a LocalDateTime object. + * Supports standard format (YYYY-MM-DD HH:mm) and relative formats. + * + * @param dateTimeString - The date-time string to parse + * @param referenceTime - Optional reference time for relative parsing + * @return - LocalDateTime object representing the parsed date-time + * @throws PlopBotException - If the date-time string is empty or invalid + */ + private static LocalDateTime parseDateTimeString(String dateTimeString, LocalDateTime referenceTime) throws PlopBotException { + if (dateTimeString == null || dateTimeString.trim().isEmpty()) { + throw new PlopBotException("Date/time cannot be empty"); + } + + try { + return LocalDateTime.parse(dateTimeString.trim(), + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + } catch (DateTimeParseException e1) { + try { + return parseRelativeDateTime(dateTimeString.trim(), referenceTime); + } catch (Exception e2) { + throw new PlopBotException("Invalid time format. Use 'YYYY-MM-DD HH:mm' or 'DAY HHam/pm'"); + } + } + } + + /** + * Parses a relative date-time string. + * + * @param dateTimeString - The relative date-time string to parse + * @param referenceTime - Optional reference time for relative parsing + * @return - LocalDateTime object representing the parsed date-time + * @throws PlopBotException - If the parsing fails + */ + private static LocalDateTime parseRelativeDateTime(String dateTimeString, LocalDateTime referenceTime) throws PlopBotException { + String[] parts = dateTimeString.split(" ", 2); + if (parts.length != 2) { + throw new PlopBotException("Invalid date/time format. Use 'YYYY-MM-DD HH:mm' or 'DAY HHam/pm'"); + } + + LocalDate date = parseDateString(parts[0]); + LocalTime time = parseTimeString(parts[1]); + return LocalDateTime.of(date, time); + } + + /** + * Parses a time string in either 24-hour or AM/PM format. + * + * @param timeString - The time string to parse + * @return - LocalTime object representing the parsed time + * @throws PlopBotException - If the time format is invalid + */ + private static LocalTime parseTimeString(String timeString) throws PlopBotException { + try { + return LocalTime.parse(timeString, DateTimeFormatter.ofPattern("HH:mm")); + } catch (DateTimeParseException e) { + try { + return parseAmPmTime(timeString); + } catch (Exception ex) { + throw new PlopBotException("Invalid time format. Use 'HH:mm' (24-hour) or 'HHam/pm'"); + } + } + } + + /** + * Parses a time string in AM/PM format. + * + * @param timeString - The AM/PM time string to parse + * @return - LocalTime object representing the parsed time + * @throws PlopBotException - If the AM/PM time format is invalid + */ + private static LocalTime parseAmPmTime(String timeString) throws PlopBotException { + String formattedTime = timeString.toLowerCase(); + if (!formattedTime.endsWith("am") && !formattedTime.endsWith("pm")) { + throw new PlopBotException("Invalid time format. Must end with 'am' or 'pm'"); + } + + try { + int hour = Integer.parseInt(formattedTime.substring(0, formattedTime.length() - 2)); + boolean isPm = formattedTime.endsWith("pm"); + + if (hour < 1 || hour > 12) { + throw new PlopBotException("Hour must be between 1 and 12"); + } + + if (isPm && hour < 12) hour += 12; + else if (!isPm && hour == 12) hour = 0; + + return LocalTime.of(hour, 0); + } catch (NumberFormatException e) { + throw new PlopBotException("Invalid time format: " + timeString); + } + } +} diff --git a/src/main/java/PlopBot.java b/src/main/java/PlopBot.java new file mode 100644 index 000000000..c70a56583 --- /dev/null +++ b/src/main/java/PlopBot.java @@ -0,0 +1,292 @@ +import java.util.ArrayList; + +/** + * Main class for the PlopBot task management application. + * Handles the core functionality of managing tasks, processing commands, + * and coordinating between different components (UI, Storage, Parser). + */ +public class PlopBot { + private Storage storage; + private TaskList tasks; + private Ui ui; + private Parser parser; + + /** + * Main entry point for the PlopBot application. + * Initializes and runs the bot with default storage location. + * + * @param args - Command line arguments (not used) + */ + public static void main(String[] args) { + new PlopBot("data/tasks.txt").run(); + } + + /** + * Constructs a new PlopBot instance with specified storage location. + * Initializes UI, storage, task list, and parser components. + * + * @param filePath - Path to the file where tasks will be stored + */ + public PlopBot(String filePath) { + ui = new Ui(); + storage = new Storage(filePath); + try { + tasks = new TaskList(storage.load()); + } catch (PlopBotException e) { + ui.showLoadingError(); + tasks = new TaskList(); + } + parser = new Parser(); + } + + /** + * Starts the main execution loop of PlopBot. + * Continuously reads and processes user commands until exit is requested. + */ + public void run() { + ui.showWelcome(); + boolean isExit = false; + while (!isExit) { + String fullCommand = ui.readCommand(); + try { + isExit = processCommand(fullCommand); + } catch (PlopBotException e) { + ui.showError(e.getMessage()); + } + } + ui.showExit(); + } + + /** + * Processes a single command from the user's input. + * + * @param fullCommand - The full command string input by the user + * @return - Boolean indicating if the program should exit + * @throws PlopBotException - If the command is invalid or execution fails + */ + private boolean processCommand(String fullCommand) throws PlopBotException { + String[] commandParts = parser.parseCommand(fullCommand); + switch (commandParts[0]) { + case "bye": + case "exit": + case "quit": + return true; + case "todo": + handleAddTask(commandParts); + break; + case "deadline": + handleAddDeadline(commandParts); + break; + case "event": + handleAddEvent(commandParts); + break; + case "delete": + handleDeleteTask(commandParts); + break; + case "find": + handleFindTasks(commandParts); + break; + case "list": + handleListTasks(); + break; + case "mark": + handleMarkTask(commandParts); + break; + case "unmark": + handleUnmarkTask(commandParts); + break; + default: + throw new PlopBotException("Unknown command"); + } + return false; + } + + /** + * Common method to add a task and save to storage. + * + * @param task - The task to add + * @throws PlopBotException - If task creation or saving fails + */ + private void addAndSaveTask(Task task) throws PlopBotException { + tasks.addTask(task); + storage.save(tasks.getTasks()); + ui.showTaskAdded(task, tasks.size()); + } + + /** + * Handles adding a new task and saving it. + * + * @param commandParts - The parsed command parts + * @throws PlopBotException - If task creation or saving fails + */ + private void handleAddTask(String[] commandParts) throws PlopBotException { + Task newTask = parser.parseTask(commandParts); + addAndSaveTask(newTask); + } + + /** + * Handles adding a deadline task with proper error handling. + * + * @param commandParts - The parsed command parts + * @throws PlopBotException - If deadline creation or saving fails + */ + private void handleAddDeadline(String[] commandParts) throws PlopBotException { + try { + Task newDeadline = parser.parseTask(commandParts); + addAndSaveTask(newDeadline); + } catch (PlopBotException e) { + throw new PlopBotException(formatDeadlineError(e.getMessage())); + } + } + + /** + * Formats the deadline error message. + * + * @param baseMessage - The base error message + * @return - Formatted error message + */ + private String formatDeadlineError(String baseMessage) { + return String.format("%s\n Usage: deadline description /by DATE" + + "\n DATE can be 'Sunday', 'Mon', 'Tuesday', or 'YYYY-MM-DD'", + baseMessage); + } + + /** + * Handles adding an event task with proper error handling. + * + * @param commandParts - The parsed command parts + * @throws PlopBotException - If event creation or saving fails + */ + private void handleAddEvent(String[] commandParts) throws PlopBotException { + try { + Task newEvent = parser.parseTask(commandParts); + addAndSaveTask(newEvent); + } catch (PlopBotException e) { + throw new PlopBotException(formatEventError(e.getMessage())); + } + } + + /** + * Formats the event error message. + * + * @param baseMessage - The base error message + * @return - Formatted error message + */ + private String formatEventError(String baseMessage) { + return String.format("%s\n Usage: event description /from START_TIME /to END_TIME" + + "\n TIME can be 'Mon 2pm', 'Tuesday 14:00', or 'YYYY-MM-DD HH:MM'", + baseMessage); + } + + /** + * Handles deleting a task. + *t + * @param commandParts - The parsed command parts + * @throws PlopBotException - If the command format is invalid or task deletion fails + */ + private void handleDeleteTask(String[] commandParts) throws PlopBotException { + if (commandParts.length != 2) { + throw new PlopBotException("Invalid delete command. Usage: delete "); + } + try { + int index = parseTaskIndex(commandParts[1]); + if (index < 0 || index >= tasks.size()) { + throw new PlopBotException("Task number " + (index + 1) + " does not exist. Please use 'list' to see all tasks."); + } + Task removedTask = tasks.removeTask(index); + storage.save(tasks.getTasks()); + ui.showTaskRemoved(removedTask, tasks.size()); + } catch (NumberFormatException e) { + throw new PlopBotException("Invalid task number. Please provide a number."); + } + } + + /** + * Handles finding tasks by keyword. + * + * @param commandParts - The parsed command parts + * @throws PlopBotException - If the find command format is invalid + */ + private void handleFindTasks(String[] commandParts) throws PlopBotException { + validateFindCommand(commandParts); + String keyword = commandParts[1]; + ArrayList matchingTasks = tasks.findTasks(keyword); + ui.showMatchingTasks(matchingTasks); + } + + /** + * Validates the find command format. + * + * @param commandParts - The parsed command parts + * @throws PlopBotException - If the command format is invalid + */ + private void validateFindCommand(String[] commandParts) throws PlopBotException { + if (commandParts.length < 2) { + throw new PlopBotException("The 'find' command requires a keyword.\n Usage: find "); + } + } + + /** + * Handles listing all tasks. + */ + private void handleListTasks() { + ui.showTasks(tasks.getTasks()); + } + + /** + * Parses a task index from string input. + * + * @param indexStr - The index string to parse + * @return - The parsed index (0-based) + * @throws PlopBotException - If the index string is not a valid number + */ + private int parseTaskIndex(String indexStr) throws PlopBotException { + try { + return Integer.parseInt(indexStr) - 1; + } catch (NumberFormatException e) { + throw new PlopBotException("Invalid task number. Please provide a number."); + } + } + + /** + * Handles marking a task as done. + * + * @param commandParts - The parsed command parts containing the task index + * @throws PlopBotException - If the command format is invalid or task marking fails + */ + private void handleMarkTask(String[] commandParts) throws PlopBotException { + if (commandParts.length != 2) { + throw new PlopBotException("Invalid mark command. Usage: mark "); + } + try { + int taskIndex = Integer.parseInt(commandParts[1]) - 1; + Task task = tasks.getTask(taskIndex); + task.markAsDone(); + storage.save(tasks.getTasks()); + ui.showTaskMarked(task); + } catch (NumberFormatException e) { + throw new PlopBotException("Invalid task number. Please provide a number."); + } + } + + /** + * Handles unmarking a task (setting it as not done). + * + * @param commandParts - The parsed command parts containing the task index + * @throws PlopBotException - If the command format is invalid or task unmarking fails + */ + private void handleUnmarkTask(String[] commandParts) throws PlopBotException { + if (commandParts.length != 2) { + throw new PlopBotException("Invalid unmark command. Usage: unmark "); + } + try { + int taskIndex = Integer.parseInt(commandParts[1]) - 1; + Task task = tasks.getTask(taskIndex); + task.markAsUndone(); + storage.save(tasks.getTasks()); + ui.showTaskUnmarked(task); + } catch (NumberFormatException e) { + throw new PlopBotException("Invalid task number. Please provide a number."); + } + } +} diff --git a/src/main/java/PlopBotException.java b/src/main/java/PlopBotException.java new file mode 100644 index 000000000..730efe72e --- /dev/null +++ b/src/main/java/PlopBotException.java @@ -0,0 +1,5 @@ +public class PlopBotException extends Exception { + public PlopBotException(String message) { + super(message); + } +} diff --git a/src/main/java/Storage.java b/src/main/java/Storage.java new file mode 100644 index 000000000..ffaf18a4a --- /dev/null +++ b/src/main/java/Storage.java @@ -0,0 +1,140 @@ +import java.io.*; +import java.nio.file.*; +import java.time.*; +import java.time.format.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Handles the persistence of task data to and from storage. + * This class manages reading and writing tasks to a file in a specified format. + * Each task is stored as a single line with fields separated by vertical bars (|). + * + * The file format for each task type is as follows: + * - Todo: T | isDone | description + * - Deadline: D | isDone | description | deadline + * - Event: E | isDone | description | startTime | endTime + */ +public class Storage { + private String filePath; + + /** + * Constructs a Storage object with the specified file path. + * + * @param filePath The path where task data will be stored + */ + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * Loads tasks from the storage file. + * Creates necessary directories if they don't exist. + * If the file exists, reads and parses each line into a Task object. + * + * @return - ArrayList of Task objects loaded from the file + * @throws PlopBotException - If there is an error reading the file or parsing tasks + */ + public ArrayList load() throws PlopBotException { + ArrayList tasks = new ArrayList<>(); + try { + Files.createDirectories(Paths.get(filePath).getParent()); + if (Files.exists(Paths.get(filePath))) { + List lines = Files.readAllLines(Paths.get(filePath)); + for (String line : lines) { + tasks.add(parseTaskFromString(line)); + } + } + } catch (IOException e) { + throw new PlopBotException("Error loading tasks: " + e.getMessage()); + } + return tasks; + } + + /** + * Saves the provided list of tasks to the storage file. + * Creates necessary directories if they don't exist. + * Each task is converted to a string format before being written. + * + * @param tasks - ArrayList of Task objects to be saved + * @throws PlopBotException - If there is an error writing to the file + */ + public void save(ArrayList tasks) throws PlopBotException { + try { + Files.createDirectories(Paths.get(filePath).getParent()); + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath)); + for (Task task : tasks) { + writer.write(convertTaskToString(task)); + writer.newLine(); + } + writer.close(); + } catch (IOException e) { + throw new PlopBotException("Error saving tasks: " + e.getMessage()); + } + } + + /** + * Parses a string representation of a task into a Task object. + * The string should be in the format: type | isDone | description [| additional fields...] + * where type is 'T' for Todo, 'D' for Deadline, or 'E' for Event. + * + * @param line - The string representation of the task + * @return - Task object parsed from the string + * @throws PlopBotException - If the string format is invalid or contains invalid data + */ + private Task parseTaskFromString(String line) throws PlopBotException { + String[] parts = line.split("\\|"); + if (parts.length < 3) { + throw new PlopBotException("Invalid task format: " + line); + } + String type = parts[0].trim(); + boolean isDone = parts[1].trim().equals("1"); + String description = parts[2].trim(); + + Task task; + switch (type) { + case "T": + task = new Task(description); + break; + case "D": + if (parts.length < 4) throw new PlopBotException("Invalid deadline format: " + line); + LocalDate deadline = LocalDate.parse(parts[3].trim(), DateTimeFormatter.ISO_LOCAL_DATE); + task = new Task(description, deadline); + break; + case "E": + if (parts.length < 5) throw new PlopBotException("Invalid event format: " + line); + LocalDateTime startTime = LocalDateTime.parse(parts[3].trim(), DateTimeFormatter.ISO_LOCAL_DATE_TIME); + LocalDateTime endTime = LocalDateTime.parse(parts[4].trim(), DateTimeFormatter.ISO_LOCAL_DATE_TIME); + task = new Task(description, startTime, endTime); + break; + default: + throw new PlopBotException("Unknown task type: " + type); + } + if (isDone) task.toggleStatus(); + return task; + } + + /** + * Converts a Task object into its string representation for storage. + * The format varies based on the task type: + * - Todo: T | isDone | description + * - Deadline: D | isDone | description | deadline + * - Event: E | isDone | description | startTime | endTime + * + * @param task - The Task object to convert + * @return - String representation of the task + */ + private String convertTaskToString(Task task) { + String baseString = String.format("%s | %d | %s", + task.getTypeIcon(), task.getStatus() ? 1 : 0, task.getName()); + switch (task.getTypeIcon()) { + case "D": + return baseString + " | " + task.getDeadline().format(DateTimeFormatter.ISO_LOCAL_DATE); + case "E": + return baseString + " | " + task.getStartTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + " | " + task.getEndTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + default: + return baseString; + } + } +} diff --git a/src/main/java/Task.java b/src/main/java/Task.java new file mode 100644 index 000000000..c97976525 --- /dev/null +++ b/src/main/java/Task.java @@ -0,0 +1,188 @@ +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Represents a task in the PlopBot task management system. + * Tasks can be of three types: TODO, EVENT, or DEADLINE. + * Each task has a name and completion status, with additional + * temporal attributes based on its type. + */ +public class Task { + private String taskName; + private TaskType type; + private boolean isDone; + private LocalDate deadline; + private LocalDateTime startTime; + private LocalDateTime endTime; + + /** + * Enumeration of possible task types in the system. + * TODO: A basic task with just a description + * EVENT: A task with start and end times + * DEADLINE: A task with a completion deadline + */ + public enum TaskType { + TODO, EVENT, DEADLINE + } + + /** + * Constructs a new TODO task with the specified name. + * The task is initially marked as not done. + * + * @param taskName - The name or description of the task + */ + public Task(String taskName) { + this.taskName = taskName; + this.type = TaskType.TODO; + this.isDone = false; + } + + /** + * Constructs a new DEADLINE task with the specified name and deadline. + * The task is initially marked as not done. + * + * @param taskName - The name or description of the task + * @param deadline - The date by which the task must be completed + */ + public Task(String taskName, LocalDate deadline) { + this.taskName = taskName; + this.type = TaskType.DEADLINE; + this.deadline = deadline; + this.isDone = false; + } + + /** + * Constructs a new EVENT task with the specified name, start time, and end time. + * The task is initially marked as not done. + * + * @param taskName - The name or description of the task + * @param startTime - The date and time when the event starts + * @param endTime - The date and time when the event ends + */ + public Task(String taskName, LocalDateTime startTime, LocalDateTime endTime) { + this.taskName = taskName; + this.type = TaskType.EVENT; + this.startTime = startTime; + this.endTime = endTime; + this.isDone = false; + } + + /** + * Returns the type identifier of the task. + * 'T' for TODO, 'D' for DEADLINE, 'E' for EVENT. + * + * @return - A single character string representing the task type + */ + public String getTypeIcon() { + switch (type) { + case TODO: return "T"; + case DEADLINE: return "D"; + case EVENT: return "E"; + default: return "?"; + } + } + + /** + * Returns the name or description of the task. + * + * @return - The task's name + */ + public String getName() { + return taskName; + } + + /** + * Returns the deadline date for DEADLINE tasks. + * + * @return - The task's deadline date, or null if not a DEADLINE task + */ + public LocalDate getDeadline() { + return deadline; + } + + /** + * Returns the start time for EVENT tasks. + * + * @return - The event's start time, or null if not an EVENT task + */ + public LocalDateTime getStartTime() { + return startTime; + } + + /** + * Returns the end time for EVENT tasks. + * + * @return - The event's end time, or null if not an EVENT task + */ + public LocalDateTime getEndTime() { + return endTime; + } + + /** + * Returns the completion status of the task. + * + * @return - true if the task is marked as done, false otherwise + */ + public boolean getStatus() { + return isDone; + } + + /** + * Toggles the completion status of the task. + * If the task was marked as done, it will be marked as not done, and vice versa. + */ + public void toggleStatus() { + this.isDone = !this.isDone; + } + + /** + * Marks the task as completed. + * + * @throws PlopBotException - If the task is already marked as done + */ + public void markAsDone() throws PlopBotException { + if (isDone) { + throw new PlopBotException("Task is already marked as done."); + } + isDone = true; + } + + /** + * Marks the task as not completed. + * + * @throws PlopBotException - If the task is already marked as not done + */ + public void markAsUndone() throws PlopBotException { + if (!isDone) { + throw new PlopBotException("Task is not yet done."); + } + isDone = false; + } + + /** + * Returns a string representation of the task. + * The format varies based on the task type: + * - TODO: [T][status] description + * - DEADLINE: [D][status] description (by: YYYY-MM-DD) + * - EVENT: [E][status] description (from: YYYY-MM-DD HH:mm to: YYYY-MM-DD HH:mm) + * where status is 'X' for done tasks and ' ' for not done tasks. + * + * @return - A formatted string representation of the task + */ + @Override + public String toString() { + String base = String.format("[%s][%s] %s", getTypeIcon(), isDone ? "X" : " ", taskName); + switch (type) { + case DEADLINE: + return String.format("%s (by: %s)", base, + deadline.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + case EVENT: + return String.format("%s (from: %s to: %s)", base, + startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), + endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + default: + return base; + } + } +} diff --git a/src/main/java/TaskList.java b/src/main/java/TaskList.java new file mode 100644 index 000000000..9fee44253 --- /dev/null +++ b/src/main/java/TaskList.java @@ -0,0 +1,99 @@ +import java.util.ArrayList; + +/** + * Manages a collection of tasks for the PlopBot application. + * This class provides methods to add, remove, find, and manage tasks in a list. + * It serves as the primary data structure for task management operations. + */ +public class TaskList { + private ArrayList tasks; + + /** + * Constructs an empty TaskList. + * Initializes a new ArrayList to store Task objects. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Constructs a TaskList with an existing collection of tasks. + * + * @param tasks - The ArrayList of Task objects to initialize the TaskList with + */ + public TaskList(ArrayList tasks) { + this.tasks = tasks; + } + + /** + * Adds a new task to the list. + * + * @param task - The Task object to be added to the list + */ + public void addTask(Task task) { + tasks.add(task); + } + + /** + * Removes and returns a task at the specified index. + * + * @param index - The index of the task to remove (0-based) + * @return - The removed Task object + * @throws PlopBotException - If the index is out of bounds or invalid + */ + public Task removeTask(int index) throws PlopBotException { + if (index < 0 || index >= tasks.size()) { + throw new PlopBotException("Invalid task index"); + } + return tasks.remove(index); + } + + /** + * Searches for tasks whose names contain the specified keyword. + * The search is case-insensitive. + * + * @param keyword - The search term to match against task names + * @return - An ArrayList containing all tasks whose names contain the keyword + */ + public ArrayList findTasks(String keyword) { + ArrayList matchingTasks = new ArrayList<>(); + for (Task task : tasks) { + if (task.getName().toLowerCase().contains(keyword.toLowerCase())) { + matchingTasks.add(task); + } + } + return matchingTasks; + } + + /** + * Returns the complete list of tasks. + * + * @return - An ArrayList containing all tasks in the list + */ + public ArrayList getTasks() { + return tasks; + } + + /** + * Retrieves a task at the specified index. + * + * @param index - The index of the task to retrieve (0-based) + * @return - The Task object at the specified index + * @throws PlopBotException - If the index is out of bounds or invalid + */ + public Task getTask(int index) throws PlopBotException { + if (index < 0 || index >= tasks.size()) { + throw new PlopBotException("Task index out of range."); + } + return tasks.get(index); + } + + /** + * Returns the total number of tasks in the list. + * + * @return - The number of tasks currently in the list + */ + public int size() { + return tasks.size(); + } +} diff --git a/src/main/java/Ui.java b/src/main/java/Ui.java new file mode 100644 index 000000000..6eb28d153 --- /dev/null +++ b/src/main/java/Ui.java @@ -0,0 +1,148 @@ +import java.util.ArrayList; +import java.util.Scanner; + +/** + * The UI component of PlopBot that handles all user interactions. + * This class is responsible for displaying messages to the user and + * getting user input from the command line interface. + */ +public class Ui { + private static final String HORIZONTAL_LINE = "//" + "\u2500".repeat(50); + private static final String ECHO_LINE = " " + "\u2500".repeat(48); + private Scanner scanner; + + /** + * Constructs a new UI instance and initializes the Scanner for reading user input. + */ + public Ui() { + scanner = new Scanner(System.in); + } + + /** + * Displays the welcome message when PlopBot starts. + * Includes a greeting and prompt for user input. + */ + public void showWelcome() { + System.out.println(HORIZONTAL_LINE); + System.out.println("Hello! I'm PlopBot."); + System.out.println("What can I do for you today?\n"); + } + + /** + * Displays the exit message when PlopBot terminates. + * Shows a farewell message to the user. + */ + public void showExit() { + System.out.println(HORIZONTAL_LINE); + System.out.println("Thank you for choosing PlopBot. Have a great day!\n"); + } + + /** + * Reads a command from the user's input. + * + * @return - A String containing the user's command, with leading and trailing whitespace removed + */ + public String readCommand() { + return scanner.nextLine().trim(); + } + + /** + * Displays all tasks in the user's task list. + * Each task is numbered and shown with its complete details. + * + * @param tasks - An ArrayList of Task objects to be displayed + */ + public void showTasks(ArrayList tasks) { + System.out.println(ECHO_LINE); + System.out.println(" Here are the tasks in your list:"); + for (int i = 0; i < tasks.size(); i++) { + System.out.println(" " + (i + 1) + "." + tasks.get(i)); + } + System.out.println(ECHO_LINE); + } + + /** + * Confirms the addition of a new task and shows the updated task count. + * + * @param task - The Task that was added to the list + * @param totalTasks - The current total number of tasks in the list + */ + public void showTaskAdded(Task task, int totalTasks) { + System.out.println(ECHO_LINE); + System.out.println(" Added: " + task); + System.out.println(" You now have " + totalTasks + " tasks in the list."); + System.out.println(ECHO_LINE); + } + + /** + * Confirms the removal of a task and shows the updated task count. + * + * @param task - The Task that was removed from the list + * @param totalTasks - The current total number of tasks in the list + */ + public void showTaskRemoved(Task task, int totalTasks) { + System.out.println(ECHO_LINE); + System.out.println(" Removed: " + task); + System.out.println(" You now have " + totalTasks + " tasks in the list."); + System.out.println(ECHO_LINE); + } + + /** + * Displays tasks that match a search criterion. + * If no tasks match, displays an appropriate message. + * + * @param tasks - An ArrayList of Task objects that match the search criteria + */ + public void showMatchingTasks(ArrayList tasks) { + System.out.println(ECHO_LINE); + if (tasks.isEmpty()) { + System.out.println(" No matching tasks found."); + } else { + System.out.println(" Here are the matching tasks in your list:"); + for (int i = 0; i < tasks.size(); i++) { + System.out.println(" " + (i + 1) + "." + tasks.get(i)); + } + } + System.out.println(ECHO_LINE); + } + + /** + * Displays confirmation that a task has been marked as complete. + * + * @param task - The Task that was marked as complete + */ + public void showTaskMarked(Task task) { + System.out.println(" I've marked this task as done:"); + System.out.println(" " + task.toString()); + } + + /** + * Displays confirmation that a task has been unmarked (set as incomplete). + * + * @param task - The Task that was unmarked + */ + public void showTaskUnmarked(Task task) { + System.out.println(" I've unmarked this task:"); + System.out.println(" " + task.toString()); + } + + /** + * Displays an error message to the user. + * The message is formatted with proper indentation and line breaks. + * + * @param message - The error message to display + */ + public void showError(String message) { + System.out.println(ECHO_LINE); + System.out.println(" Oops! " + message.replace("\n", "\n ")); + System.out.println(ECHO_LINE); + } + + /** + * Displays an error message when there's a problem loading the task file. + * Informs the user that the program will start with an empty task list. + */ + public void showLoadingError() { + showError("Problem loading tasks from file. Starting with an empty task list."); + } +}