diff --git a/docs/README.md b/docs/README.md index 8077118eb..2b1fd3586 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,82 @@ -# User Guide - +# Wenny Task Manager User Guide +Wenny is your friendly task manager that helps you keep track of your endless numbers of to do tasks, deadlines and events. ## Features +Notes about the command format: +1. Words in UPPER_CASE are the parameters to be supplied by the user. +e.g. in todo DESCRIPTION, DESCRIPTION is a parameter which can be used as `todo Homework`. + +2. Extraneous parameters for commands that do not take in parameters (such as list and bye) will be ignored. +e.g. if the command specifies `list 123`, it will be interpreted as list. + +## List current tasks `list` + +List all current tasks. + +Format: `list` + +## Exit the program `bye` + +Exit the program. + +Format: `bye` + +## Add a to-do task `todo` + +Add to-do task into the list. + +Format: `list DESCRIPTION` + +Example: +- `todo complete week 7 tasks` +- `todo sleep` + + +## Add a deadline task `deadline` +Add a task with a deadline. + +Format: `deadline DESCRIPTION /by DEADLINE` + +Example: +- `deadline complete week 8 tasks /by 12 Oct`. + + +## Add an event task `event` +Add an event into the task list. + +Format: `event DESCRIPTION /from START_TIME /to END_TIME`. + +Example: +- `event IA interview /from 7 Oct 2pm /to 7 Oct 4pm`. + +## Mark a task as done `mark` +Mark a task on the task list as done. -### Feature-ABC +Format: `mark TASK_NUMBER`. -Description of the feature. +Example: +- `mark 1` -### Feature-XYZ +## Mark a task as undone `unmark` +Mark a task on the task list as undone. -Description of the feature. +Format: `unmark TASK_NUMBER`. -## Usage +Example: +- `unmark 1`. -### `Keyword` - Describe action -Describe the action and its outcome. +## Delete a task `delete` +Delete a task from the task list. -Example of usage: +Format: `delete TASK_NUMBER`. -`keyword (optional arguments)` +Example: +- `delete 1`. -Expected outcome: +## Find the tasks that matches a certain keyword `find` +Find the tasks that matches a certain keyword . -Description of the outcome. +Format: `find KEYWORD`. -``` -expected output -``` +Example: +- `find John`. diff --git a/src/main/java/Command.java b/src/main/java/Command.java new file mode 100644 index 000000000..a1d927d3a --- /dev/null +++ b/src/main/java/Command.java @@ -0,0 +1,119 @@ +import java.util.ArrayList; + +/** + * Execute commands from the user. + */ +public class Command { + private String command; + private String arg; + private boolean isExit; + + /** + * Constructor of a Command + * + * @param command Command to be executed + * @param arg Arguments of the given command + */ + public Command(String command, String arg) { + this.command = command; + this.arg = arg; + this.isExit = false; + } + + public void setExit(boolean exit) { + isExit = exit; + } + + public boolean isExit() { + return isExit; + } + + /** + * Execute the command given by the user + * Each command is handled by its corresponding method in {@code TaskList}. If there + * is any changes to the list of tasks, it will be saved into the file through {@code Storage}. + * Relevant information will be shown to the user through corresponding methods in {@code Ui}. + * A custom exceptions will be thrown if an invalid command is received or an invalid argument of + * the respective command. + * + * @param taskList TaskList object that contains a list of Tasks + * @param ui User interface object that handles the printing of information + * @param storage Storage object that saves the updated list into filepath + * @throws DukeException Custom Exception with customer error message + */ + public void execute(TaskList taskList, Ui ui, Storage storage) throws DukeException{ + switch(this.command) { + case "bye": + setExit(true); + ui.showByeMessage(); + break; + case "list": + ui.showTasks(taskList); + break; + case "mark": + try { + taskList.markTask(this.arg, true); + storage.save(taskList); + ui.showMark(taskList, this.arg, true); + } catch (DukeException e) { + ui.showError(e); + } + break; + case "unmark": + try { + taskList.markTask(this.arg, false); + storage.save(taskList); + ui.showMark(taskList, this.arg, false); + } catch (DukeException e) { + ui.showError(e); + } + break; + case "todo": + try { + taskList.addTodo(this.arg); + storage.save(taskList); + ui.showAddTask(taskList); + } catch (DukeException e) { + ui.showError(e); + } + break; + case "deadline": + try { + taskList.addDeadline(this.arg); + storage.save(taskList); + ui.showAddTask(taskList); + } catch (DukeException e) { + ui.showError(e); + } + break; + case "event": + try { + taskList.addEvent(this.arg); + storage.save(taskList); + ui.showAddTask(taskList); + } catch (DukeException e) { + ui.showError(e); + } + break; + case "delete": + try { + Task removedTask = taskList.deleteTask(this.arg); + storage.save(taskList); + ui.showDeleteTask(taskList, removedTask); + } catch (DukeException e) { + ui.showError(e); + } + break; + case "find": + try { + ArrayList matchingTasks = taskList.find(this.arg); + ui.showFind(matchingTasks); + } catch (DukeException e) { + ui.showError(e); + } + break; + default: // Unknown command exception + throw new DukeException("Invalid input, please try again"); + } + } +} diff --git a/src/main/java/Deadline.java b/src/main/java/Deadline.java new file mode 100644 index 000000000..72724cc11 --- /dev/null +++ b/src/main/java/Deadline.java @@ -0,0 +1,31 @@ +public class Deadline extends Task { + String by; + public Deadline(String description, String by) { + super(description); + this.by = by; + } + public String getBy() { + return by; + } + public void setBy(String by) { + this.by = by; + } + + /** + * Returns the format of the task to be printed out to user + * @return String representation of the task + */ + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by + ")"; + } + + /** + * Returns the format of the task to be saved into a file + * @return String representation of the task + */ + @Override + public String toSave() { + return "D | " + super.toSave() + " | " + by; + } +} diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java index 5d313334c..74c0df72b 100644 --- a/src/main/java/Duke.java +++ b/src/main/java/Duke.java @@ -1,10 +1,62 @@ +import java.util.ArrayList; +import java.util.Scanner; + +/** + * This is the main program of Duke. + * Duke is a chatbot for user track tasks such as todos, deadlines and events. + * User can add tasks, mark tasks as done, delete tasks or find tasks based on a keyword. + * These tasks are automatically saved in a file in ./data/data.txt whenever an operation + * on the TaskList is made. This file will be loaded on future start up of the chatbot + * + */ public class Duke { + + private Storage storage; + private TaskList taskList; + private Ui ui; + + /** + * Constructor of Duke. Initialise User interface, Storage and TaskList instances. + * @param filePath Path to the file where tasks are to be loaded from and saved to. + */ + public Duke(String filePath) { + ui = new Ui(); + storage = new Storage(filePath); + ui.showLoadingFile(); + try { + taskList = new TaskList(storage.load()); + ui.showFileLoaded(); + } catch (DukeException e) { + ui.showError(e); + taskList = new TaskList(); + } + } + + /** + * To start up the chatbot. + * A greetings will be printed on the command line interface and the chatbot will + * await instructions from the user. The chatbot will run until an exit command is received. + * Any exception during the operation will produce an error message to notify the user + */ + public void run() { + Parser parser = new Parser(); + ui.showStartMessage(); + boolean isExit = false; + while (!isExit) { + try { + String fullCommand = ui.readCommand(); + Command c = parser.parse(fullCommand); + c.execute(taskList, ui, storage); + isExit = c.isExit(); + } catch (DukeException e) { + ui.showError(e); + } + } + System.exit(0); + } + public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); + new Duke("./data/data.txt").run(); } + } diff --git a/src/main/java/DukeException.java b/src/main/java/DukeException.java new file mode 100644 index 000000000..9e0c7d012 --- /dev/null +++ b/src/main/java/DukeException.java @@ -0,0 +1,19 @@ +/** + * Custom Exception for Duke + */ +public class DukeException extends Exception{ + String errorMessage; + public DukeException(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * Format error message for printing to command line interface. + * @return Return the error message of the specific exception in String format. + */ + @Override + public String toString() { + return errorMessage; + } + +} diff --git a/src/main/java/Event.java b/src/main/java/Event.java new file mode 100644 index 000000000..9e299cfa5 --- /dev/null +++ b/src/main/java/Event.java @@ -0,0 +1,45 @@ +public class Event extends Task { + String from; + String to; + + public Event(String description, String from, String to) { + super(description); + setFrom(from); + setTo(to); + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + /** + * Returns the format of the task to be printed out to user + * @return String representation of the task + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + from + ", to: " + to + ")"; + } + + /** + * Returns the format of the task to be saved into a file + * @return String representation of the task + */ + @Override + public String toSave() { + return "E | " + super.toSave() + " | " + from + " | " + to; + } +} + diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 000000000..9f37e4e0a --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: Duke + diff --git a/src/main/java/Parser.java b/src/main/java/Parser.java new file mode 100644 index 000000000..dab652756 --- /dev/null +++ b/src/main/java/Parser.java @@ -0,0 +1,22 @@ +/** + * Parser object that handles splitting of command line arguments given by the user. + */ +public class Parser { + + public Parser() { + } + + /** + * Split user input into command and argument of the command. + * + * @param fullCommand User input. + * @return Return a Command object for execution of the command. + */ + public Command parse(String fullCommand) { + String[] substr = fullCommand.split("\\s+", 2); + if (substr.length == 1) { + return new Command(substr[0], ""); + } + return new Command(substr[0], substr[1]); + } +} diff --git a/src/main/java/Storage.java b/src/main/java/Storage.java new file mode 100644 index 000000000..ccea9c230 --- /dev/null +++ b/src/main/java/Storage.java @@ -0,0 +1,122 @@ +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * Storage object that handles loading and saving of tasks into specified file + */ +public class Storage { + private String filePath; + + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * This method attempts to load pre-existing tasks from the file specified by + * the filepath. It will read the text file line by line and extract the information + * based on a format specified by the program. + * + * @return A TaskList object that contains the tasks loaded from the file. + * @throws DukeException If the file cannot be found or is corrupted. + */ + public ArrayList load() throws DukeException { + ArrayList allTasks = new ArrayList(); + try { + File f = new File(filePath); + Scanner s = new Scanner(f); + while (s.hasNext()) { + String line = s.nextLine(); + String[] lineTokens = line.split("\\|", 3); + String taskType = lineTokens[0].trim(); + String completionStatus = lineTokens[1].trim(); + String description = lineTokens[2].trim(); + switch (taskType) { + case "T": + ToDo todo = new ToDo(description); + allTasks.add(todo); + if (completionStatus.equals("1")) { + todo.setDone(true); + } + break; + case "D": + String[] deadlineTokens = description.split("\\|"); + description = deadlineTokens[0].trim(); + String by = deadlineTokens[1].trim(); + Deadline deadline = new Deadline(description, by); + allTasks.add(deadline); + if (completionStatus.equals("1")) { + deadline.setDone(true); + } + break; + case "E": + String[] eventTokens = description.split("\\|"); + description = eventTokens[0].trim(); + String from = eventTokens[1].trim(); + String to = eventTokens[2].trim(); + Event event = new Event(description, from, to); + allTasks.add(event); + if (completionStatus.equals("1")) { + event.setDone(true); + } + break; + default: + System.out.println("\tUnknown task encountered. Skipping."); + break; + } + } + } catch (FileNotFoundException e) { + throw new DukeException("Failed to load file: file not found."); + } catch (ArrayIndexOutOfBoundsException e) { + throw new DukeException("Failed to load file: file is corrupted."); + } + return allTasks; + } + + /** + * + * This method save the tasks in TaskList into the file specified by filepath. + * If the file specified by the filepath does not exist, it will create a new file as + * specified by the filepath. + * + * @param taskList TaskList object that contains the tasks. + * @throws DukeException If there is an error during the saving process due to corrupted + * data or invalid input. + */ + public void save(TaskList taskList) throws DukeException { + ArrayList tasks = taskList.getTasks(); + try { + createFile(); + FileWriter fw = new FileWriter(filePath); + for (Task task : tasks) { + fw.write(task.toSave() + "\n"); + } + fw.close(); + } catch (IOException e) { + throw new DukeException("An error has occurred while saving"); + } + } + + /** + * This method create the file specified by the filepath. + * + * @throws DukeException If it fails to create the specified file. + */ + public void createFile() throws DukeException { + try { + File f = new File(filePath); + if (f.exists()) { + return; + } + if (!f.getParentFile().exists()) { + f.getParentFile().mkdirs(); + } + f.createNewFile(); + } catch (IOException e) { + throw new DukeException("Cannot create file; reason: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/Task.java b/src/main/java/Task.java new file mode 100644 index 000000000..4bf169bc8 --- /dev/null +++ b/src/main/java/Task.java @@ -0,0 +1,40 @@ +public class Task { + private String description; + private boolean isDone; + public Task(String description) { + setDescription(description); + setDone(false); + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isDone() { + return isDone; + } + + public void setDone(boolean done) { + isDone = done; + } + + /** + * Returns the format of the task to be printed out to user + * @return String representation of the task + */ + public String toString() { + return (isDone ? "[X]" : "[ ]") + " " + description; + } + + /** + * Returns the format of the task to be saved into a file + * @return String representation of the task + */ + public String toSave() { + return (isDone ? "1" : "0") + " | " + description; + } +} diff --git a/src/main/java/TaskList.java b/src/main/java/TaskList.java new file mode 100644 index 000000000..848e39a8a --- /dev/null +++ b/src/main/java/TaskList.java @@ -0,0 +1,164 @@ +import java.util.ArrayList; + +/** + * A list of tasks. + */ +public class TaskList { + + private ArrayList tasks; + public TaskList(ArrayList tasks) { + this.tasks = tasks; + } + public TaskList() { + this.tasks = new ArrayList(); + } + + public ArrayList getTasks() { + return tasks; + } + + public void add(Task task) { + this.tasks.add(task); + } + public void remove(Task task){ + this.tasks.remove(task); + } + + /** + * This method attempts to add a Todo task into the TaskList. + * It will check whether the description of the task is empty before + * creating a ToDo object and add it into the TaskList. + * + * @param description Details regarding the todo task. + * @throws DukeException If the description of the todo task is empty. + */ + public void addTodo(String description) throws DukeException { + if (description.isBlank()) { // Check if description is empty + throw new DukeException("Description of a todo cannot be empty"); + } + tasks.add(new ToDo(description)); + + } + + /** + * This method attempts to add a deadline task into the TaskList. + * It will check whether the description of the task is empty before + * extracting the deadline of the task. It will then create a new Deadline + * object and add it into the TaskList. + * + * @param description Details regarding the deadline task. + * @throws DukeException If the description of the deadline task is empty + * or when the /by keyword is not provided. + */ + public void addDeadline(String description) throws DukeException { + try { + String[] deadlineTokens = description.split("/by"); + description = deadlineTokens[0].trim(); + String by = deadlineTokens[1].trim(); + if (description.isBlank() | by.isBlank()) { // Check if description is empty + throw new DukeException("Wrong format! Expected format: deadline DESCRIPTION /by TIME"); + } + Deadline deadline = new Deadline(description, by); + tasks.add(deadline); + } catch (ArrayIndexOutOfBoundsException e) { + throw new DukeException("Wrong format! Expected format: deadline DESCRIPTION /by TIME"); + } + + } + + /** + * This method attempts to add an event task to the TaskList. + * It will check whether the description of the task is empty before extracting + * the dates/time for from when the event start to when the event ends. It then + * creates a Event object and add it into the TaskList. + * + * @param description Details regarding the Event task. + * @throws DukeException If the description of the Event task is empty + * or when the /from and /to keywords are the provided. + */ + public void addEvent(String description) throws DukeException { + try { + String[] eventTokens = description.split("/from"); + description = eventTokens[0].trim(); + String[] fromAndToTokens = eventTokens[1].split("/to"); + String from = fromAndToTokens[0].trim(); + String to = fromAndToTokens[1].trim(); + if (description.isBlank() | from.isBlank() | to.isBlank()) { // Check if format is correct + throw new DukeException("Wrong format! Expected format: " + + "event DESCRIPTION /from START_TIME /to END_TIME"); + } + Event event = new Event(description, from, to); + tasks.add(event); + } catch (ArrayIndexOutOfBoundsException e) { + throw new DukeException("Wrong format! Expected format: event DESCRIPTION /from START_TIME /to END_TIME"); + } + + } + + /** + * This method attempts to mark a task in the TaskList + * It will check whether the task index provided by the user is valid before marking the task as + * the respective isDone argument. + * + * @param taskID Index of the task to be marked. + * @param isDone True to mark the task as done and False to mark the task as undone. + * @throws DukeException if task index is not found or task index in not an int. + */ + public void markTask(String taskID, boolean isDone) throws DukeException { + try { + int index = Integer.parseInt(taskID); + tasks.get(index-1).setDone(isDone); + } catch (NullPointerException | NumberFormatException e) { + throw new DukeException("Oops! An integer taskID is expected"); + } catch (IndexOutOfBoundsException e) { + throw new DukeException("Oops! Task " + taskID + " does not exist"); + } + } + + /** + * This method attempts to delete a task based on the task index from the TaskList + * It will check whether the task index provided by the user exist before deleting the task. + * + * @param taskID Index of task to be deleted. + * @return The removed task to be printed by Ui. + * @throws DukeException If task index is not an integer or task index does not exist. + */ + public Task deleteTask(String taskID) throws DukeException { + try { + int index = Integer.parseInt(taskID); + Task removedTask = tasks.get(index-1); + tasks.remove(index-1); + return removedTask; + } catch (NullPointerException | NumberFormatException e) { + throw new DukeException("Oops! An integer taskID is expected"); + } catch (IndexOutOfBoundsException e) { + throw new DukeException("Oops! Task " + taskID + " does not exist"); + } + } + + /** + * This method attempts to find the tasks with descriptions that matches the keyword provided + * It first check if the keyword provided is valid before going through the TaskList. For every + * task, it splits the description into substrings and compare word by word with the keyword. + * + * @param keyword Keyword to be searched for. + * @return A list of tasks that matches the keyword for Ui object to print. + * @throws DukeException If keyword is not a word or null. + */ + public ArrayList find(String keyword) throws DukeException { + if (keyword.isBlank()) { + throw new DukeException("Keyword cannot be empty"); + } + ArrayList matchingTasks = new ArrayList(); + for (Task task : tasks) { + String[] substrs = task.getDescription().split("\\s+"); + for (String substr : substrs) { + if (substr.equals(keyword)) { + matchingTasks.add(task); + break; + } + } + } + return matchingTasks; + } +} diff --git a/src/main/java/ToDo.java b/src/main/java/ToDo.java new file mode 100644 index 000000000..6bfbf268f --- /dev/null +++ b/src/main/java/ToDo.java @@ -0,0 +1,23 @@ +public class ToDo extends Task { + + public ToDo(String description) { + super(description); + } + + /** + * Returns the format of the task to be printed out to user + * @return String representation of the task + */ + public String toString() { + return "[T]" + super.toString(); + } + + /** + * Returns the format of the task to be saved into a file + * @return String representation of the task + */ + @Override + public String toSave() { + return "T | " + super.toSave(); + } +} diff --git a/src/main/java/Type.java b/src/main/java/Type.java new file mode 100644 index 000000000..39d0667be --- /dev/null +++ b/src/main/java/Type.java @@ -0,0 +1,3 @@ +public enum Type { + TODO, DEADLINE, EVENT +} diff --git a/src/main/java/Ui.java b/src/main/java/Ui.java new file mode 100644 index 000000000..8d331128e --- /dev/null +++ b/src/main/java/Ui.java @@ -0,0 +1,114 @@ +import java.util.ArrayList; +import java.util.Scanner; + +/** + * User interface object that handles the format and content of various outputs to the user + * through command line interface. + */ +public class Ui { + private static final String LINE_DIVIDER = "\t---------------------------------------"; + protected Scanner scanner; + + public Ui(){ + this.scanner = new Scanner(System.in); + } + + public void showLoadingFile() { + showLine(); + System.out.println("\tLoading file......"); + showLine(); + } + + public void showFileLoaded() { + showLine(); + System.out.println("\tFile has been successfully loaded"); + showLine(); + } + public void showStartMessage() { + showLine(); + String intro = "\tHello! I'm Wenny!\n\tHow may I help you?"; + System.out.println(intro); + showLine(); + } + + public void showByeMessage() { + showLine(); + System.out.println("\tBye. Hope to see you again soon!"); + showLine(); + } + public void showTasks(TaskList tasklist) { + ArrayList tasks = tasklist.getTasks(); + showLine(); + if (tasks.isEmpty()) { + System.out.println("\tYou do not have any task in the list."); + } else { + System.out.println("\tHere are the tasks in your list:"); + for (int i = 0; i < tasks.size(); i++) { + System.out.print("\t"); + System.out.print(i + 1 + "."); + System.out.println(tasks.get(i)); + } + } + showLine(); + } + + public void showAddTask(TaskList taskList) { + ArrayList tasks = taskList.getTasks(); + showLine(); + System.out.println("\tGot it. I've added this task:"); + System.out.println("\t" + tasks.get(tasks.size()-1)); + System.out.println("\tNow you have " + tasks.size() + " task(s) in the list."); + showLine(); + } + + public void showDeleteTask(TaskList taskList, Task removedTask) { + ArrayList tasks = taskList.getTasks(); + showLine(); + System.out.println("\tNoted. I've removed this task:"); + System.out.println("\t" + removedTask); + System.out.println("\tNow you have " + tasks.size() + " task(s) in the list."); + showLine(); + } + + public void showMark(TaskList taskList, String taskID, boolean isDone) { + ArrayList tasks = taskList.getTasks(); + int index = Integer.parseInt(taskID); + showLine(); + if (isDone) { + System.out.println("\tNice! I've marked this task as done:"); + } else { + System.out.println("\tNice! I've marked this task as undone:"); + } + System.out.println("\t" + tasks.get(index-1)); + showLine(); + } + + public void showLine() { + System.out.println(LINE_DIVIDER); + } + + public void showError(Exception e) { + showLine(); + System.out.println("\t" + e); + showLine(); + } + + public String readCommand() { + return scanner.nextLine(); + } + + public void showFind(ArrayList matchingTasks) { + showLine(); + if (matchingTasks.isEmpty()) { + System.out.println("\tKeyword does not match any task in the list."); + } else { + System.out.println("\tHere are the matching tasks in your list:"); + for (int i = 0; i < matchingTasks.size(); i++) { + System.out.print("\t"); + System.out.print(i + 1 + "."); + System.out.println(matchingTasks.get(i)); + } + } + showLine(); + } +}