diff --git a/README.md b/README.md index af0309a9e..bc86519ed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# Elyk project template This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. @@ -13,7 +13,7 @@ Prerequisites: JDK 17, update Intellij to the most recent version. 1. If there are any further prompts, accept the defaults. 1. Configure the project to use **JDK 17** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
In the same dialog, set the **Project language level** field to the `SDK default` option. -1. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: +1. After that, locate the `src/main/java/Elyk.java` file, right-click it, and choose `Run Elyk.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: ``` Hello from ____ _ diff --git a/docs/README.md b/docs/README.md index 47b9f984f..ea1b8647d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,160 @@ -# Duke User Guide +# Elyk User Guide -// Update the title above to match the actual product name +Elyk is a Personal Assistant Chatbot that acts as a task manager to +helps users keep track of their tasks via a command line interface (CLI). -// Product screenshot goes here +## Adding ToDos -// Product intro goes here +This feature allows users to add and track tasks that are without any date/time +attached to it, e.g., visit a museum, which are represented by the symbol "T". -## Adding deadlines +Input format: `todo ` -// Describe the action and its outcome. +Example: `todo visit a museum` -// Give examples of usage +Expected output: -Example: `keyword (optional arguments)` +``` +Got it. I've added this task: + [T][ ] visit a museum +Now you have 5 tasks in the list. +``` + +## Adding Deadlines + +This feature allows users to add and track tasks that need to be done before a +specific date/time, e.g., finish proposal by 04/06/2025 9pm, which are represented +by the symbol "D". -// A description of the expected outcome goes here +Input format: `deadline /by ` + +Example: `deadline finish proposal /by 04/06/2025 9pm` + +Expected output: ``` -expected output +Got it. I've added this task: + [D][ ] finish proposal (by: 04/06/2025 9pm) +Now you have 3 tasks in the list. ``` -## Feature ABC +## Adding Events + +This feature allows users to add and track tasks that start at a specific date/time +and ends at a specific date/time, e.g., basketball training 3-6pm, which are represented +by the symbol "E". + +Input format: `event /from /to ` + +Example: `event basketball training /from 3pm /to 6pm` + +Expected output: + +``` +Got it. I've added this task: + [E][ ] basketball training (from: 3pm to: 6pm) +Now you have 7 tasks in the list. +``` + +## Listing tasks + +This feature allows users to view all the tasks that have been stored by Elyk. + +Input format: `list` + +Example: `list` + +Expected output: + +``` +Here are the tasks in your list: +1.[T][X] visit a museum +2.[D][ ] finish proposal (by: 04/06/2025 9pm) +3.[E][ ] basketball training (from: 3pm to: 6pm) +4.[T][X] eat dinner +5.[T][ ] fitness exercises +``` + +## Marking tasks as Done + +This feature allows users to mark tasks as done, which are represented +by the symbol "X". + +Input format: `mark ` + +Example: `mark 3` + +Expected output: + +``` +Nice! I've marked this task as done: + [E][X] basketball training (from: 3pm to: 6pm) +``` -// Feature details +## Marking tasks as Not Done +This feature allows users to mark tasks as done, which are represented +by the symbol " ". -## Feature XYZ +Input format: `unmark ` + +Example: `unmark 1` + +Expected output: + +``` +OK, I've marked this task as not done yet: + [T][ ] visit a museum +``` + +## Deleting tasks + +This feature allows users to delete tasks that have been stored by Elyk. + +Input format: `delete ` + +Example: `delete 2` + +Expected output: + +``` +Noted. I've removed this task: + [D][ ] finish proposal (by: 04/06/2025 9pm) +Now you have 4 tasks in the list. +``` + +## Finding tasks + +This feature allows users to search for tasks that contain a certain keyword +in the description. + +Input format: `find ` + +Example: `find run` + +Expected output: + +``` +Here are the matching tasks in your list: +1.[T][X] run 10k +2.[T][ ] run 5k +3.[T][ ] charity run +4.[D][ ] slow run (by: 5pm) +5.[E][X] running exercise (from: 6pm to: 10pm) +``` + +## Exiting the program + +This feature allows users to say bye to Elyk and exit the chatbot program. + +Input format: `bye` + +Example: `bye` + +Expected output: + +``` +Bye. Hope to see you again soon! -// Feature details \ No newline at end of file +Process finished with exit code 0 +``` \ No newline at end of file diff --git a/src/main/java/Deadline.java b/src/main/java/Deadline.java new file mode 100644 index 000000000..1593fe5eb --- /dev/null +++ b/src/main/java/Deadline.java @@ -0,0 +1,28 @@ +/** + * A child class of Task class that contains the description and deadline of the tasks + */ +public class Deadline extends Task { + + protected String by; + + /** + * Constructor of Deadline class + * + * @param description descriptionof the task + * @param by deadline of the task + */ + public Deadline(String description, String by) { + super(description); + this.by = by; + } + + /** + * Converts the task to printable String argument + * + * @return a String representing the task + */ + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by + ")"; + } +} 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/Elyk.java b/src/main/java/Elyk.java new file mode 100644 index 000000000..8a002a005 --- /dev/null +++ b/src/main/java/Elyk.java @@ -0,0 +1,111 @@ +/** + * The main class of task manager: Elyk, which controls all the executions of the main program by + * utilising the functionality of all the other classes. + */ +public class Elyk { + public static int taskNum = 0; + public static String input = ""; + public static String description = ""; + public static String from = ""; + public static String to = ""; + public static String by = ""; + public static String keyword = ""; + private static Storage storage; + private static TaskList taskList; + private static Ui ui; + + /** + * Constructor of Elyk class which initialises a Storage, TaskList and Ui + * + * @param elykFile path of the file used to load and save tasks + */ + public Elyk(String elykFile) { + ui = new Ui(); + storage = new Storage(elykFile); + taskList = storage.loadTasks(); + } + + public static void main(String[] args) { + new Elyk("data/Elyk.txt").run(); + } + + /** + * Runs all the features that the task manager Elyk contains + */ + public void run() { + ui.greet(); + + while (true) { + try { + input = ui.getNextCommand(); + String commandType = Parser.updateCommand(input); + switch (commandType) { + case "bye": + ui.sayBye(); + System.exit(0); + case "list": + ui.printTasks(); + for (int i = 0; i < taskList.size(); i++) { + ui.printIndividualTask(taskList.getTask(i), i); + } + break; + case "mark": + taskNum = Parser.getTaskNum(input); + Task doneTask = taskList.getTask(taskNum - 1); + doneTask.markAsDone(); + ui.markTaskDone(doneTask); + storage.saveTasks(taskList); + break; + case "unmark": + taskNum = Parser.getTaskNum(input); + Task notDoneTask = taskList.getTask(taskNum - 1); + notDoneTask.markAsNotDone(); + ui.markTaskNotDone(notDoneTask); + storage.saveTasks(taskList); + break; + case "delete": + taskNum = Parser.getTaskNum(input); + Task deletedTask = taskList.removeTask(taskNum - 1); + ui.deleteTask(deletedTask, taskList.size()); + storage.saveTasks(taskList); + break; + case "todo": + description = Parser.getDescription(input, "todo"); + Task addedTodo = taskList.addTask(new Todo(description)); + ui.inputTask(addedTodo, taskList.size()); + storage.saveTasks(taskList); + break; + case "deadline": + description = Parser.getDescription(input, "deadline"); + by = Parser.getBy(input); + Task addedDeadline = taskList.addTask(new Deadline(description, by)); + ui.inputTask(addedDeadline, taskList.size()); + storage.saveTasks(taskList); + break; + case "event": + description = Parser.getDescription(input, "event"); + from = Parser.getFrom(input); + to = Parser.getTo(input); + Task addedEvent = taskList.addTask(new Event(description, from, to)); + ui.inputTask(addedEvent, taskList.size()); + storage.saveTasks(taskList); + break; + case "find": + keyword = Parser.getKeyword(input); + TaskList foundTasks = taskList.findMatchingTasks(keyword); + ui.printMatchingTasks(); + for (int i = 0; i < foundTasks.size(); i++) { + ui.printIndividualTask(foundTasks.getTask(i), i); + } + break; + default: + throw new ElykException(); + } + } catch (ElykException e) { + ui.printErrorMessage(" Sorry :( I currently does not support this command, please try again."); + } catch (IndexOutOfBoundsException e) { + ui.printErrorMessage(" Hmm... There might be some missing information in your command..."); + } + } + } +} diff --git a/src/main/java/ElykException.java b/src/main/java/ElykException.java new file mode 100644 index 000000000..7b58dffdc --- /dev/null +++ b/src/main/java/ElykException.java @@ -0,0 +1,3 @@ +public class ElykException extends Exception { + +} diff --git a/src/main/java/Event.java b/src/main/java/Event.java new file mode 100644 index 000000000..da4696a20 --- /dev/null +++ b/src/main/java/Event.java @@ -0,0 +1,31 @@ +/** + * A child class of Task class that contains the description, start and end time of the tasks + */ +public class Event extends Task { + + protected String from; + protected String to; + + /** + * Constructor of Event class + * + * @param description description of the task + * @param from start time of the task + * @param to end time of the task + */ + public Event(String description, String from, String to) { + super(description); + this.from = from; + this.to = to; + } + + /** + * Converts the task to printable String argument + * + * @return a String representing the task + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + from + " to: " + to + ")"; + } +} \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 000000000..1f3718822 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: Elyk + diff --git a/src/main/java/Parser.java b/src/main/java/Parser.java new file mode 100644 index 000000000..f478de617 --- /dev/null +++ b/src/main/java/Parser.java @@ -0,0 +1,105 @@ +/** + * Deals with making sense of the user command, i.e. converting it to actions, task no., task description, etc. + */ +public class Parser { + /** + * Convert user's input to an action command + * + * @param input line of input from user + * @return command type to be executed + */ + public static String updateCommand(String input) { + if (input.equals("bye")) { + return "bye"; + } else if (input.equals("list")) { + return "list"; + } else if (input.startsWith("mark")) { + return "mark"; + } else if (input.startsWith("unmark")) { + return "unmark"; + } else if (input.startsWith("delete")) { + return "delete"; + } else if (input.startsWith("todo")) { + return "todo"; + } else if (input.startsWith("deadline")) { + return "deadline"; + } else if (input.startsWith("event")) { + return "event"; + } else if (input.startsWith("find")) { + return "find"; + } else { + return "default"; + } + } + + /** + * Convert user's input to a specific task no. + * + * @param input line of input from user + * @return task no. of the task user is referring to in the input + */ + public static int getTaskNum(String input) { + String[] words = input.split(" "); + return Integer.parseInt(words[1]); + } + + /** + * Convert user's input to a specific task description + * + * @param input line of input from user + * @param commandType command type to be executed + * @return description of the task user is referring to in the input + */ + public static String getDescription(String input, String commandType) { + return switch (commandType) { + case "todo" -> input.substring(5); + case "deadline" -> { + int byPos = input.indexOf("/by"); + yield input.substring(9, byPos - 1); + } + case "event" -> { + int fromPos = input.indexOf("/from"); + yield input.substring(6, fromPos - 1); + } + default -> ""; + }; + } + + /** + * Convert user's input to a specific deadline + * + * @param input line of input from user + * @return deadline of the task user is referring to in the input + */ + public static String getBy(String input) { + int byPos = input.indexOf("/by"); + return input.substring(byPos + 4); + } + + /** + * Convert user's input to a specific start time of an event + * + * @param input line of input from user + * @return start time of the event user is referring to in the input + */ + public static String getFrom(String input) { + int fromPos = input.indexOf("/from"); + int toPos = input.indexOf("/to"); + return input.substring(fromPos + 6, toPos - 1); + } + + /** + * Convert user's input to a specific end time of an event + * + * @param input line of input from user + * @return end time of the event user is referring to in the input + */ + public static String getTo(String input) { + int toPos = input.indexOf("/to"); + return input.substring(toPos + 4); + } + + public static String getKeyword(String input) { + return input.substring(5); + } +} diff --git a/src/main/java/Storage.java b/src/main/java/Storage.java new file mode 100644 index 000000000..38fe8521d --- /dev/null +++ b/src/main/java/Storage.java @@ -0,0 +1,141 @@ +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Scanner; + +/** + * Deals with loading tasks from the file and saving tasks in the file, each with + * a helper function to deal with formatting + */ +public class Storage { + private static String dataFolder; + private static String dataFile; + + /** + * Constructor of Storage that extracts both the folder and file paths + * + * @param elykFile path of the file used to load and save tasks + */ + public Storage(String elykFile) { + dataFolder = elykFile.substring(0, elykFile.indexOf("/")); + dataFile = elykFile; + } + + /** + * Loads tasks from the file into a TaskList object + * + * @return a TaskList object containing all tasks loaded from the file + */ + public TaskList loadTasks() { + TaskList tasks = new TaskList(); + + try { + File folder = new File(dataFolder); + if (!folder.exists()) { + folder.mkdir(); + } + + File file = new File(dataFile); + if (!file.exists()) { + file.createNewFile(); + return tasks; + } + + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (!line.isEmpty()) { + Task task = parseTaskFromString(line); + if (task != null) { + tasks.addTask(task); + } + } + } + scanner.close(); + + } catch (IOException e) { + System.out.println("There is an error while loading data :(" + e.getMessage()); + } + + return tasks; + } + + /** + * Saves tasks into the file from a TaskList object + * + * @param tasks a TaskList object whose tasks are to be saved into the file + */ + public void saveTasks(TaskList tasks) { + try { + FileWriter writer = new FileWriter(dataFile); + for (Task task : tasks.getTaskList()) { + writer.write(convertTaskToString(task) + "\n"); + } + writer.close(); + + } catch (IOException e) { + System.out.println("There is an error while saving data :(" + e.getMessage()); + } + } + + /** + * Converts a task into a storable String type argument + * + * @param task the task to be saved + * @return a String representing the task + */ + private static String convertTaskToString(Task task) { + StringBuilder stringBuilder = new StringBuilder(); + + if (task instanceof Todo) { + stringBuilder.append("T"); + } else if (task instanceof Deadline) { + stringBuilder.append("D"); + } else if (task instanceof Event) { + stringBuilder.append("E"); + } + + stringBuilder.append(" | ").append(task.isDone ? "1" : "0"); + stringBuilder.append(" | ").append(task.description); + + if (task instanceof Deadline) { + stringBuilder.append(" | ").append(((Deadline) task).by); + } else if (task instanceof Event) { + stringBuilder.append(" | ").append(((Event) task).from); + stringBuilder.append(" | ").append(((Event) task).to); + } + + return stringBuilder.toString(); + } + + /** + * Parses a task from the string stored in the file + * + * @param line a String representing the task to be loaded + * @return a task to be loaded + */ + private static Task parseTaskFromString(String line) { + String[] parts = line.split(" \\| "); + + if (parts.length < 3) { + return null; + } + + String type = parts[0]; + boolean isDone = parts[1].equals("1"); + String description = parts[2]; + + Task task = switch (type) { + case "T" -> new Todo(description); + case "D" -> new Deadline(description, parts[3]); + case "E" -> new Event(description, parts[3], parts[4]); + default -> null; + }; + + if (task != null && isDone) { + task.markAsDone(); + } + + return task; + } +} diff --git a/src/main/java/Task.java b/src/main/java/Task.java new file mode 100644 index 000000000..e7857646b --- /dev/null +++ b/src/main/java/Task.java @@ -0,0 +1,50 @@ +/** + * An abstract class that provides a basic template of a Task object + */ +public abstract class Task { + protected String description; + protected boolean isDone; + + /** + * Constructor of Task class + * + * @param description description of the task + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Convert the task's status of marked/not marked into something printable + * + * @return a symbol that represents whether a task is marked or not + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); + } + + /** + * Mark a task as done + */ + public void markAsDone() { + isDone = true; + } + + /** + * Mark a task as not done + */ + public void markAsNotDone() { + isDone = false; + } + + /** + * Converts the task to printable String argument + * + * @return a String representing the task + */ + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + description; + } +} diff --git a/src/main/java/TaskList.java b/src/main/java/TaskList.java new file mode 100644 index 000000000..eabb3cbca --- /dev/null +++ b/src/main/java/TaskList.java @@ -0,0 +1,74 @@ +import java.util.ArrayList; + +/** + * Contains the task list represented by "tasks" and operations related to it, e.g. to add/delete task in the list + */ +public class TaskList { + private ArrayList tasks; + + /** + * Default constructor of a Task List object + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Adds a specific task to the task list + * + * @param task task added to the task list + * @return the task added to the task list + */ + public Task addTask(Task task) { + tasks.add(task); + return task; + } + + /** + * Removes a specific task from the task list + * + * @param index index of the task removed from the task list + * @return the task removed from the task list + */ + public Task removeTask(int index) { + return tasks.remove(index); + } + + /** + * Gets a specific task from the task list + * + * @param index index of the task obtained from the taskList + * @return the task obtained from the taskList + */ + public Task getTask(int index) { + return tasks.get(index); + } + + /** + * Gets the size of the task list + * + * @return size of the task list + */ + public int size() { + return tasks.size(); + } + + /** + * Gets a task list in the form of ArrayList + * + * @return an ArrayList object representing the full task list + */ + public ArrayList getTaskList() { + return tasks; + } + + public TaskList findMatchingTasks(String keyword) { + TaskList matchingTasks = new TaskList(); + for (Task task : tasks) { + if (task.description.contains(keyword)) { + matchingTasks.addTask(task); + } + } + return matchingTasks; + } +} \ No newline at end of file diff --git a/src/main/java/Todo.java b/src/main/java/Todo.java new file mode 100644 index 000000000..b2817c3b1 --- /dev/null +++ b/src/main/java/Todo.java @@ -0,0 +1,23 @@ +/** + * A child class of Task class that only contains the description of the tasks + */ +public class Todo extends Task { + /** + * Constructor of Todo class + * + * @param description description of the task + */ + public Todo(String description) { + super(description); + } + + /** + * Converts the task to printable String argument + * + * @return a String representing the task + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/Ui.java b/src/main/java/Ui.java new file mode 100644 index 000000000..a4444bcf1 --- /dev/null +++ b/src/main/java/Ui.java @@ -0,0 +1,118 @@ +import java.util.Scanner; + +/** + * Deals with all the interactions with the user, mainly getting the input from user and output messages + * corresponding to the command requested by user. + */ +public class Ui { + private Scanner scanner; + + /** + * Constructor of Ui class that adds a Scanner to receive user's input + */ + public Ui() { + scanner = new Scanner(System.in); + } + + /** + * Reads the next line of input from user + * + * @return the next command + */ + public String getNextCommand() { + return scanner.nextLine(); + } + + /** + * Prints an error message according to a certain error + * + * @param errorMessage the error message intended to be printed + */ + public void printErrorMessage(String errorMessage) { + System.out.println(errorMessage); + } + + /** + * Greets the user at the beginning of the program + */ + public void greet() { + String greet = """ + Hello! I'm Elyk + What can I do for you? + """; + System.out.println(greet); + } + + /** + * Says bye to user before exiting the program + */ + public void sayBye() { + String bye = "Bye. Hope to see you again soon!"; + System.out.println(bye); + } + + /** + * Prints a message w.r.t. the task being marked done + * + * @param task task being marked done + */ + public void markTaskDone(Task task) { + System.out.println("Nice! I've marked this task as done:"); + System.out.println(" " + task); + } + + /** + * Prints a message w.r.t. the task being marked not done + * + * @param task task being marked not done + */ + public void markTaskNotDone(Task task) { + System.out.println("OK, I've marked this task as not done yet:"); + System.out.println(" " + task); + } + + /** + * Prints a message w.r.t. the task being added and shows the current total no. of tasks + * + * @param task task being added + * @param taskCounter current total no. of tasks + */ + public void inputTask(Task task, int taskCounter) { + System.out.println("Got it. I've added this task:"); + System.out.println(" " + task); + System.out.println("Now you have " + taskCounter + " tasks in the list."); + } + + /** + * Prints a message w.r.t. the task being deleted and shows the current total no. of tasks + * + * @param task task being deleted + * @param taskCounter current total no. of tasks + */ + public void deleteTask(Task task, int taskCounter) { + System.out.println("Noted. I've removed this task:"); + System.out.println(" " + task); + System.out.println("Now you have " + taskCounter + " tasks in the list."); + } + + /** + * Prints a line of message before displaying all the tasks + */ + public void printTasks() { + System.out.println("Here are the tasks in your list:"); + } + + public void printMatchingTasks() { + System.out.println("Here are the matching tasks in your list:"); + } + + /** + * Prints a single task according to its index in the TaskList + * + * @param task task i in the TaskList + * @param i index of task + */ + public void printIndividualTask(Task task, int i) { + System.out.println((i+1) + "." + task); + } +} diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 087374464..e09635375 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -15,7 +15,7 @@ IF ERRORLEVEL 1 ( REM no error here, errorlevel == 0 REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT -java -classpath ..\bin Duke < input.txt > ACTUAL.TXT +java -classpath ..\bin Elyk < input.txt > ACTUAL.TXT REM compare the output to the expected output FC ACTUAL.TXT EXPECTED.TXT