diff --git a/data/savedlist.txt b/data/savedlist.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/README.md b/docs/README.md index 8077118eb..20c60ecc4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,180 @@ # User Guide +Welcome to the HinaBot User Guide! HinaBot is a personal assistant program that helps you +maintain a to-do list. + ## Features -### Feature-ABC +### Support for multiple task types + +HinaBot supports 3 different types of items on a to-do list: regular tasks, deadlines (tasks which have a due-date), +and events (which have a start and end date). + +### Search function -Description of the feature. +HinaBot allows you to search your to-do list for tasks that contain a particular substring, making it easier to keep +track of large numbers of tasks. -### Feature-XYZ +### Delete and mark functions -Description of the feature. +HinaBot allows you to mark tasks as done or not done and also supports deletion of a task. + +### Save and load function +All changes you make to your to-do list are stored locally, meaning you can pick up right where you left off. ## Usage -### `Keyword` - Describe action +### `todo` - Add a regular task to the to-do list -Describe the action and its outcome. +Adds a regular task (no due date) to the to-do list. By default, the task is set as not done. Example of usage: -`keyword (optional arguments)` +`todo buy milk` + +Expected outcome: + +The task is added to your to-do list and HinaBot announces how many tasks are currently on your list + +``` +Noted! This task has been added: +[T][ ] buy milk +There are 1 items on your list. +``` +### `deadline` - Add a deadline to the to-do list + +Adds a deadline (with a do-by date in dd-MMM-yyyy HH:mm format) to the to-do list. By default, the task is set as not done. + +Example of usage: + +`deadline Assignment 1 /by 04-Apr-2023 23:59` Expected outcome: -Description of the outcome. +The deadline is added to your to-do list and HinaBot announces how many tasks are currently on your list + +``` +Noted! This task has been added: +[D][ ] Assignment 1 (by: 04-Apr-2023 23:59) +There are 2 items on your list +``` +### `event` - Add an event to the to-do list + +Adds an event (with start and end times in dd-MMM-yyyy HH:mm format) to the to-do list. By default, the task is set as not done. + +Example of usage: + +`event Midterms /from 03-Mar-2023 13:00 /to 03-Mar-2023 17:30` + +Expected outcome: + +The deadline is added to your to-do list and HinaBot announces how many tasks are currently on your list + +``` +Noted! This task has been added: +[E][ ] Midterms (from: 03-Mar-2023 13:00 to: 03-Mar-2023 17:30) +There are 2 items on your list +``` +### `list` - Lists all tasks in the to-do list + +Prints a list of all tasks in the to-do list including the task type and done status. + +Example of usage: + +`list` + +Expected outcome: + +The deadline is added to your to-do list and HinaBot announces how many tasks are currently on your list + +``` +1. [T][ ] buy milk +2. [D][ ] Assignment 1 (by: 04-Apr-2023 23:59) +3. [E][ ] Midterms (from: 03-Mar-2023 13:00 to: 03-Mar-2023 17:30) +``` +### `mark` - Marks a task as done + +Marks an item by its index on the to-do list as done. This is indicated by an 'X' in the checkbox next to +its description. + +Example of usage: + +`mark 1` + +Expected outcome: +An acknowledgement message and an 'X' is printed in the checkbox next to item 1 on the to-do list. ``` -expected output +Roger that! This task is marked as done: +[T][X] buy milk ``` + +### `unmark` - Marks a task as not done + +Marks an item by its index on the to-do list as not done. This is indicated by a blank checkbox next to +its description. + +Example of usage: + +`unmark 1` + +Expected outcome: + +An acknowledgement message and a blank checkbox is printed next to item 1 on the to-do list. +``` +Roger that! This task is marked as not done: +[T][ ] buy milk +``` + +### `delete` - Remove a task from the to-do list + +Removes the item at the specified index from the to-do list. + +Example of usage: + +`delete 3` + +Expected outcome: + +An acknowledgement message and the description of the task being removed is shown, followed by an announcement +of the size of the remaining list. +``` +Got it! This task will be removed: +[E][ ] Midterms (from: 03-Mar-2023 13:00 to: 03-Mar-2023 17:30) +There are 2 items on your list. +``` + +### `find` - Search for a task + +Searches the to-do list for all tasks with a description containing the substring provided by the user. + +Example of usage: + +`find buy` + +Expected outcome: + +Any matching tasks, along with their index on the list, are printed. + +``` +Found a match! Here are the results master! +1. [T][ ] buy milk +``` + +### `bye` - Exits the program + +Terminates the program and prints a goodbye message. + +Example of usage: + +`bye` + +Expected outcome: + +The program exits with code 0 and a goodbye message is shown. + +``` +Goodbye master, let's meet again soon... + +Process finished with exit code 0 +``` \ No newline at end of file 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..d83708e47 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: hina.HinaBot + diff --git a/src/main/java/hina/HinaBot.java b/src/main/java/hina/HinaBot.java new file mode 100644 index 000000000..7d794f1c8 --- /dev/null +++ b/src/main/java/hina/HinaBot.java @@ -0,0 +1,47 @@ +package hina; + +import hina.exceptions.HinaException; +import hina.helper.Parser; +import hina.helper.Storage; +import hina.helper.TaskList; +import hina.helper.Ui; + +import java.io.FileNotFoundException; + +public class HinaBot { + private static Ui ui; + private static Storage storage; + private static TaskList tasks; + + /** + * Class constructor specifying the path to a file containing save data. Tries to + * access the save file and attempts to create a new one if it does not exist. + * + * @param savePath path to the save file. + * @param dataDir directory the save file is stored in. + */ + public HinaBot(String savePath, String dataDir) { + ui = new Ui(); + storage = new Storage(); + Ui.showGreeting(); + try { + tasks = new TaskList(Storage.readSaveFile(savePath)); + } catch (FileNotFoundException exception) { + Storage.createSaveFile(savePath, dataDir); + } + } + + public static void main(String[] args) { + new HinaBot("data/savedlist.txt", "data"); + + while (true) { + try { + Parser.readCommand(); + } catch (HinaException cmdException) { + Ui.invalidCommandMessage(); + } catch (StringIndexOutOfBoundsException argException) { + Ui.notEnoughDetails(); + } + } + } +} diff --git a/src/main/java/hina/exceptions/HinaException.java b/src/main/java/hina/exceptions/HinaException.java new file mode 100644 index 000000000..f0c448cb4 --- /dev/null +++ b/src/main/java/hina/exceptions/HinaException.java @@ -0,0 +1,4 @@ +package hina.exceptions; + +public class HinaException extends Throwable { +} diff --git a/src/main/java/hina/helper/Parser.java b/src/main/java/hina/helper/Parser.java new file mode 100644 index 000000000..4f7c52dc7 --- /dev/null +++ b/src/main/java/hina/helper/Parser.java @@ -0,0 +1,87 @@ +package hina.helper; + +import hina.exceptions.HinaException; + +import java.io.IOException; +import java.util.Scanner; + +/** + * Contains methods to process user input. + */ +public class Parser { + /** + * Reads a user input from the command line and determines if it was a command. + * If the input was a recognised command, handle it respectively, else throws + * a HinaException. + * + * @throws HinaException If the input is not recognised as a valid command. + */ + public static void readCommand() throws HinaException { + String line; + Scanner in = new Scanner(System.in); + line = in.nextLine().trim(); + if (line.equalsIgnoreCase("bye")) { + Ui.showExitMessage(); + System.exit(0); + } else if (line.equalsIgnoreCase("list")) { + TaskList.listTasks(); + } else if (line.split(" ")[0].equalsIgnoreCase("mark")) { + try { + int taskIndex = Integer.parseInt(line.split(" ")[1]); + TaskList.markTask(taskIndex); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + Ui.invalidNumberMessage(); + } + try { + Storage.writeToFile(); + } catch (IOException ioexception) { + Ui.couldNotSaveMessage(); + } + } else if (line.split(" ")[0].equalsIgnoreCase("unmark")) { + int taskIndex = Integer.parseInt(line.split(" ")[1]); + TaskList.unmarkTask(taskIndex); + try { + Storage.writeToFile(); + } catch (IOException ioexception) { + Ui.couldNotSaveMessage(); + } + } else if (line.split(" ")[0].equalsIgnoreCase("todo")) { + TaskList.addTask(line.substring(5)); + try { + Storage.writeToFile(); + } catch (IOException ioexception) { + Ui.couldNotSaveMessage(); + } + } else if (line.split(" ")[0].equalsIgnoreCase("deadline")) { + TaskList.addDeadline(line.substring(9)); + try { + Storage.writeToFile(); + } catch (IOException ioexception) { + Ui.couldNotSaveMessage(); + } + } else if (line.split(" ")[0].equalsIgnoreCase("event")) { + TaskList.addEvent(line.substring(6)); + try { + Storage.writeToFile(); + } catch (IOException ioexception) { + Ui.couldNotSaveMessage(); + } + } else if (line.split(" ")[0].equalsIgnoreCase("delete")) { + try { + int taskIndex = Integer.parseInt(line.split(" ")[1]); + TaskList.deleteTask(taskIndex); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + Ui.invalidNumberMessage(); + } + try { + Storage.writeToFile(); + } catch (IOException ioexception) { + Ui.couldNotSaveMessage(); + } + } else if (line.split(" ")[0].equalsIgnoreCase("find")) { + TaskList.findTask(line); + } else { + throw new HinaException(); + } + } +} diff --git a/src/main/java/hina/helper/Storage.java b/src/main/java/hina/helper/Storage.java new file mode 100644 index 000000000..ff1868b4c --- /dev/null +++ b/src/main/java/hina/helper/Storage.java @@ -0,0 +1,103 @@ +package hina.helper; + +import hina.task.Deadline; +import hina.task.Event; +import hina.task.Task; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Scanner; + +public class Storage { + /** + * Class constructor. + */ + public Storage() { + } + + /** + * Returns an ArrayList containing Task objects from a .txt save file + * at a specified path on the hard disk. + * + * @param savePath relative path to the save file. + * @return the ArrayList of saved Task elements. + * @throws FileNotFoundException If the file could not be found at the specified path. + */ + public static ArrayList readSaveFile(String savePath) throws FileNotFoundException { + ArrayList savedList = new ArrayList<>(); + File saveFile = new File(savePath); + Scanner save = new Scanner(saveFile); + Ui.saveFound(); + while (save.hasNext()) { + String line = save.nextLine(); + String[] taskDetails = line.split(" / "); + switch (taskDetails[0]) { + case "T": + Task savedTask = new Task(taskDetails[2]); + savedTask.setDone(!taskDetails[1].contains("0")); + savedList.add(savedTask); + break; + case "D": + try { + Deadline savedDeadline = new Deadline(taskDetails[2], + LocalDateTime.parse(taskDetails[3], TaskList.formatter)); + savedDeadline.setDone(!taskDetails[1].contains("0")); + savedList.add(savedDeadline); + } catch (DateTimeParseException e) { + System.out.println(e.getMessage()); + } + break; + case "E": + Event savedEvent = new Event(taskDetails[2], LocalDateTime.parse(taskDetails[3], TaskList.formatter), + LocalDateTime.parse(taskDetails[4], TaskList.formatter)); + savedEvent.setDone(!taskDetails[1].contains("0")); + savedList.add(savedEvent); + break; + } + } + return savedList; + } + + /** + * Creates a new .txt file at the specified path. If the directory does not exist, + * creates a new one. + * + * @param savePath relative path to the new file. + * @param dataDir relative path to the new file's directory. + */ + public static void createSaveFile(String savePath, String dataDir) { + Ui.saveNotFound(); + File newFile = new File(savePath); + try { + Path dataPath = Paths.get(dataDir); + Files.createDirectory(dataPath); + newFile.createNewFile(); + Ui.saveCreated(); + } catch (IOException ioException) { + Ui.fileCreateError(); + } + } + + /** + * Prints all the formatted Task objects in the TaskList on separate lines + * into a .txt file on the hard disk. + * + * @throws IOException If the file could not be written to. + */ + public static void writeToFile() throws IOException { + FileWriter saveFile = new FileWriter("data/savedlist.txt"); + for (Task taskToSave : TaskList.getTaskList()) { + saveFile.write(taskToSave.toSave()); + saveFile.write("\n"); + } + saveFile.close(); + } +} diff --git a/src/main/java/hina/helper/TaskList.java b/src/main/java/hina/helper/TaskList.java new file mode 100644 index 000000000..e6ff96180 --- /dev/null +++ b/src/main/java/hina/helper/TaskList.java @@ -0,0 +1,186 @@ +package hina.helper; + +import hina.task.Deadline; +import hina.task.Event; +import hina.task.Task; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +public class TaskList { + private static ArrayList taskList; + + /** + * Specifies the date-time format to be used. + */ + public static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm"); + + /** + * Class constructor with a taskList initialised with the Task elements of + * another ArrayList. + * + * @param savedList the list from which elements are initialised. + */ + public TaskList(ArrayList savedList) { + taskList = savedList; + } + + /** + * Returns an ArrayList containing the Task elements currently stored in + * the taskList. + * + * @return the ArrayList of Task objects. + */ + public static ArrayList getTaskList() { + return taskList; + } + + /** + * Prints line-by-line the data of the Task objects stored in the taskList. + */ + public static void listTasks() { + if (taskList.size() == 0) { + Ui.emptyListMessage(); + } + for (Task task : taskList) { + int i = taskList.indexOf(task); + System.out.print(i + 1); + System.out.print(". "); + System.out.println(task.toString()); + } + } + + /** + * Prints the number of elements in the taskList. + */ + public static void getTaskCount() { + System.out.printf("There are %d items on your list.\n", taskList.size()); + } + + /** + * Adds a Task object to the taskList. + * + * @param description the description of the Task. + */ + public static void addTask(String description) { + Task newTask = new Task(description); + taskList.add(newTask); + Ui.taskAdded(newTask); + getTaskCount(); + } + + /** + * Adds a Deadline object to the taskList. + * + * @param deadline a String specifying the description of the Deadline and + * the do-by date and time. + */ + public static void addDeadline(String deadline) { + String[] details = deadline.split("/by"); + if (details.length < 2) { + System.out.println("Hina needs to know the deadline for this task!"); + } else { + try { + LocalDateTime by = LocalDateTime.parse(details[1].trim(), formatter); + Deadline newDeadline = new Deadline(details[0], by); + taskList.add(newDeadline); + Ui.taskAdded(newDeadline); + getTaskCount(); + + } catch (DateTimeException exception) { + Ui.showDateTimeError(); + } + } + } + + /** + * Adds an Event object to the taskList. + * + * @param event a String specifying the description of the Event and + * the start and end time. + */ + public static void addEvent(String event) { + String[] details = event.split("/from"); + if (details.length < 2) { + System.out.println("Please tell Hina when this event starts!"); + } else { + if (details[1].split("/to").length < 2) { + System.out.println("Please tell Hina when this event ends!"); + } else { + try { + LocalDateTime from = LocalDateTime.parse(details[1].split("/to")[0].trim(), formatter); + LocalDateTime to = LocalDateTime.parse(details[1].split("/to")[1].trim(), formatter); + Event newEvent = new Event(details[0], from, to); + taskList.add(newEvent); + Ui.taskAdded(newEvent); + getTaskCount(); + + } catch (DateTimeException exception) { + Ui.showDateTimeError(); + } + + } + } + } + public static void deleteTask(int taskIndex) { + try { + Task toDelete = taskList.get(taskIndex - 1); + System.out.println("Got it! This task will be removed:"); + System.out.println(toDelete); + taskList.remove(taskIndex - 1); + getTaskCount(); + } catch (IndexOutOfBoundsException e) { + Ui.invalidNumberMessage(); + } + } + public static void markTask(int taskIndex) { + try { + taskList.get(taskIndex - 1).setDone(true); + System.out.println("Roger that! This task is marked as done: "); + System.out.println(taskList.get(taskIndex - 1).toString()); + } catch (IndexOutOfBoundsException e) { + Ui.invalidNumberMessage(); + } + } + public static void unmarkTask(int taskIndex) { + try { + taskList.get(taskIndex - 1).setDone(false); + System.out.println("Roger that! This task is marked as not done: "); + System.out.println(taskList.get(taskIndex - 1).toString()); + } catch (IndexOutOfBoundsException e) { + Ui.invalidNumberMessage(); + } + } + + /** + * Determines, ignoring and trailing and leading whitespaces, if any of the + * Task descriptions contain the exact query substring. Prints + * the Task data of those that do and prints a message if none are + * found. + * + * @param line the query substring. + */ + public static void findTask(String line) { + String query = line.substring(4).trim(); + boolean matchFound = false; + ArrayList matchList = new ArrayList<>(); + for (Task task : taskList) { + if (task.getDescription().contains(query)) { + int i = taskList.indexOf(task); + String match = String.format("%d. %s", i + 1, task); + matchList.add(match); + matchFound = true; + } + } + if (matchFound) { + Ui.taskFoundMessage(); + for (String task : matchList) { + System.out.println(task); + } + } else { + Ui.taskNotFoundMessage(); + } + } +} diff --git a/src/main/java/hina/helper/Ui.java b/src/main/java/hina/helper/Ui.java new file mode 100644 index 000000000..d6264a961 --- /dev/null +++ b/src/main/java/hina/helper/Ui.java @@ -0,0 +1,62 @@ +package hina.helper; + +import hina.task.Task; + +/** + * Contains methods to print messages to the user. + */ +public class Ui { + /** + * Class constructor. + */ + public Ui() { + } + + public static void showGreeting() { + System.out.println("Hello master!"); + System.out.println("What are your orders?"); + } + public static void showExitMessage() { + System.out.println("Goodbye master, let's meet again soon..."); + } + public static void invalidCommandMessage() { + System.out.println(">.< Hina does not recognise this command!"); + } + public static void notEnoughDetails() { + System.out.println("@_@ Please give Hina more details!"); + } + public static void couldNotSaveMessage() { + System.out.println("Something went wrong, could not save!"); + } + public static void taskFoundMessage() { + System.out.println("Found a match! Here are the results master!"); + } + public static void taskNotFoundMessage() { + System.out.println("Hina could not find anything about that in master's list..."); + } + public static void showDateTimeError() { + System.out.println("Please use dd-MMM-yyyy HH:mm format!"); + } + public static void emptyListMessage() { + System.out.println("There are no items on the list :o"); + } + public static void taskAdded(Task task) { + System.out.println("Noted! This task has been added:"); + System.out.println(task); + } + public static void saveFound() { + System.out.println("Saved list found, loading saved list..."); + } + public static void saveNotFound() { + System.out.println("Save file not found! Creating new file..."); + } + public static void saveCreated() { + System.out.println("Save file created!"); + } + public static void fileCreateError() { + System.out.println("T.T Ahh! Something went wrong, could not create file!"); + } + public static void invalidNumberMessage() { + System.out.println("That's not a valid number!"); + } +} diff --git a/src/main/java/hina/task/Deadline.java b/src/main/java/hina/task/Deadline.java new file mode 100644 index 000000000..0a347be81 --- /dev/null +++ b/src/main/java/hina/task/Deadline.java @@ -0,0 +1,41 @@ +package hina.task; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Represents a Task on a to-do list with a deadline. + */ +public class Deadline extends Task { + private LocalDateTime by; + String byString; + + /** + * Class constructor specifying this Deadline's description and + * due date. + * + * @param description description of this Deadline. + * @param by LocalDateTime object specifying the due date. + */ + public Deadline(String description, LocalDateTime by) { + super(description); + this.by = by; + this.byString = this.by.format(DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm")); + } + + @Override + public String toString() { + String mark; + if (super.isDone()) { + mark = "X"; + } else { + mark = " "; + } + return String.format("[D][%s] %s (by: %s)", mark, super.getDescription(), byString); + } + + @Override + public String toSave() { + return String.format("D / %s / %s / %s", isDone? "1" : "0", description, byString); + } +} diff --git a/src/main/java/hina/task/Event.java b/src/main/java/hina/task/Event.java new file mode 100644 index 000000000..ba8fca8ec --- /dev/null +++ b/src/main/java/hina/task/Event.java @@ -0,0 +1,54 @@ +package hina.task; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Represents an event on a to-do list that has start and end dates and times. + */ +public class Event extends Task { + private LocalDateTime from; + private String fromStr; + private LocalDateTime to; + private String toStr; + + /** + * Class constructor specifying this Event's description, start and end + * time. + * + * @param description description of this Event. + * @param from LocalDateTime object specifying the start time. + * @param to LocalDateTime object specifying the end time. + */ + public Event(String description, LocalDateTime from, LocalDateTime to) { + super(description); + this.from = from; + this.to = to; + this.fromStr = this.from.format(DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm")); + this.toStr = this.to.format(DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm")); + } + + @Override + public String toString() { + String mark; + if (super.isDone()) { + mark = "X"; + } else { + mark = " "; + } + return String.format("[E][%s] %s(from: %s to: %s)", mark, super.getDescription(), fromStr, toStr); + } + + public String getFromStr() { + return fromStr; + } + + public String getToStr() { + return toStr; + } + + @Override + public String toSave() { + return String.format("E / %s / %s / %s / %s", isDone? "1" : "0", description, fromStr, toStr); + } +} diff --git a/src/main/java/hina/task/Task.java b/src/main/java/hina/task/Task.java new file mode 100644 index 000000000..479603fcb --- /dev/null +++ b/src/main/java/hina/task/Task.java @@ -0,0 +1,83 @@ +package hina.task; + +/** + * Represents a task on a to-do list that can be set as done or undone. + * + */ +public class Task { + protected String description; + protected boolean isDone; + + /** + * Returns done status of the Task. + * + * @return done status. + */ + public boolean isDone() { + return isDone; + } + + /** + * Sets the done status of the Task. + * + * @param done status to set the done status to. + */ + public void setDone(boolean done) { + isDone = done; + } + + /** + * Class constructor specifying this Task's description. + * + * @param description name of this Task. + */ + public Task(String description) { + this.description = description; + isDone = false; + } + + /** + * Returns the description of this Task. + * + * @return description of this Task. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of this Task. + * + * @param description new description of this task. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Returns a formatted string with the type, isDone status + * and description of this Task. + * + * @return the formatted string. + */ + public String toString() { + String mark; + if (isDone) { + mark = "X"; + } else { + mark = " "; + } + return String.format("[T][%s] %s", mark, description); + } + + /** + * Returns a formatted string with the type, a number representing the isDone + * status ('0' for false, '1' for true), and description of the Task + * for saving to the hard disk. + * + * @return the formatted string. + */ + public String toSave() { + return String.format("T / %s / %s", isDone? "1" : "0", description); + } +}