diff --git a/docs/README.md b/docs/README.md index 8077118eb..5e1bac1d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,69 @@ -# User Guide +# KevBot Guide ## Features -### Feature-ABC +### List -Description of the feature. +List all outstanding todos, deadlines, and events. -### Feature-XYZ +### Find -Description of the feature. +Filter the listed tasks by a specific keyword(s). -## Usage +### Mark + +Mark a task as completed/incomplete. + +### Delete -### `Keyword` - Describe action +Delete a specific task. -Describe the action and its outcome. +### Add + +Add a new todo, deadline, or event task. + +## Usage + +### `list` - List all tasks Example of usage: -`keyword (optional arguments)` +`list` + +### `find` - Match tasks by keyword + +Example of usage: + +`find (keyword)` + +### `mark` - Mark a task +Marks an undone task as done and vice versa. Task index is 1-based as displayed in the list command. + +Example of usage: + +`mark (task index)` + +### `delete` - Remove a task +Task index is 1-based as displayed in the list command. + +Example of usage: + +`delete (task index)` + +### `todo` - Add a todo + +Example of usage: + +`todo (description)` + +### `deadline` - Add a deadline + +Example of usage: + +`deadline (description) /by (end time)` -Expected outcome: +### `event` - Add an event -Description of the outcome. +Example of usage: -``` -expected output -``` +`event (description) /from (start time) /to (end time)` \ No newline at end of file diff --git a/duke.txt b/duke.txt new file mode 100644 index 000000000..8b865ac94 --- /dev/null +++ b/duke.txt @@ -0,0 +1,6 @@ +T|false|asdfkasdh +T|true|re98ui +D|true|return book|tmrw +E|false|proj meeting|today|forever +T|false|hu +T|true|deez 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..6e864153e --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: duke.Duke + diff --git a/src/main/java/duke/Duke.java b/src/main/java/duke/Duke.java new file mode 100644 index 000000000..46d6919e2 --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,59 @@ +package duke; + +import duke.command.DukeException; +import duke.parser.Parser; +import duke.storage.StorageFile; +import duke.tasklist.TaskList; +import duke.ui.TextUi; + +public class Duke { + private TextUi ui; + private Parser parser; + private TaskList tasks; + private StorageFile storage; + + public Duke(String filePath) { + tasks = new TaskList(); + ui = new TextUi(); + parser = new Parser(); + storage = new StorageFile(filePath); + + try { + tasks = new TaskList(storage.load()); + } catch (DukeException e) { + ui.showInitFailedMessage(); + tasks = new TaskList(); + } + } + + public void run() { + ui.showWelcomeMessage(); + + String userCommandText = ui.getUserCommand(); + while (!userCommandText.equals("bye")) { + String result = handleCommand(userCommandText); + ui.showResultToUser(result); + userCommandText = ui.getUserCommand(); + } + + ui.showGoodbyeMessage(); + } + + public String handleCommand(String userInput) { + if (userInput.equals("list")) { + return tasks.getIndexedTasks(); + } else { + try { + String result = parser.executeCommand(userInput, tasks); + storage.saveTasks(tasks); + return result; + } catch (Exception e) { + return e.getMessage(); + } + } + } + + public static void main(String[] args) { + new Duke("duke.txt").run(); + } +} diff --git a/src/main/java/duke/command/DukeException.java b/src/main/java/duke/command/DukeException.java new file mode 100644 index 000000000..2ef6e369c --- /dev/null +++ b/src/main/java/duke/command/DukeException.java @@ -0,0 +1,10 @@ +package duke.command; + +/** + * Wrapper for an exception throw within Duke + */ +public class DukeException extends Exception { + public DukeException(String message) { + super(message); + } +} diff --git a/src/main/java/duke/parser/Parser.java b/src/main/java/duke/parser/Parser.java new file mode 100644 index 000000000..0438f0b05 --- /dev/null +++ b/src/main/java/duke/parser/Parser.java @@ -0,0 +1,108 @@ +package duke.parser; + +import duke.command.DukeException; +import duke.task.Deadline; +import duke.task.Event; +import duke.task.Todo; +import duke.tasklist.TaskList; + +import java.util.HashMap; + +/** + * A Parser object is responsible for parsing and executing + * a user provided command string. + */ +public class Parser { + public Parser() {} + + /** + * Returns a human-readable result of the command + * executed. + * + * @param line inputted by user. + * @param tasks that are currently in the TaskList + * @return A human-readable string of the command's result. + * @throws DukeException If command has issues. + */ + public String executeCommand(String line, TaskList tasks) throws DukeException { + int divider = line.indexOf(" "); + if (divider == -1) { + throw new DukeException("Sorry! Not sure what you mean"); + } + if (divider == line.length() - 1) { + throw new DukeException("Please enter a non-empty parameter value"); + } + + + if (line.startsWith("todo")){ + String description = line.substring(divider + 1); + + return tasks.addTask(new Todo(description)); + } else if (line.startsWith("find")) { + String keyword = line.substring(divider + 1); + + return tasks.getIndexedTasksByKeyword(keyword); + } + + if (line.contains("mark") || line.startsWith("delete")) { + int idx = Integer.parseInt(line.substring(divider + 1)) - 1; + if (idx < 0 || idx >= tasks.size()) { + throw new DukeException("Sorry! That's not a valid task"); + } + + if (line.contains("mark")) { + return tasks.markTask(idx, line.startsWith("mark")); + } else { + return tasks.removeTask(idx); + } + } + + HashMap parameters = parseParameters(line); + String description = parameters.get("description"); + if (description == null) { + throw new DukeException("Sorry! Please provide a valid description"); + } + if (line.startsWith("deadline")) { + String by = parameters.get("by"); + if (by == null) { + throw new DukeException("Sorry! Please provide a valid `by`"); + } + return tasks.addTask(new Deadline(description, by)); + } else if (line.startsWith("event")) { + String from = parameters.get("from"); + String to = parameters.get("to"); + if (from == null || to == null) { + throw new DukeException("Sorry! Please provide a valid `from` and/or `to`"); + } + return tasks.addTask(new Event(description, from, to)); + } else { + throw new DukeException("Sorry! Please enter a valid command"); + } + } + + private static HashMap parseParameters(String line) throws DukeException { + HashMap fieldToValue = new HashMap<>(); + + int startDescription = line.indexOf(" "); + int endOfDescription = line.indexOf(" /"); + if (startDescription == endOfDescription || startDescription == -1 || endOfDescription == -1) { + throw new DukeException("Sorry! Not sure what you mean"); + } + fieldToValue.put("description", line.substring(startDescription + 1, endOfDescription)); + + String[] splitParams = line.split(" /"); + for (int i = 1; i < splitParams.length; i++) { + String rawParam = splitParams[i]; + int divider = rawParam.indexOf(" "); + if (divider == -1) { + throw new DukeException("Sorry! Please enter valid inputs"); + } + + String field = rawParam.substring(0, divider); + String value = rawParam.substring(divider + 1); + fieldToValue.put(field, value); + } + + return fieldToValue; + } +} diff --git a/src/main/java/duke/storage/StorageFile.java b/src/main/java/duke/storage/StorageFile.java new file mode 100644 index 000000000..b5b2883c6 --- /dev/null +++ b/src/main/java/duke/storage/StorageFile.java @@ -0,0 +1,93 @@ +package duke.storage; + +import duke.command.DukeException; +import duke.task.Deadline; +import duke.task.Event; +import duke.task.Task; +import duke.task.Todo; +import duke.tasklist.TaskList; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * Handles Duke's storage capabilities. A StorageFile object handles the + * loading and saving aspects of new tasks. + */ +public class StorageFile { + String filePath; + public StorageFile(String filePath) { + this.filePath = filePath; + } + + /** + * Loads all tasks from the .txt file + * when the program is first ran. + * + * @return List of Task objects representing saved tasks. + * @throws DukeException If there is an issue loading tasks. + */ + public List load() throws DukeException { + List tasks = new ArrayList<>(); + File f = new File(filePath); + + try { + f.createNewFile(); + Scanner s = new Scanner(f); + while (s.hasNext()) { + String line = s.nextLine(); + String[] parsed = line.split("\\|"); + String taskType = parsed[0]; + String description = parsed[2]; + + switch (taskType) { + case "T": + tasks.add(new Todo(description)); + break; + case "D": + String by = parsed[3]; + tasks.add(new Deadline(description, by)); + break; + case "E": + String from = parsed[3]; + String to = parsed[4]; + tasks.add(new Event(description, from, to)); + break; + default: + throw new DukeException("Unknown task type detected"); + } + + if (parsed[1].equals("true")) { + tasks.get(tasks.size() - 1).setStatus(true); + } + } + } catch (IOException e) { + throw new DukeException(e.getMessage()); + } + + return tasks; + } + + /** + * Saves all tasks to the .txt file after + * a command that modifies the TaskList + * + * @param tasks to be written to the file + * @throws DukeException If there is an error writing. + */ + public void saveTasks(TaskList tasks) throws DukeException { + File f = new File(filePath); + + try { + FileWriter fw = new FileWriter(f); + fw.write(tasks.getSerializedTasks()); + fw.close(); + } catch (IOException e) { + throw new DukeException(e.getMessage()); + } + } +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 000000000..b9e24ebb7 --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,22 @@ +package duke.task; + +/** + * Type of task that represents a deadline with end time. + */ +public class Deadline extends Task { + private String by; + public Deadline(String description, String by) { + super(description); + this.by = by; + } + + @Override + public String getFormattedTask() { + return "[D] " + super.getFormattedTask() + " (by: " + by + ")"; + } + + @Override + public String getSerializedString() { + return "D|" + super.getSerializedString() + "|" + by; + } +} diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 000000000..c587b7fbc --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,24 @@ +package duke.task; + +/** + * Type of task that represents an event with start/end times. + */ +public class Event extends Task { + private String from; + private String to; + public Event(String description, String from, String to) { + super(description); + this.from = from; + this.to = to; + } + + @Override + public String getFormattedTask() { + return "[E] " + super.getFormattedTask() + " (from: " + from + " to: " + to + ")"; + } + + @Override + public String getSerializedString() { + return "E|" + super.getSerializedString() + "|" + from + "|" + to; + } +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 000000000..3b8e54c05 --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,54 @@ +package duke.task; + +/** + * Generic task class. Contains different methods to format a task. + */ +public class Task { + protected String description; + protected boolean isDone; + + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Returns a formatted task string for printing purposes. + * + * @return String of formatted task. + */ + public String getFormattedTask() { + return "[" + getStatusIcon() + "] " + description; + } + private String getStatusIcon() { + return (isDone ? "X" : " "); // mark done task with X + } + + /** + * Setter to allow isDone status to be modified + * + * @param isDone boolean of new status + */ + public void setStatus(boolean isDone) { + this.isDone = isDone; + } + + /** + * Returns a string of the task serialized to be saved + * in the text file. + * + * @return Serialized string + */ + public String getSerializedString() { + return isDone + "|" + description; + } + + /** + * Returns description of the task. + * + * @return Task description. + */ + public String getDescription() { + return description; + } +} diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java new file mode 100644 index 000000000..a12dcda86 --- /dev/null +++ b/src/main/java/duke/task/Todo.java @@ -0,0 +1,20 @@ +package duke.task; + +/** + * Type of task that represents a to-do object. + */ +public class Todo extends Task { + public Todo(String description) { + super(description); + } + + @Override + public String getFormattedTask() { + return "[T] " + super.getFormattedTask(); + } + + @Override + public String getSerializedString() { + return "T|" + super.getSerializedString(); + } +} diff --git a/src/main/java/duke/tasklist/TaskList.java b/src/main/java/duke/tasklist/TaskList.java new file mode 100644 index 000000000..5f947b59f --- /dev/null +++ b/src/main/java/duke/tasklist/TaskList.java @@ -0,0 +1,131 @@ +package duke.tasklist; + +import duke.task.Task; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper around the List to allow for controlled access and formatting methods. + */ +public class TaskList { + private List tasks = new ArrayList<>(); + public TaskList() {} + + public TaskList(List tasks) { + this.tasks = tasks; + } + + /** + * Marks specific task as either done or undone + * and returns the status of the marking. + * + * @param index of task to be marked. + * @param isDone boolean representing status of the task. + * @return Status of marking as string. + */ + public String markTask(int index, boolean isDone) { + final StringBuilder formatted = new StringBuilder(); + tasks.get(index).setStatus(isDone); + + if (isDone) { + formatted.append("Nice! I've marked this task as done:").append("\n"); + } else { + formatted.append("OK, I've marked this task as not done yet:").append("\n"); + } + formatted.append(tasks.get(index).getFormattedTask()); + + return formatted.toString(); + } + + + /** + * Adds a new task to the TaskList and returns + * the status of the addition. + * + * @param task to be added + * @return Status of the addition. + */ + public String addTask(Task task) { + tasks.add(task); + + return "Got it. I've added this task:\n" + task.getFormattedTask() + "\nNow you have " + tasks.size() + " tasks in the list."; + } + + /** + * Removes a task from the TaskList by index and + * returns the status of the removal. + * + * @param idx of task to be removed + * @return Status of the removal + */ + public String removeTask(int idx) { + Task removedTask = tasks.remove(idx); + + return "Got it. I've removed this task:\n" + removedTask.getFormattedTask() + "\nNow you have " + tasks.size() + " tasks in the list."; + } + + /** + * Returns a task object by index. + * + * @param idx Index of task to return. + * @return task object + */ + public Task get(int idx) { + return tasks.get(idx); + } + + /** + * Returns a formatted version of all tasks + * in TaskList with 1-based indexing. + * + * @return String formatted list + */ + public String getIndexedTasks() { + StringBuilder formatted = new StringBuilder(); + for (int i = 0; i < tasks.size() && tasks.get(i) != null; i++) { + formatted.append(i + 1).append(". ").append(tasks.get(i).getFormattedTask()).append("\n"); + } + + return formatted.toString(); + } + + public int size() { + return tasks.size(); + } + + /** + * Returns a serialized version of all tasks + * in TaskList for saving into the .txt file. + * + * @return String serialized list + */ + public String getSerializedTasks() { + StringBuilder formatted = new StringBuilder(); + for (Task task : tasks) { + formatted.append(task.getSerializedString()).append("\n"); + } + + return formatted.toString(); + } + + /** + * Returns a formatted version of all tasks + * in TaskList with 1-based indexing that match + * the specified keyword parameter. + * + * @param keyword string to match against description of tasks + * @return String formatted list + */ + public String getIndexedTasksByKeyword(String keyword) { + int idx = 1; + StringBuilder formatted = new StringBuilder(); + for (Task task : tasks) { + if (task.getDescription().contains(keyword)) { + formatted.append(idx++).append(". ").append(task.getFormattedTask()).append("\n"); + } + } + + return formatted.toString(); + } +} diff --git a/src/main/java/duke/ui/TextUi.java b/src/main/java/duke/ui/TextUi.java new file mode 100644 index 000000000..4f07c625d --- /dev/null +++ b/src/main/java/duke/ui/TextUi.java @@ -0,0 +1,63 @@ +package duke.ui; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Scanner; + +/** + * TextUi object is responsible for all messages displayed to the user. + * All formatting is specified and followed in this class. + */ +public class TextUi { + public static final String MESSAGE_WELCOME = "Hello! I'm KevBot"; + private static final String MESSAGE_GOODBYE = "Bye. Hope to see you again soon!"; + public static final String MESSAGE_INIT_FAILED = "Failed to initialise address book application. Exiting..."; + + /** A decorative prefix added to the beginning of lines printed by AddressBook */ + private static final String LINE_PREFIX = "|| "; + + /** A platform independent line separator. */ + private static final String LS = System.lineSeparator(); + + private static final String DIVIDER = "==================================================="; + + private final Scanner in; + private final PrintStream out; + public TextUi() { + this(System.in, System.out); + } + + public TextUi(InputStream in, PrintStream out) { + this.in = new Scanner(in); + this.out = out; + } + + public void showWelcomeMessage() { + showToUser( + DIVIDER, + DIVIDER, + MESSAGE_WELCOME, + DIVIDER); + } + + public void showGoodbyeMessage() { + showToUser(MESSAGE_GOODBYE, DIVIDER, DIVIDER); + } + + public void showResultToUser(String result) { + showToUser(result, DIVIDER); + } + + public String getUserCommand() { + return in.nextLine(); + } + + private void showToUser(String... message) { + for (String m : message) { + out.println(LINE_PREFIX + m.replace("\n", LS + LINE_PREFIX)); + } + } + + public void showInitFailedMessage() { + showToUser(MESSAGE_INIT_FAILED, DIVIDER, DIVIDER); + } +}