+ * '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