diff --git a/.gitignore b/.gitignore index 2873e189e..6690f772d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +/data/ +/text-ui-test/data/ diff --git a/README.md b/README.md index 8715d4d91..0a09509ef 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,61 @@ -# Duke 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. - -## Setting up in Intellij - -Prerequisites: JDK 11, update Intellij to the most recent version. - -1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first) -1. Open the project into Intellij as follows: - 1. Click `Open`. - 1. Select the project directory, and click `OK`. - 1. If there are any further prompts, accept the defaults. -1. Configure the project to use **JDK 11** (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. -3. 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: - ``` - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - ``` +# Nuke User Guide +Nuke is a desktop app for managing tasks using CLI (Command Line Interface). + + +## Features + + +### Exiting: `bye` + +Exits program while saving the tasks on the file system. + +Format: `bye` + +### Adding a task: `todo` + +Adds a task to the list. + +Format: `todo ` + +### Adding a task with a deadline: `deadline` + +Adds a task with a deadline to the list. + +Format: `deadline /by ` + +### Adding an event: `event` + +Adds an event to the list. + +Format: `event /from /to ` + +### Viewing all tasks: `list` + +Shows a list of tasks in the list. + +Format: `list` + +### Marking a task as done: `mark` + +Marks a task as done. + +Format: `mark ` + +### Marking a task as not done: `unmark` + +Marks a task as not done. + +Format: `unmark ` + +### Deleting a task: `delete` + +Deletes a task in the list. + +Format: `delete ` + +### Finding a task by keyword: `find` + +Finds a task in the list by a keyword. + +Format: `find ` + diff --git a/docs/README.md b/docs/README.md index 8077118eb..38f9c23c5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,198 @@ # User Guide +Nuke is a desktop app for managing tasks using CLI (Command Line Interface). ## Features -### Feature-ABC +### Exiting: `bye` -Description of the feature. +Exits program while saving the tasks on the file system. -### Feature-XYZ +### Adding a task: `todo` + +Adds a task to the list. + +### Adding a task with a deadline: `deadline` + +Adds a task with a deadline to the list. + +### Adding an event: `event` + +Adds an event to the list. + +### Viewing all tasks: `list` + +Shows a list of tasks in the list. + +### Marking a task as done: `mark` + +Marks a task as done. + +### Marking a task as not done: `unmark` + +Marks a task as not done. + +### Deleting a task: `delete` + +Deletes a task in the list. + +### Finding a task by keyword: `find` + +Finds a task in the list by a keyword. -Description of the feature. ## Usage -### `Keyword` - Describe action +### `bye` - Exits program + +Exits program while saving the tasks on the file system. + +Example of usage: + +`bye` + +Expected outcome: + +``` +[@] Bye. Hope to see you again soon! +``` + +### `todo` - Adds a task + +Adds a task to the list. + +Example of usage: + +`todo ` + +Expected outcome: + +If input is `todo read book`, + +``` +[@] Got it. I've added this task: + [T][ ] read book +[@] Now you have 1 task in the list. +``` + +### `deadline` - Adds a task with a deadline + +Adds a task with a deadline to the list. + +Example of usage: + +`deadline /by ` + +Expected outcome: + +If input is `deadline return book /by June 6th`, + +``` +[@] Got it. I've added this task: + [D][ ] return book (by: June 6th) +[@] Now you have 2 tasks in the list. +``` + +### `event` - Adds an event + +Adds an event to the list. + +Example of usage: + +`event /from /to ` + +Expected outcome: + +If input is `event project meeting /from Aug 6th 2pm /to 4pm`, + +``` +[@] Got it. I've added this task: + [E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +[@] Now you have 3 tasks in the list. +``` + +### `list` - Views all tasks + +Shows a list of tasks in the list. + +Example of usage: + +`list` + +Expected outcome: + +``` +[@] Here are the tasks in your list: +1.[T][ ] read book +2.[D][ ] return book (by: June 6th) +3.[E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +``` + +### `mark` - Marks a task as done + +Marks a task as done. + +Example of usage: + +`mark ` + +Expected outcome: + +If input is `mark 1`, + +``` +[@] Nice! I've marked this task as done: + [T][X] read book +``` + +### `unmark` - Marks a task as not done + +Marks a task as not done. + +Example of usage: + +`unmark ` + +Expected outcome: + +If input is `unmark 3`, + +``` +[@] OK, I've marked this task as not done yet: + [E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +``` + +### `delete` - Deletes a task + +Deletes a task in the list. + +Example of usage: + +`delete ` + +Expected outcome: + +If input is `delete 3`, + +``` +[@] Noted. I've removed this task: + [E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +[@] Now you have 2 tasks in the list. +``` + +### `find` - Finds a task by keyword -Describe the action and its outcome. +Finds a task in the list by a keyword. -Example of usage: +Example of usage: -`keyword (optional arguments)` +`find ` Expected outcome: -Description of the outcome. +If input is `find book`, ``` -expected output +[@] Here are the matching tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) ``` 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..940e1c023 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 0.1 +Main-Class: nuke.Nuke + diff --git a/src/main/java/nuke/Nuke.java b/src/main/java/nuke/Nuke.java new file mode 100644 index 000000000..dc2f1f0fc --- /dev/null +++ b/src/main/java/nuke/Nuke.java @@ -0,0 +1,193 @@ +package nuke; + +import nuke.command.Command; +import nuke.command.CommandParser; +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.command.exception.InvalidCommandTypeException; +import nuke.storage.Storage; +import nuke.storage.exception.TaskFileCopyException; +import nuke.storage.exception.TaskLoadException; +import nuke.storage.exception.TaskSaveException; +import nuke.task.Deadline; +import nuke.task.Event; +import nuke.task.Task; +import nuke.task.TaskList; +import nuke.task.Todo; + +/** + * Represents main part of Nuke. + */ +public class Nuke { + private final Storage storage; + private final TaskList tasks; + private final Ui ui; + private boolean isRunning; + + public static void main(String[] args) { + new Nuke().run(); + } + + private Nuke() { + ui = new Ui(); + ui.printWelcome(); + + storage = new Storage(); + tasks = loadTasksFromStorage(storage, ui); + + isRunning = true; + } + + private void run() { + ui.printGreetingQuestion(); + + // Loop for user input + while (isRunning) { + String input = ui.scanNextLine(); + runCommand(input); + ui.printBlankLine(); + } + + saveTasksToStorage(storage, ui); + } + + private TaskList loadTasksFromStorage(Storage storage, Ui ui) { + try { + return new TaskList(storage.loadTasks()); + } catch (TaskLoadException e) { + // Invoked when loading tasks from the file fails + ui.printTaskLoadError(e); + return new TaskList(); + } catch (TaskFileCopyException e) { + // Invoked when + // 1. loading tasks from the file fails and + // 2. backing up the file fails + // If user does not ignore the error, throw RuntimeException. + ui.handleTaskFileCopyError(e); + return new TaskList(); + } + } + + private void runCommand(String commandLine) { + try { + Command command = CommandParser.parseCommand(commandLine); + command.run(this); + } catch (InvalidCommandTypeException e) { + ui.printCommandTypeError(e); + } catch (InvalidCommandArgumentException e) { + ui.printCommandArgumentError(e); + } + } + + private void saveTasksToStorage(Storage storage, Ui ui) { + try { + storage.saveTasks(tasks.getFormattedTasks()); + } catch (TaskSaveException e) { + e.tasks = tasks.getTasks(); + ui.handleTaskSaveError(e); + } + } + + /** + * Makes Nuke to quit. + */ + public void quit() { + isRunning = false; + ui.printBye(); + } + + /** + * Adds the task. + * + * @param task task to add + */ + public void addTask(Task task) { + tasks.add(task); + ui.printAddedTask(task.toString(), countTasks()); + } + + /** + * Lists all tasks. + */ + public void listTask() { + ui.printListOfTasks(tasks.getTasks()); + } + + /** + * Marks the task of the index as done. + * + * @param idx index of the task to be marked done + */ + public void markTask(int idx) { + String markedTask = tasks.mark(idx); + ui.printMarkedTask(markedTask); + } + + /** + * Marks the task of the index as not done. + * + * @param idx index of the task to be marked not done + */ + public void unmarkTask(int idx) { + String unmarkedTask = tasks.unmark(idx); + ui.printUnmarkedTask(unmarkedTask); + } + + /** + * Deletes the task of the index. + * + * @param idx index of the task to be deleted + */ + public void deleteTask(int idx) { + String deletedTask = tasks.delete(idx); + ui.printDeletedTask(deletedTask, countTasks()); + } + + /** + * Finds tasks with the keyword. + * + * @param keyword keyword + */ + public void findTasks(String keyword) { + String[] foundTasks = tasks.find(keyword); + ui.printFoundTask(foundTasks); + } + + /** + * Adds {@link Todo}. + * + * @param name name of {@link Todo} to be added + */ + public void addTodo(String name) { + addTask(new Todo(name)); + } + + /** + * Adds {@link Deadline}. + * + * @param name name of {@link Deadline} to be added + * @param by deadline of {@link Deadline} to be added + */ + public void addDeadline(String name, String by) { + addTask(new Deadline(name, by)); + } + + /** + * Adds {@link Event}. + * + * @param name name of {@link Event} to be added + * @param from start period of {@link Event} to be added + * @param to end period of {@link Event} to be added + */ + public void addEvent(String name, String from, String to) { + addTask(new Event(name, from, to)); + } + + /** + * Returns the number of the tasks. + * + * @return the number of the tasks + */ + public int countTasks() { + return tasks.size(); + } +} diff --git a/src/main/java/nuke/NukeDateTime.java b/src/main/java/nuke/NukeDateTime.java new file mode 100644 index 000000000..de016664a --- /dev/null +++ b/src/main/java/nuke/NukeDateTime.java @@ -0,0 +1,148 @@ +package nuke; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Locale; + +/** + * Represents a chronological period. + * It supports both {@link String} and {@link LocalDateTime}. + */ +public class NukeDateTime { + private boolean isUsingLocalDateTime; + private LocalDateTime localDateTime; + private String string; + + /** + * Constructs a period with a string. + * + * @param dateTime string indicating the period + */ + public NukeDateTime(String dateTime) { + try { + localDateTime = parseDateTime(dateTime); + isUsingLocalDateTime = true; + string = toString(); + } catch (NukeException e) { + localDateTime = null; + isUsingLocalDateTime = false; + string = dateTime; + } + } + + /** + * Returns a string representation of the period. + * The format is as following. + *

+ * 'MMM dd yyyy, HH:mm' + * + * @return a string representation of the period + */ + @Override + public String toString() { + if (isUsingLocalDateTime) { + return localDateTime.format( + DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm", Locale.US) + ); + } else { + return string; + } + } + + /** + * Parses the period from {@link String} to {@link LocalDateTime}. + * Tries to parse using 10 different formats. + *

+ * ISO_DATE; yyyy-MM-dd with optional offset
+ * MM-dd
+ * ISO_TIME; HH:mm:ss or HH:mm with optional offset
+ * ISO_DATE_TIME; yyyy-MM-dd'T'HH:mm:ss or yyyy-MM-dd'T'HH:mm with optional offset
+ * yyyy-MM-dd HH:mm:ss
+ * yyyy-MM-dd HH:mm
+ * MM-dd HH:mm:ss
+ * MM-dd HH:mm
+ * d/M/y HHmm
+ * d/M/y + * + * @param dateTime period + * @return parsed period + * @throws NukeDateTimeParseException if parsing fails + */ + private LocalDateTime parseDateTime(String dateTime) + throws NukeDateTimeParseException { + try { + return LocalDate.parse( + dateTime, + DateTimeFormatter.ISO_DATE + ).atStartOfDay(); + } catch (DateTimeParseException ignored) { } + try { + return LocalDate.parse( + dateTime, + createFormatterWithoutYear("MM-dd") + ).atStartOfDay(); + } catch (DateTimeParseException ignored) { } + try { + return LocalTime.parse( + dateTime, + DateTimeFormatter.ISO_TIME + ).atDate(LocalDate.now()); + } catch (DateTimeParseException ignored) { } + try { + return LocalDateTime.parse( + dateTime, + DateTimeFormatter.ISO_DATE_TIME + ); + } catch (DateTimeParseException ignored) { } + try { + return LocalDateTime.parse( + dateTime, + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + ); + } catch (DateTimeParseException ignored) { } + try { + return LocalDateTime.parse( + dateTime, + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + ); + } catch (DateTimeParseException ignored) { } + try { + return LocalDateTime.parse( + dateTime, + createFormatterWithoutYear("MM-dd HH:mm:ss") + ); + } catch (DateTimeParseException ignored) { } + try { + return LocalDateTime.parse( + dateTime, + createFormatterWithoutYear("MM-dd HH:mm") + ); + } catch (DateTimeParseException ignored) { } + try { + return LocalDateTime.parse( + dateTime, + DateTimeFormatter.ofPattern("d/M/y HHmm") + ); + } catch (DateTimeParseException ignored) { } + try { + return LocalDate.parse( + dateTime, + DateTimeFormatter.ofPattern("d/M/y") + ).atStartOfDay(); + } catch (DateTimeParseException ignored) { } + + throw new NukeDateTimeParseException(); + } + + private static DateTimeFormatter createFormatterWithoutYear(String pattern) { + return new DateTimeFormatterBuilder() + .appendPattern(pattern) + .parseDefaulting(ChronoField.YEAR, LocalDate.now().getYear()) + .toFormatter(); + } +} diff --git a/src/main/java/nuke/NukeDateTimeParseException.java b/src/main/java/nuke/NukeDateTimeParseException.java new file mode 100644 index 000000000..4596169e7 --- /dev/null +++ b/src/main/java/nuke/NukeDateTimeParseException.java @@ -0,0 +1,4 @@ +package nuke; + +public class NukeDateTimeParseException extends NukeException { +} diff --git a/src/main/java/nuke/NukeException.java b/src/main/java/nuke/NukeException.java new file mode 100644 index 000000000..9a089e3a3 --- /dev/null +++ b/src/main/java/nuke/NukeException.java @@ -0,0 +1,4 @@ +package nuke; + +public class NukeException extends Exception { +} diff --git a/src/main/java/nuke/Ui.java b/src/main/java/nuke/Ui.java new file mode 100644 index 000000000..3674b4518 --- /dev/null +++ b/src/main/java/nuke/Ui.java @@ -0,0 +1,200 @@ +package nuke; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.command.exception.InvalidCommandTypeException; +import nuke.storage.exception.TaskFileCopyException; +import nuke.storage.exception.TaskLoadException; +import nuke.storage.exception.TaskSaveException; + +import java.util.Scanner; + +/** + * Represents UI of Nuke. + */ +public class Ui { + private final String IGNORE_ERROR_COMMAND = "ignore"; + + private final Scanner USER_IN; + + /** + * Constructs UI of Nuke. + */ + public Ui() { + USER_IN = new Scanner(System.in); + } + + /** + * Scans a line from user input. + * + * @return user input as a line of string + */ + public String scanNextLine() { + return USER_IN.nextLine(); + } + + /** + * Returns if user inputs to ignore error. + * + * @return if the user input equals to the {@code IGNORE_ERROR_COMMAND}. + */ + public boolean scanIgnore() { + return scanNextLine().equals(IGNORE_ERROR_COMMAND); + } + + /** + * Scans until user input is not empty. + */ + public void scanWhileEmpty() { + //noinspection StatementWithEmptyBody + while (scanNextLine().isEmpty()); + } + + public void printWelcome() { + System.out.println(LOGO); + System.out.println(); + System.out.println("[@] Hello! I'm Nuke."); + } + + public void printGreetingQuestion() { + System.out.println("[@] What can I do for you?"); + System.out.println(); + } + + public void printBlankLine() { + System.out.println(); + } + + public void printBye() { + System.out.println("[@] Bye. Hope to see you again soon!"); + } + + public void printAddedTask(String addedTask, int taskCnt) { + System.out.println("[@] Got it. I've added this task:"); + System.out.println(" " + addedTask); + System.out.printf("[@] Now you have %d task%s in the list.\n", taskCnt, taskCnt == 1 ? "": "s"); + } + + public void printListOfTasks(String[] tasks) { + if(tasks.length == 0) { + System.out.println("[@] There are no tasks in your list."); + return; + } + System.out.println("[@] Here are the tasks in your list:"); + for (int i = 0; i < tasks.length; i++) { + System.out.printf("%d.%s\n", i + 1, tasks[i]); + } + } + + public void printMarkedTask(String markedTask) { + System.out.println("[@] Nice! I've marked this task as done:"); + System.out.println(" " + markedTask); + } + + public void printUnmarkedTask(String unmarkedTask) { + System.out.println("[@] OK, I've marked this task as not done yet:"); + System.out.println(" " + unmarkedTask); + } + + public void printDeletedTask(String deletedTask, int taskCnt) { + System.out.println("[@] Noted. I've removed this task:"); + System.out.println(" " + deletedTask); + System.out.printf("[@] Now you have %d task%s in the list.\n", taskCnt, taskCnt == 1 ? "": "s"); + } + + public void printFoundTask(String[] foundTasks) { + if(foundTasks.length == 0) { + System.out.println("[@] There are no matching tasks in your list."); + return; + } + System.out.println("[@] Here are the matching tasks in your list:"); + for (int i = 0; i < foundTasks.length; i++) { + System.out.printf("%d.%s\n", i + 1, foundTasks[i]); + } + } + + public void printCommandError(String description, String detail) { + System.out.println("[@] Wrong input; " + description); + System.out.println("[@] " + detail); + } + + public void printCommandTypeError(InvalidCommandTypeException e) { + String desc = String.format("There is no command called '%s'.", e.type); + String detail = "Existing command: bye, list, mark, unmark, todo, deadline, event, delete, find"; + printCommandError(desc, detail); + } + + public void printCommandArgumentError(InvalidCommandArgumentException e) { + String desc = e.reason; + printCommandError(desc, e.correctUsage); + } + + public void printTaskLoadError(TaskLoadException e) { + System.out.println(); + System.out.println("[@] Error occurred while loading the tasks!"); + System.out.println("[@] I backed up your save file."); + System.out.println("[@] Path: " + e.backupFilePath); + System.out.println(); + } + + /** + * Handles an error which occurs when + * loading tasks from the file fails and + * backing up the file fails. + * If user does not ignore the error, this throws RuntimeException. + * + * @param e exception + */ + public void handleTaskFileCopyError(TaskFileCopyException e) { + printTaskFileCopyError(e, IGNORE_ERROR_COMMAND); + if (!scanIgnore()) { + throw new RuntimeException(e); + } + } + + public void printTaskFileCopyError(TaskFileCopyException e, String ignoreCommand) { + System.out.println(); + System.out.println("[@] Error occurred while loading the tasks!"); + System.out.println("[@] I tried to back up your save file, but it failed as well."); + System.out.println("[@] You can back up your save file manually."); + System.out.println("[@] Path: " + e.filePath); + System.out.println(); + System.out.printf("[@] Ignore and continue to run by entering '%s'.", ignoreCommand); + System.out.println("[@] Otherwise I will quit."); + System.out.println(); + } + + /** + * Handles an error which occurs when + * saving tasks to the file fails. + * If user enters non-empty input, this continues to quit. + * + * @param e exception + */ + public void handleTaskSaveError(TaskSaveException e) { + printTaskSaveError(e.tasks); + scanWhileEmpty(); + } + + public void printTaskSaveError(String[] tasks) { + System.out.println("[@] Error occurred while saving the tasks!"); + System.out.println("[@] I will show all the tasks you have added."); + printListOfTasks(tasks); + System.out.println(); + System.out.println("[@] Please back up your save file manually."); + System.out.println("[@] Continue to quit by entering non-empty input."); + System.out.println(); + } + + public static final String LOGO = + " _.-^^---....,,-- \n" + + " _-- --_ \n" + + "< >)\n" + + "| |\n" + + " \\._ _./ \n" + + " ```--. . , ; .--''' \n" + + " | | | \n" + + " .-=|| | |=-. \n" + + " `-=#$%&%$#=-' \n" + + " | ; :| \n" + + " _____.,-#%&$@%#&#~,._____ "; +} diff --git a/src/main/java/nuke/command/ByeCommand.java b/src/main/java/nuke/command/ByeCommand.java new file mode 100644 index 000000000..b2f2d1ac0 --- /dev/null +++ b/src/main/java/nuke/command/ByeCommand.java @@ -0,0 +1,34 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class ByeCommand extends Command { + public static final String TYPE = "bye"; + private static final String USAGE = TYPE; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (!args.isEmpty()) { + throwArgumentException(ERROR_MSG_TOO_MANY_ARGS); + } + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) { + nuke.quit(); + } + + private static final String ERROR_MSG_TOO_MANY_ARGS = + "Command 'bye' should have no arguments."; +} diff --git a/src/main/java/nuke/command/Command.java b/src/main/java/nuke/command/Command.java new file mode 100644 index 000000000..9d8025a37 --- /dev/null +++ b/src/main/java/nuke/command/Command.java @@ -0,0 +1,69 @@ +package nuke.command; + +import nuke.Nuke; +import nuke.command.exception.InvalidCommandArgumentException; + +import java.util.Arrays; + +/** + * Represents command which can be input by users. + */ +public abstract class Command { + + /** + * Parses arguments and then applies the result to set the values. + * @param args command argument + * @throws InvalidCommandArgumentException if parsing argument fails + */ + public abstract void applyArguments(String args) throws InvalidCommandArgumentException; + + /** + * Returns the type of the command. + * + * @return the type of the command. + */ + protected abstract String getType(); + + /** + * Returns the usage of the command. + * + * @return the usage of the command. + */ + protected abstract String getUsage(); + + /** + * Runs the command. + * + * @param nuke the Nuke which is running this command. + * @throws InvalidCommandArgumentException if running fails due to one or more arguments + */ + public abstract void run(Nuke nuke) throws InvalidCommandArgumentException; + + /** + * Checks if one of forbidden characters exists, + * and throws exception if it does. + * @param parsedArgs array of parsed arguments + * @throws InvalidCommandArgumentException if one of forbidden characters exists in the arguments + */ + protected void checkForbiddenCharacters(String[] parsedArgs) + throws InvalidCommandArgumentException { + if (Arrays.stream(parsedArgs).anyMatch(s -> s.contains(FORBIDDEN_CHARACTERS))) { + String errorMessage = String.format(ERROR_MSG_FORBIDDEN_CHARACTERS, getType()); + throwArgumentException(errorMessage); + } + } + + /** + * Throws an argument exception. + * @param reason reason of the exception + * @throws InvalidCommandArgumentException with a reason and the usage of the command + */ + protected void throwArgumentException(String reason) + throws InvalidCommandArgumentException { + throw new InvalidCommandArgumentException(reason, "Usage: " + getUsage()); + } + + private static final String FORBIDDEN_CHARACTERS = "/"; + private static final String ERROR_MSG_FORBIDDEN_CHARACTERS = + "Command '%s' should not contain '" + FORBIDDEN_CHARACTERS + "' in arguments."; +} diff --git a/src/main/java/nuke/command/CommandParser.java b/src/main/java/nuke/command/CommandParser.java new file mode 100644 index 000000000..429374330 --- /dev/null +++ b/src/main/java/nuke/command/CommandParser.java @@ -0,0 +1,137 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.command.exception.InvalidCommandTypeException; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents parser of commands. + */ +public class CommandParser { + + /** + * Parses a line of command into {@link Command} and returns it. + * @param commandLine string of command + * @return command; result of parsing + * @throws InvalidCommandTypeException if type of command is invalid + * @throws InvalidCommandArgumentException if one or more arguments of command are invalid + */ + public static Command parseCommand(String commandLine) + throws InvalidCommandTypeException, InvalidCommandArgumentException { + // Get command type and arguments. + String type = getCommandType(commandLine); + String args = getCommandArguments(commandLine, type); + + // Construct command using command type. + Command command = getBlankCommand(type); + // Apply arguments on command. + command.applyArguments(args); + + return command; + } + + private static Command getBlankCommand(String type) + throws InvalidCommandTypeException { + Command command; + switch (type) { + case ByeCommand.TYPE: + command = new ByeCommand(); + break; + case ListCommand.TYPE: + command = new ListCommand(); + break; + case MarkCommand.TYPE: + command = new MarkCommand(); + break; + case UnmarkCommand.TYPE: + command = new UnmarkCommand(); + break; + case TodoCommand.TYPE: + command = new TodoCommand(); + break; + case DeadlineCommand.TYPE: + command = new DeadlineCommand(); + break; + case EventCommand.TYPE: + command = new EventCommand(); + break; + case DeleteCommand.TYPE: + command = new DeleteCommand(); + break; + case FindCommand.TYPE: + command = new FindCommand(); + break; + default: + throw new InvalidCommandTypeException(type); + } + return command; + } + + private static String getCommandType(String commandLine) { + return commandLine.split(REGEX_WHITESPACE)[0]; + } + + private static String getCommandArguments(String commandLine, String type) { + return commandLine.substring(type.length()).strip(); + } + + /** + * Returns if the string is one word. + * @param str string + * @return if the string is not one word. + */ + public static boolean isNotOneWord(String str) { + if (str.isEmpty()) { + return true; + } else return str.split(REGEX_WHITESPACE).length != 1; + } + + /** + * Returns if the string is not containing exact one label + * @param str string + * @param label label + * @return if the string is not containing exact one label + */ + public static boolean isNotContainingExactOneLabel(String str, String label) { + String[] argSplit = str.split(REGEX_WHITESPACE); + long labelCnt = Arrays.stream(argSplit).filter(a -> a.equals(label)).count(); + return labelCnt != 1; + } + + /** + * Returns if the string matches the regular expression. + * @param str string + * @param regex regular expression + * @return if the string matches the regular expression + */ + public static boolean matches(String str, String regex) { + Matcher matcher = Pattern.compile(regex).matcher(str); + return matcher.matches(); + } + + /** + * Parses the arguments using the regular expression and returns them. + * Arguments in the regular expression has to in the group. + * + * @param args arguments + * @param regex regular expression + * @return parsed arguments + */ + public static String[] parseArguments(String args, String regex) { + String[] parsedArgs; + + Matcher matcher = Pattern.compile(regex).matcher(args); + matcher.find(); + parsedArgs = new String[matcher.groupCount()]; + for (int i = 1; i <= matcher.groupCount(); i++) { + parsedArgs[i - 1] = matcher.group(i); + } + + return parsedArgs; + } + + private static final String REGEX_WHITESPACE = "\\s"; +} diff --git a/src/main/java/nuke/command/DeadlineCommand.java b/src/main/java/nuke/command/DeadlineCommand.java new file mode 100644 index 000000000..76a4bdf9e --- /dev/null +++ b/src/main/java/nuke/command/DeadlineCommand.java @@ -0,0 +1,58 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class DeadlineCommand extends Command { + public static final String TYPE = "deadline"; + private static final String BY_LABEL = "/by"; + private static final String USAGE = TYPE + " ((name)) " + BY_LABEL + " ((deadline))"; + private static final String REGEX_NAME_EMPTY = BY_LABEL + "(.*)"; + private static final String REGEX_BY_EMPTY = "(.+)\\s" + BY_LABEL; + private static final String REGEX_PARSE = "(.+)\\s" + BY_LABEL + "\\s(.+)"; + + private String name; + private String by; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (args.isEmpty()) { + throwArgumentException(ERROR_MSG_NO_ARGS); + } else if (CommandParser.isNotContainingExactOneLabel(args, BY_LABEL)) { + throwArgumentException(ERROR_MSG_INVALID_NUMBER_OF_BY); + } else if (CommandParser.matches(args, REGEX_NAME_EMPTY)) { + throwArgumentException(ERROR_MSG_NAME_EMPTY); + } else if (CommandParser.matches(args, REGEX_BY_EMPTY)) { + throwArgumentException(ERROR_MSG_BY_EMPTY); + } + + String[] parsedArgs = CommandParser.parseArguments(args, REGEX_PARSE); + checkForbiddenCharacters(parsedArgs); + name = parsedArgs[0]; + by = parsedArgs[1]; + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) { + nuke.addDeadline(name, by); + } + + private static final String ERROR_MSG_NO_ARGS = + "Command 'deadline' should have two arguments, name and deadline of the task."; + private static final String ERROR_MSG_INVALID_NUMBER_OF_BY = + "Command 'deadline' should have one '/by' label for the deadline."; + private static final String ERROR_MSG_NAME_EMPTY = + "Command 'deadline' should have a string for name of the task."; + private static final String ERROR_MSG_BY_EMPTY = + "Command 'deadline' should have a string for deadline of the task."; +} diff --git a/src/main/java/nuke/command/DeleteCommand.java b/src/main/java/nuke/command/DeleteCommand.java new file mode 100644 index 000000000..115c3e773 --- /dev/null +++ b/src/main/java/nuke/command/DeleteCommand.java @@ -0,0 +1,49 @@ +package nuke.command; + +import nuke.Nuke; +import nuke.command.exception.InvalidCommandArgumentException; + +public class DeleteCommand extends Command { + public static final String TYPE = "delete"; + private static final String USAGE = TYPE + " ((index))"; + + private int index; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (CommandParser.isNotOneWord(args)) { + throwArgumentException(ERROR_MSG_INVALID_NUMBER_OF_ARGS); + } + // Input index starts with 1, logical index starts with 0. + try { + index = Integer.parseInt(args) - 1; + } catch (NumberFormatException e) { + throwArgumentException(ERROR_MSG_INDEX_NOT_INTEGER); + } + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) throws InvalidCommandArgumentException { + if(index < 0 || index >= nuke.countTasks()) { + throwArgumentException(ERROR_MSG_INDEX_INVALID_VALUE); + } + nuke.deleteTask(index); + } + + private static final String ERROR_MSG_INVALID_NUMBER_OF_ARGS = + "Command 'delete' should have one argument, index of the task."; + private static final String ERROR_MSG_INDEX_NOT_INTEGER = + "Command 'delete' should have a number for index of the task."; + private static final String ERROR_MSG_INDEX_INVALID_VALUE = + "The value of index is invalid. Please check the number of tasks."; +} diff --git a/src/main/java/nuke/command/EventCommand.java b/src/main/java/nuke/command/EventCommand.java new file mode 100644 index 000000000..755e245b9 --- /dev/null +++ b/src/main/java/nuke/command/EventCommand.java @@ -0,0 +1,72 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class EventCommand extends Command { + public static final String TYPE = "event"; + private static final String FROM_LABEL = "/from"; + private static final String TO_LABEL = "/to"; + private static final String USAGE = + TYPE + " ((name)) " + FROM_LABEL + " ((start)) " + TO_LABEL + " ((end))"; + private static final String REGEX_NAME_EMPTY = FROM_LABEL + "(.*)"; + private static final String REGEX_FROM_EMPTY = + "(.+)\\s" + FROM_LABEL + "\\s" + TO_LABEL + "(.*)"; + private static final String REGEX_TO_EMPTY = + "(.+)\\s" + FROM_LABEL + "\\s(.+)\\s" + TO_LABEL; + private static final String REGEX_PARSE = + "(.+)\\s" + FROM_LABEL + "\\s(.+)\\s" + TO_LABEL + "\\s(.+)"; + + private String name; + private String from; + private String to; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (args.isEmpty()) { + throwArgumentException(ERROR_MSG_NO_ARGS); + } else if (CommandParser.isNotContainingExactOneLabel(args, FROM_LABEL)) { + throwArgumentException(ERROR_MSG_INVALID_NUMBER_OF_FROM_TO); + } else if (CommandParser.isNotContainingExactOneLabel(args, TO_LABEL)) { + throwArgumentException(ERROR_MSG_INVALID_NUMBER_OF_FROM_TO); + } else if (CommandParser.matches(args, REGEX_NAME_EMPTY)) { + throwArgumentException(ERROR_MSG_NAME_EMPTY); + } else if (CommandParser.matches(args, REGEX_FROM_EMPTY)) { + throwArgumentException(ERROR_MSG_FROM_EMPTY); + } else if (CommandParser.matches(args, REGEX_TO_EMPTY)) { + throwArgumentException(ERROR_MSG_TO_EMPTY); + } + + String[] parsedArgs = CommandParser.parseArguments(args, REGEX_PARSE); + checkForbiddenCharacters(parsedArgs); + name = parsedArgs[0]; + from = parsedArgs[1]; + to = parsedArgs[2]; + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) { + nuke.addEvent(name, from, to); + } + + private static final String ERROR_MSG_NO_ARGS = + "Command 'event' should have three arguments, name, start and end of the event."; + private static final String ERROR_MSG_INVALID_NUMBER_OF_FROM_TO = + "Command 'event' should have one '/from' label and one '/to' label for the period of event."; + private static final String ERROR_MSG_NAME_EMPTY = + "Command 'event' should have a string for name of the task."; + private static final String ERROR_MSG_FROM_EMPTY = + "Command 'event' should have a string, after '/from' label, for start period of the task."; + private static final String ERROR_MSG_TO_EMPTY = + "Command 'event' should have a string, after '/to' label, for end period of the task."; +} diff --git a/src/main/java/nuke/command/FindCommand.java b/src/main/java/nuke/command/FindCommand.java new file mode 100644 index 000000000..66f33d2ac --- /dev/null +++ b/src/main/java/nuke/command/FindCommand.java @@ -0,0 +1,38 @@ +package nuke.command; + +import nuke.Nuke; +import nuke.command.exception.InvalidCommandArgumentException; + +public class FindCommand extends Command { + public static final String TYPE = "find"; + private static final String USAGE = TYPE + " ((keyword))"; + + private String keyword; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (args.isEmpty()) { + throwArgumentException(ERROR_MSG_NO_ARGS); + } + checkForbiddenCharacters(new String[]{args}); + keyword = args; + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) { + nuke.findTasks(keyword); + } + + private static final String ERROR_MSG_NO_ARGS = + "Command 'find' should have one argument, keyword for matching."; +} diff --git a/src/main/java/nuke/command/ListCommand.java b/src/main/java/nuke/command/ListCommand.java new file mode 100644 index 000000000..8327b153f --- /dev/null +++ b/src/main/java/nuke/command/ListCommand.java @@ -0,0 +1,34 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class ListCommand extends Command { + public static final String TYPE = "list"; + private static final String USAGE = TYPE; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (!args.isEmpty()) { + throwArgumentException(ERROR_MSG_TOO_MANY_ARGS); + } + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) { + nuke.listTask(); + } + + private static final String ERROR_MSG_TOO_MANY_ARGS = + "Command 'list' should have no arguments."; +} diff --git a/src/main/java/nuke/command/MarkCommand.java b/src/main/java/nuke/command/MarkCommand.java new file mode 100644 index 000000000..70368e147 --- /dev/null +++ b/src/main/java/nuke/command/MarkCommand.java @@ -0,0 +1,49 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class MarkCommand extends Command { + public static final String TYPE = "mark"; + private static final String USAGE = TYPE + " ((index))"; + + private int index; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (CommandParser.isNotOneWord(args)) { + throwArgumentException(ERROR_MSG_INVALID_NUMBER_OF_ARGS); + } + // Input index starts with 1, logical index starts with 0. + try { + index = Integer.parseInt(args) - 1; + } catch (NumberFormatException e) { + throwArgumentException(ERROR_MSG_INDEX_NOT_INTEGER); + } + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) throws InvalidCommandArgumentException { + if(index < 0 || index >= nuke.countTasks()) { + throwArgumentException(ERROR_MSG_INDEX_INVALID_VALUE); + } + nuke.markTask(index); + } + + private static final String ERROR_MSG_INVALID_NUMBER_OF_ARGS = + "Command 'mark' should have one argument, index of the task."; + private static final String ERROR_MSG_INDEX_NOT_INTEGER = + "Command 'mark' should have a number for index of the task."; + private static final String ERROR_MSG_INDEX_INVALID_VALUE = + "The value of index is invalid. Please check the number of tasks."; +} diff --git a/src/main/java/nuke/command/TodoCommand.java b/src/main/java/nuke/command/TodoCommand.java new file mode 100644 index 000000000..2defa8faa --- /dev/null +++ b/src/main/java/nuke/command/TodoCommand.java @@ -0,0 +1,38 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class TodoCommand extends Command { + public static final String TYPE = "todo"; + private static final String USAGE = TYPE + " ((name))"; + + private String name; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (args.isEmpty()) { + throwArgumentException(ERROR_MSG_NO_ARGS); + } + checkForbiddenCharacters(new String[]{args}); + name = args; + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) { + nuke.addTodo(name); + } + + private static final String ERROR_MSG_NO_ARGS = + "Command 'todo' should have one argument, name of the task."; +} diff --git a/src/main/java/nuke/command/UnmarkCommand.java b/src/main/java/nuke/command/UnmarkCommand.java new file mode 100644 index 000000000..708357439 --- /dev/null +++ b/src/main/java/nuke/command/UnmarkCommand.java @@ -0,0 +1,49 @@ +package nuke.command; + +import nuke.command.exception.InvalidCommandArgumentException; +import nuke.Nuke; + +public class UnmarkCommand extends Command { + public static final String TYPE = "unmark"; + private static final String USAGE = TYPE + " ((index))"; + + private int index; + + @Override + public void applyArguments(String args) throws InvalidCommandArgumentException { + if (CommandParser.isNotOneWord(args)) { + throwArgumentException(ERROR_MSG_INVALID_NUMBER_OF_ARGS); + } + // Input index starts with 1, logical index starts with 0. + try { + index = Integer.parseInt(args) - 1; + } catch (NumberFormatException e) { + throwArgumentException(ERROR_MSG_INDEX_NOT_INTEGER); + } + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected String getUsage() { + return USAGE; + } + + @Override + public void run(Nuke nuke) throws InvalidCommandArgumentException { + if(index < 0 || index >= nuke.countTasks()) { + throwArgumentException(ERROR_MSG_INDEX_INVALID_VALUE); + } + nuke.unmarkTask(index); + } + + private static final String ERROR_MSG_INVALID_NUMBER_OF_ARGS = + "Command 'unmark' should have one argument, index of the task."; + private static final String ERROR_MSG_INDEX_NOT_INTEGER = + "Command 'unmark' should have a number for index of the task."; + private static final String ERROR_MSG_INDEX_INVALID_VALUE = + "The value of index is invalid. Please check the number of tasks."; +} diff --git a/src/main/java/nuke/command/exception/CommandException.java b/src/main/java/nuke/command/exception/CommandException.java new file mode 100644 index 000000000..762e6185e --- /dev/null +++ b/src/main/java/nuke/command/exception/CommandException.java @@ -0,0 +1,6 @@ +package nuke.command.exception; + +import nuke.NukeException; + +public class CommandException extends NukeException { +} diff --git a/src/main/java/nuke/command/exception/InvalidCommandArgumentException.java b/src/main/java/nuke/command/exception/InvalidCommandArgumentException.java new file mode 100644 index 000000000..7b35a166f --- /dev/null +++ b/src/main/java/nuke/command/exception/InvalidCommandArgumentException.java @@ -0,0 +1,11 @@ +package nuke.command.exception; + +public class InvalidCommandArgumentException extends CommandException { + public String reason; + public String correctUsage; + + public InvalidCommandArgumentException(String reason, String correctUsage) { + this.reason = reason; + this.correctUsage = correctUsage; + } +} diff --git a/src/main/java/nuke/command/exception/InvalidCommandTypeException.java b/src/main/java/nuke/command/exception/InvalidCommandTypeException.java new file mode 100644 index 000000000..a3177afb4 --- /dev/null +++ b/src/main/java/nuke/command/exception/InvalidCommandTypeException.java @@ -0,0 +1,9 @@ +package nuke.command.exception; + +public class InvalidCommandTypeException extends CommandException { + public String type; + + public InvalidCommandTypeException(String type) { + this.type = type; + } +} diff --git a/src/main/java/nuke/storage/Storage.java b/src/main/java/nuke/storage/Storage.java new file mode 100644 index 000000000..484e549a8 --- /dev/null +++ b/src/main/java/nuke/storage/Storage.java @@ -0,0 +1,93 @@ +package nuke.storage; + +import nuke.storage.exception.TaskFileCopyException; +import nuke.storage.exception.TaskLoadException; +import nuke.storage.exception.TaskParseException; +import nuke.storage.exception.TaskSaveException; +import nuke.task.Task; +import nuke.task.TaskParser; + +import java.io.File; +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.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * Represents storage of Nuke. + */ +public class Storage { + private static final String DIR_DATA = "data"; + private static final String FILENAME_TASKS = "nuke.txt"; + private static final String FILENAME_TASKS_BACKUP = "nuke_old.txt"; + + /** + * Loads saved tasks from the designated file. + * + * @return list of saved tasks + * @throws TaskLoadException if loading the tasks fails + * @throws TaskFileCopyException if both loading the tasks and backing up the file fails + */ + public ArrayList loadTasks() + throws TaskLoadException, TaskFileCopyException { + ArrayList loadedTasks = new ArrayList<>(); + + Path currentRelativePath = Paths.get(""); + Path currentDir = currentRelativePath.toAbsolutePath(); + Path filePath = currentDir.resolve(DIR_DATA).resolve(FILENAME_TASKS); + + File file = new File(filePath.toString()); + if (!file.exists()) { + return loadedTasks; + } + try { + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + Task task = TaskParser.parseTask(line); + loadedTasks.add(task); + } + } catch (IOException | TaskParseException e) { + Path oldFilePath = currentDir.resolve(DIR_DATA).resolve(FILENAME_TASKS_BACKUP); + try { + Files.copy(filePath, oldFilePath, StandardCopyOption.REPLACE_EXISTING); + throw new TaskLoadException(FILENAME_TASKS_BACKUP); + } catch (IOException ex) { + throw new TaskFileCopyException(FILENAME_TASKS); + } + } + return loadedTasks; + } + + /** + * Saves the tasks to the designated file. + * + * @param formattedTasks tasks formatted to save. + * @throws TaskSaveException if saving the tasks fails + */ + public void saveTasks(String[] formattedTasks) throws TaskSaveException { + Path currentRelativePath = Paths.get(""); + Path currentDir = currentRelativePath.toAbsolutePath(); + Path dirPath = currentDir.resolve(DIR_DATA); + Path filePath = currentDir.resolve(DIR_DATA).resolve(FILENAME_TASKS); + + File dir = new File(dirPath.toString()); + File file = new File(filePath.toString()); + try { + dir.mkdir(); + file.createNewFile(); + try (FileWriter writer = new FileWriter(file)) { + for (String formattedTask: formattedTasks) { + writer.write(formattedTask); + writer.write('\n'); + } + } + } catch (IOException e) { + throw new TaskSaveException(); + } + } +} diff --git a/src/main/java/nuke/storage/exception/StorageException.java b/src/main/java/nuke/storage/exception/StorageException.java new file mode 100644 index 000000000..35c37696e --- /dev/null +++ b/src/main/java/nuke/storage/exception/StorageException.java @@ -0,0 +1,6 @@ +package nuke.storage.exception; + +import nuke.NukeException; + +public class StorageException extends NukeException { +} diff --git a/src/main/java/nuke/storage/exception/TaskFileCopyException.java b/src/main/java/nuke/storage/exception/TaskFileCopyException.java new file mode 100644 index 000000000..fee4fc556 --- /dev/null +++ b/src/main/java/nuke/storage/exception/TaskFileCopyException.java @@ -0,0 +1,9 @@ +package nuke.storage.exception; + +public class TaskFileCopyException extends StorageException { + public String filePath; + + public TaskFileCopyException(String filePath) { + this.filePath = filePath; + } +} diff --git a/src/main/java/nuke/storage/exception/TaskLoadException.java b/src/main/java/nuke/storage/exception/TaskLoadException.java new file mode 100644 index 000000000..e3d8d7814 --- /dev/null +++ b/src/main/java/nuke/storage/exception/TaskLoadException.java @@ -0,0 +1,9 @@ +package nuke.storage.exception; + +public class TaskLoadException extends StorageException { + public String backupFilePath; + + public TaskLoadException(String backupFilePath) { + this.backupFilePath = backupFilePath; + } +} diff --git a/src/main/java/nuke/storage/exception/TaskParseException.java b/src/main/java/nuke/storage/exception/TaskParseException.java new file mode 100644 index 000000000..a555b999a --- /dev/null +++ b/src/main/java/nuke/storage/exception/TaskParseException.java @@ -0,0 +1,4 @@ +package nuke.storage.exception; + +public class TaskParseException extends StorageException { +} diff --git a/src/main/java/nuke/storage/exception/TaskSaveException.java b/src/main/java/nuke/storage/exception/TaskSaveException.java new file mode 100644 index 000000000..88436f783 --- /dev/null +++ b/src/main/java/nuke/storage/exception/TaskSaveException.java @@ -0,0 +1,5 @@ +package nuke.storage.exception; + +public class TaskSaveException extends StorageException { + public String[] tasks; +} diff --git a/src/main/java/nuke/task/Deadline.java b/src/main/java/nuke/task/Deadline.java new file mode 100644 index 000000000..dcab78ced --- /dev/null +++ b/src/main/java/nuke/task/Deadline.java @@ -0,0 +1,59 @@ +package nuke.task; + +import nuke.NukeDateTime; + +import static nuke.task.TaskParser.TASK_FORMAT_SEPARATOR; + +/** + * Represents a task that has a deadline. + */ +public class Deadline extends Task { + private NukeDateTime by; + + /** + * Constructs a task that has a deadline, + * with the name and the deadline. + * + * @param name name of the task. + * @param by deadline of the task. + */ + public Deadline(String name, String by) { + super(name); + setBy(by); + } + + /** + * Returns deadline of the task. + * + * @return deadline of the task + */ + public String getBy() { + return by.toString(); + } + + /** + * Sets deadline of the task. + * + * @param by deadline of the task + */ + public void setBy(String by) { + this.by = new NukeDateTime(by); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String toString() { + return String.format("%s (by: %s)", super.toString(), getBy()); + } + + @Override + public String formatData() { + return super.formatData() + TASK_FORMAT_SEPARATOR + getBy(); + } + + public static final String TYPE = "D"; +} diff --git a/src/main/java/nuke/task/Event.java b/src/main/java/nuke/task/Event.java new file mode 100644 index 000000000..4f8449e82 --- /dev/null +++ b/src/main/java/nuke/task/Event.java @@ -0,0 +1,82 @@ +package nuke.task; + +import nuke.NukeDateTime; + +import static nuke.task.TaskParser.TASK_FORMAT_SEPARATOR; + +/** + * Represents an event that has the start and the end. + */ +public class Event extends Task { + private NukeDateTime from; + private NukeDateTime to; + + /** + * Constructs an event with the name, the start period, and the end period. + * + * @param name name of the task. + * @param from start period of the task. + * @param to end period of the task. + */ + public Event(String name, String from, String to) { + super(name); + setFrom(from); + setTo(to); + } + + /** + * Returns start period of the event. + * + * @return start period of the event + */ + public String getFrom() { + return from.toString(); + } + + /** + * Sets start period of the event. + * + * @param from start period of the event + */ + public void setFrom(String from) { + this.from = new NukeDateTime(from); + } + + /** + * Returns end period of the event. + * + * @return end period of the event + */ + public String getTo() { + return to.toString(); + } + + /** + * Sets end period of the event. + * + * @param to end period of the event + */ + public void setTo(String to) { + this.to = new NukeDateTime(to); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String toString() { + return String.format("%s (from: %s to: %s)", + super.toString(), getFrom(), getTo()); + } + + @Override + public String formatData() { + return super.formatData() + TASK_FORMAT_SEPARATOR + + getFrom() + TASK_FORMAT_SEPARATOR + + getTo(); + } + + public static final String TYPE = "E"; +} diff --git a/src/main/java/nuke/task/Task.java b/src/main/java/nuke/task/Task.java new file mode 100644 index 000000000..9312df0bb --- /dev/null +++ b/src/main/java/nuke/task/Task.java @@ -0,0 +1,90 @@ +package nuke.task; + +import static nuke.task.TaskParser.TASK_FORMAT_MARKED; +import static nuke.task.TaskParser.TASK_FORMAT_SEPARATOR; +import static nuke.task.TaskParser.TASK_FORMAT_UNMARKED; + +/** + * Represents a task. + */ +public abstract class Task { + private String name; + private boolean isDone; + + /** + * Constructs a task with the name. + * + * @param name name of the task + */ + public Task(String name) { + setName(name); + setDone(false); + } + + /** + * Returns name of the task. + * + * @return name of the task + */ + public String getName() { + return name; + } + + /** + * Sets name of the task. + * + * @param name name of the task + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns condition of the task if it is done or not. + * + * @return condition of the task + */ + public boolean isDone() { + return isDone; + } + + /** + * Sets condition of the task as done or not. + * + * @param done condition of the task + */ + public void setDone(boolean done) { + this.isDone = done; + } + + /** + * Returns type of the task. + * + * @return type of the task + */ + public abstract String getType(); + + /** + * Returns string representations of the task. + * + * @return string representations of the task + */ + @Override + public String toString() { + String mark = isDone()? "X" : " "; + return String.format("[%s][%s] %s", getType(), mark, getName()); + } + + /** + * Returns the task in form of formatted manner, + * which is used to save in file. + * + * @return the task in form of formatted manner. + */ + public String formatData() { + String mark = isDone()? TASK_FORMAT_MARKED : TASK_FORMAT_UNMARKED; + return getType() + TASK_FORMAT_SEPARATOR + + mark + TASK_FORMAT_SEPARATOR + + getName(); + } +} diff --git a/src/main/java/nuke/task/TaskList.java b/src/main/java/nuke/task/TaskList.java new file mode 100644 index 000000000..f17bbc14c --- /dev/null +++ b/src/main/java/nuke/task/TaskList.java @@ -0,0 +1,106 @@ +package nuke.task; + +import java.util.ArrayList; + +/** + * Represents a list of tasks. + */ +public class TaskList { + private final ArrayList TASKS; + + /** + * Constructs an empty list of tasks. + */ + public TaskList() { + this(new ArrayList<>()); + } + + /** + * Constructs a list of tasks with all elements in {@code tasks}. + * + * @param tasks a list of tasks + */ + public TaskList(ArrayList tasks) { + this.TASKS = tasks; + } + + /** + * Adds the task in the list. + * + * @param task task to add + */ + public void add(Task task) { + TASKS.add(task); + } + + /** + * Marks the task of the index as done. + * + * @param idx index of the task to be marked done + */ + public String mark(int idx) { + Task task = TASKS.get(idx); + task.setDone(true); + return task.toString(); + } + + /** + * Marks the task of the index as not done. + * + * @param idx index of the task to be marked not done + */ + public String unmark(int idx) { + Task task = TASKS.get(idx); + task.setDone(false); + return task.toString(); + } + + /** + * Deletes the task of the index. + * + * @param idx index of the task to be deleted + */ + public String delete(int idx) { + Task task = TASKS.remove(idx); + return task.toString(); + } + + public String[] find(String keyword) { + return TASKS.stream() + .filter(task -> task.getName().contains(keyword)) + .map(Task::toString) + .toArray(String[]::new); + } + + /** + * Returns the size of the list. + * + * @return the size of the list + */ + public int size() { + return TASKS.size(); + } + + /** + * Returns string representations of all tasks. + * + * @return all tasks in form of {@link String}[]. + */ + public String[] getTasks() { + return TASKS.stream() + .map(Task::toString) + .toArray(String[]::new); + } + + /** + * Returns all tasks in form of formatted manner, + * which is used to save in file. + * + * @return all tasks in form of formatted manner. + */ + public String[] getFormattedTasks() { + return TASKS.stream() + .map(Task::formatData) + .toArray(String[]::new); + } +} diff --git a/src/main/java/nuke/task/TaskParser.java b/src/main/java/nuke/task/TaskParser.java new file mode 100644 index 000000000..f2d1d4037 --- /dev/null +++ b/src/main/java/nuke/task/TaskParser.java @@ -0,0 +1,65 @@ +package nuke.task; + +import nuke.storage.exception.TaskParseException; + +import java.util.Arrays; + +/** + * Parser that parses formatted tasks in the save file. + */ +public class TaskParser { + public static final String TASK_FORMAT_SEPARATOR = " / "; + public static final String TASK_FORMAT_MARKED = "1"; + public static final String TASK_FORMAT_UNMARKED = "0"; + + /** + * Parses a line of string into {@link Task} and returns it. + * + * @param line string of a formatted task + * @return task; result of parsing + * @throws TaskParseException if parse fails + */ + public static Task parseTask(String line) throws TaskParseException { + String[] words = line.split(TASK_FORMAT_SEPARATOR); + if (words.length < 3) { + throw new TaskParseException(); + } + + String type = words[0]; + boolean mark = parseMark(words[1]); + String[] args = Arrays.copyOfRange(words, 2, words.length); + + Task task = parseArgs(type, args); + task.setDone(mark); + return task; + } + + private static boolean parseMark(String mark) throws TaskParseException { + switch (mark) { + case TASK_FORMAT_MARKED: + return true; + case TASK_FORMAT_UNMARKED: + return false; + default: + throw new TaskParseException(); + } + } + + private static Task parseArgs(String type, String[] args) throws TaskParseException { + Task task; + switch (type) { + case Todo.TYPE: + task = new Todo(args[0]); + break; + case Deadline.TYPE: + task = new Deadline(args[0], args[1]); + break; + case Event.TYPE: + task = new Event(args[0], args[1], args[2]); + break; + default: + throw new TaskParseException(); + } + return task; + } +} diff --git a/src/main/java/nuke/task/Todo.java b/src/main/java/nuke/task/Todo.java new file mode 100644 index 000000000..db6d813a0 --- /dev/null +++ b/src/main/java/nuke/task/Todo.java @@ -0,0 +1,23 @@ +package nuke.task; + +/** + * Represents a task to do. + */ +public class Todo extends Task { + + /** + * Constructs a task to do with the name. + * + * @param name name of the task to do. + */ + public Todo(String name) { + super(name); + } + + @Override + public String getType() { + return TYPE; + } + + public static final String TYPE = "T"; +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 657e74f6e..03a831c9a 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,7 +1,217 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + _.-^^---....,,-- + _-- --_ +< >) +| | + \._ _./ + ```--. . , ; .--''' + | | | + .-=|| | |=-. + `-=#$%&%$#=-' + | ; :| + _____.,-#%&$@%#&#~,._____ + +[@] Hello! I'm Nuke. +[@] What can I do for you? + +[@] Wrong input; Command 'bye' should have no arguments. +[@] Usage: bye + +[@] Wrong input; Command 'list' should have no arguments. +[@] Usage: list + +[@] There are no tasks in your list. + +[@] Wrong input; Command 'todo' should have one argument, name of the task. +[@] Usage: todo ((name)) + +[@] Wrong input; Command 'todo' should not contain '/' in arguments. +[@] Usage: todo ((name)) + +[@] Got it. I've added this task: + [T][ ] read book +[@] Now you have 1 task in the list. + +[@] Wrong input; Command 'deadline' should have two arguments, name and deadline of the task. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Wrong input; Command 'deadline' should have a string for name of the task. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Wrong input; Command 'deadline' should have a string for name of the task. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Wrong input; Command 'deadline' should have one '/by' label for the deadline. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Wrong input; Command 'deadline' should have a string for deadline of the task. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Wrong input; Command 'deadline' should not contain '/' in arguments. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Wrong input; Command 'deadline' should not contain '/' in arguments. +[@] Usage: deadline ((name)) /by ((deadline)) + +[@] Got it. I've added this task: + [D][ ] return book (by: June 6th) +[@] Now you have 2 tasks in the list. + +[@] Wrong input; Command 'event' should have three arguments, name, start and end of the event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have a string for name of the task. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have a string for name of the task. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have a string for name of the task. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have one '/from' label and one '/to' label for the period of event. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have a string, after '/from' label, for start period of the task. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should have a string, after '/to' label, for end period of the task. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should not contain '/' in arguments. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should not contain '/' in arguments. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Wrong input; Command 'event' should not contain '/' in arguments. +[@] Usage: event ((name)) /from ((start)) /to ((end)) + +[@] Got it. I've added this task: + [E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +[@] Now you have 3 tasks in the list. + +[@] Wrong input; Command 'mark' should have one argument, index of the task. +[@] Usage: mark ((index)) + +[@] Wrong input; The value of index is invalid. Please check the number of tasks. +[@] Usage: mark ((index)) + +[@] Wrong input; Command 'mark' should have a number for index of the task. +[@] Usage: mark ((index)) + +[@] Wrong input; The value of index is invalid. Please check the number of tasks. +[@] Usage: mark ((index)) + +[@] Nice! I've marked this task as done: + [T][X] read book + +[@] Nice! I've marked this task as done: + [E][X] project meeting (from: Aug 6th 2pm to: 4pm) + +[@] Here are the tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) +3.[E][X] project meeting (from: Aug 6th 2pm to: 4pm) + +[@] Wrong input; Command 'unmark' should have one argument, index of the task. +[@] Usage: unmark ((index)) + +[@] Wrong input; Command 'unmark' should have a number for index of the task. +[@] Usage: unmark ((index)) + +[@] Wrong input; The value of index is invalid. Please check the number of tasks. +[@] Usage: unmark ((index)) + +[@] Wrong input; The value of index is invalid. Please check the number of tasks. +[@] Usage: unmark ((index)) + +[@] OK, I've marked this task as not done yet: + [E][ ] project meeting (from: Aug 6th 2pm to: 4pm) + +[@] Here are the tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) +3.[E][ ] project meeting (from: Aug 6th 2pm to: 4pm) + +[@] Got it. I've added this task: + [T][ ] delete this +[@] Now you have 4 tasks in the list. + +[@] Here are the tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) +3.[E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +4.[T][ ] delete this + +[@] Wrong input; Command 'delete' should have one argument, index of the task. +[@] Usage: delete ((index)) + +[@] Wrong input; Command 'delete' should have a number for index of the task. +[@] Usage: delete ((index)) + +[@] Wrong input; The value of index is invalid. Please check the number of tasks. +[@] Usage: delete ((index)) + +[@] Wrong input; The value of index is invalid. Please check the number of tasks. +[@] Usage: delete ((index)) + +[@] Noted. I've removed this task: + [T][ ] delete this +[@] Now you have 3 tasks in the list. + +[@] Here are the tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) +3.[E][ ] project meeting (from: Aug 6th 2pm to: 4pm) + +[@] Wrong input; Command 'find' should have one argument, keyword for matching. +[@] Usage: find ((keyword)) + +[@] Wrong input; Command 'find' should not contain '/' in arguments. +[@] Usage: find ((keyword)) + +[@] Here are the matching tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) + +[@] Got it. I've added this task: + [D][ ] dtTest (by: Sep 21 2023, 16:24) +[@] Now you have 4 tasks in the list. + +[@] Got it. I've added this task: + [E][ ] dtTest (from: Sep 22 2023, 00:00 to: Sep 23 2023, 00:00) +[@] Now you have 5 tasks in the list. + +[@] Here are the tasks in your list: +1.[T][X] read book +2.[D][ ] return book (by: June 6th) +3.[E][ ] project meeting (from: Aug 6th 2pm to: 4pm) +4.[D][ ] dtTest (by: Sep 21 2023, 16:24) +5.[E][ ] dtTest (from: Sep 22 2023, 00:00 to: Sep 23 2023, 00:00) + +[@] Bye. Hope to see you again soon! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29bb..f95cec371 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1,61 @@ +bye no +list no +list +todo +todo read/book +todo read book +deadline +deadline /by +deadline /by June 6th +deadline return book +deadline return book /by +deadline return/book /by June 6th +deadline return book /by June/6th +deadline return book /by June 6th +event +event /to +event /to 4pm +event /from +event /from Aug 6th 2pm +event /from /to +event /from Aug 6th 2pm /to +event /from Aug 6th 2pm /to 4pm +event project meeting +event project meeting /to +event project meeting /to 4pm +event project meeting /from +event project meeting /from Aug 6th 2pm +event project meeting /from /to +event project meeting /from Aug 6th 2pm /to +event project/meeting /from Aug 6th 2pm /to 4pm +event project meeting /from Aug/6th/2pm /to 4pm +event project meeting /from Aug 6th 2pm /to 4pm/ +event project meeting /from Aug 6th 2pm /to 4pm +mark +mark 0 +mark hi +mark 4 +mark 1 +mark 3 +list +unmark +unmark hi +unmark 0 +unmark 4 +unmark 3 +list +todo delete this +list +delete +delete hi +delete 0 +delete 5 +delete 4 +list +find +find bo/ok +find book +deadline dtTest /by 2023-09-21 16:24 +event dtTest /from 09-22 /to 09-23 +list +bye diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 087374464..6c7944ae8 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -6,8 +6,15 @@ if not exist ..\bin mkdir ..\bin REM delete output from previous run if exist ACTUAL.TXT del ACTUAL.TXT +if exist data\nuke.txt del data\nuke.txt + REM compile the code into the bin folder -javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\nuke\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\nuke\command\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\nuke\command\exception\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\nuke\storage\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\nuke\storage\exception\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\nuke\task\*.java IF ERRORLEVEL 1 ( echo ********** BUILD FAILURE ********** exit /b 1 @@ -15,7 +22,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 nuke.Nuke < input.txt > ACTUAL.TXT REM compare the output to the expected output FC ACTUAL.TXT EXPECTED.TXT