diff --git a/.gitignore b/.gitignore index f69985ef..09363aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ bin/ /text-ui-test/ACTUAL.txt text-ui-test/EXPECTED-UNIX.TXT +/text-ui-test/runtest.sh \ No newline at end of file diff --git a/Saves/Tasks b/Saves/Tasks new file mode 100644 index 00000000..bc921e35 --- /dev/null +++ b/Saves/Tasks @@ -0,0 +1,2 @@ +DEADLINE//0//assignment //today +TODO//0//save diff --git a/Saves/Tasks_edited b/Saves/Tasks_edited new file mode 100644 index 00000000..e5aad31d --- /dev/null +++ b/Saves/Tasks_edited @@ -0,0 +1,4 @@ +T | 1 | todo +D | 0 | deadline | sometime +E | 0 | event | now +T | 0 | kkk diff --git a/docs/README.md b/docs/README.md index 8077118e..5ea15fe1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,28 +2,116 @@ ## Features -### Feature-ABC +### Task List -Description of the feature. - -### Feature-XYZ - -Description of the feature. +There are three types of tasks that can be added to +the task list: todo, deadline and event. The task list +can track their status of completion and find tasks +through keywords. It will be stored locally and opened +automatically upon next launch of software. ## Usage -### `Keyword` - Describe action +### `Add a task` -Describe the action and its outcome. +Add a todo, deadline or event to your task list. Example of usage: -`keyword (optional arguments)` +`todo homework` Expected outcome: -Description of the outcome. +``` +added: [T][] homework +``` + +Example of usage: + +`deadline assignment /by: today` + +Expected outcome: ``` -expected output +added: [D][] assignment (by: today) +``` + +Example of usage: + +`event celebration /at: Saturday` + +Expected outcome: + +``` +added: [E][] celebration (at: Saturday) +``` + +### `List all tasks` + +List all the tasks on the task list now. + +Example of usage: + +`list` + +Expected outcome: +``` +There are 2 tasks on the list now: +1.[D][] assignment (by: today) +2.[E][] celebration (at: Saturday) +``` + +### `Mark a task` + +Mark a task as done, or unmark it as not done. + +Example of usage: +`mark 1` + +Expected outcome: + +``` +Task 1 is marked as done~ +[D][X] assignment (by: today) +``` + +Example of usage: + +`unmark 1` + +Expected outcome: +``` +Task 1 is unmarked as not done~ +[D][] assignment (by: today) +``` + +### `Delete a task` + +Delete a task from the task list. + +Example of usage: + +`delete 2` + +Expected outcome: + +``` +Task 2: [E][] celebration (at: Saturday) is removed~ +There are 1 tasks on the list now: +1.[D][] assignment (by: today) +``` + +### `Find a task` + +Find a task whose description or time contains +a given keyword or keyphrase. + +Example of usage: + +`find assignment` + +Expected outcome: ``` +1 matching tasks are found: +1.[D][] assignment (by: today) +``` \ No newline at end of file diff --git a/src/main/java/Command.java b/src/main/java/Command.java new file mode 100644 index 00000000..72c76511 --- /dev/null +++ b/src/main/java/Command.java @@ -0,0 +1,39 @@ +public abstract class Command { + private CommandType commandType; + + /** + * get the type of the command + * + * @return type of command + */ + public CommandType getCommandType() { + return commandType; + } + + /** + * get the task that is described in the command, if there is any + * + * @return default is an empty Todo object + */ + public Task getTask() { + return new Todo("", false); + } + + /** + * get the index of the task to be modified, if there is any + * + * @return default is 0 + */ + public int getIndex() { + return 0; + } + + /** + * get key associated with task, if there is any + * + * @return default is null + */ + public String getKey() { + return ""; + } +} diff --git a/src/main/java/CommandBye.java b/src/main/java/CommandBye.java new file mode 100644 index 00000000..2f043453 --- /dev/null +++ b/src/main/java/CommandBye.java @@ -0,0 +1,8 @@ +public class CommandBye extends Command { + private final CommandType commandType = CommandType.BYE; + + @Override + public CommandType getCommandType() { + return commandType; + } +} diff --git a/src/main/java/CommandType.java b/src/main/java/CommandType.java new file mode 100644 index 00000000..6658951b --- /dev/null +++ b/src/main/java/CommandType.java @@ -0,0 +1,6 @@ +/** + * This enum contains the possible type of commands + */ +public enum CommandType { + TASK, LIST, ERROR, NULL, DELETE, MARK, UNMARK, BYE, FIND +} diff --git a/src/main/java/CorruptedDataFileException.java b/src/main/java/CorruptedDataFileException.java new file mode 100644 index 00000000..92be0bf3 --- /dev/null +++ b/src/main/java/CorruptedDataFileException.java @@ -0,0 +1,6 @@ +/** + * CorruptedDataFileException occurs when Duke fails to load the saved file due to + * error in the saved file format + */ +public class CorruptedDataFileException extends Exception { +} diff --git a/src/main/java/Deadline.java b/src/main/java/Deadline.java new file mode 100644 index 00000000..2b0276bb --- /dev/null +++ b/src/main/java/Deadline.java @@ -0,0 +1,64 @@ +public class Deadline extends Task implements FormatChecker { + private static final String DEADLINE_MARK = "[D]"; + private static final TaskType taskType = TaskType.DEADLINE; + private final String time; + + /** + * Default constructor for Deadline instance + * + * @param description description of Deadline + * @param time time of deadline + */ + public Deadline(String description, String time) { + super(description); + this.time = time; + } + + /** + * Construct a deadline object given additional status of completion + * + * @param description description of deadline + * @param isDone whether the deadline is marked as done + * @param time time of the deadline + */ + public Deadline(String description, Boolean isDone, String time) { + this.description = description; + this.isDone = isDone; + this.time = time; + } + + /** + * get the icon that indicates whether the deadline is marked + * + * @return string of icon for mark status + */ + @Override + public String getStatusIcon() { + return DEADLINE_MARK + super.getStatusIcon(); + } + + /** + * get type of task, which is deadline + * + * @return type of task + */ + @Override + public TaskType getTaskType() { + return taskType; + } + + /** + * get the time of deadline + * + * @return time of deadline + */ + @Override + public String getTime() { + return this.time; + } + + @Override + public String toString() { + return String.format("%s %s(by: %s)", this.getStatusIcon(), description, this.time); + } +} diff --git a/src/main/java/DeadlineCommand.java b/src/main/java/DeadlineCommand.java new file mode 100644 index 00000000..196d07ac --- /dev/null +++ b/src/main/java/DeadlineCommand.java @@ -0,0 +1,45 @@ +public class DeadlineCommand extends Command implements FormatChecker { + + private final String description; + private final String time; + private final static CommandType commandType = CommandType.TASK; + + /** + * Construct a deadline given an almost unprocessed input command + * + * @param complexString a complex string that contains both deadline description and time + * @throws WrongCommandFormatException when input command is not in correct format + */ + public DeadlineCommand(String complexString) throws WrongCommandFormatException { + + try { + int identifierIndex = FormatChecker.findIdentifierIndex(DEADLINE_IDENTIFIER, complexString); + this.description = complexString.substring(0, identifierIndex); + FormatChecker.checkNullString(this.description); + this.time = complexString.substring(identifierIndex + TIME_IDENTIFIER_LENGTH); + FormatChecker.checkNullString(this.time); + } catch (WrongCommandFormatException e) { + throw new WrongCommandFormatException(); + } + } + + /** + * get the command type of current deadline command + * + * @return command type of deadline command, DEADLINE + */ + @Override + public CommandType getCommandType() { + return commandType; + } + + /** + * get the task that can be constructed from current deadline command + * + * @return the deadline task from current command + */ + @Override + public Task getTask() { + return new Deadline(description, time); + } +} diff --git a/src/main/java/DeleteCommand.java b/src/main/java/DeleteCommand.java new file mode 100644 index 00000000..aa5d35c5 --- /dev/null +++ b/src/main/java/DeleteCommand.java @@ -0,0 +1,34 @@ +public class DeleteCommand extends Command implements FormatChecker { + private final int index; + private final static CommandType commandType = CommandType.DELETE; + + /** + * Construct a delete command including task to delete + * + * @param index the index of task to delete + */ + public DeleteCommand(int index) { + this.index = index; + } + + /** + * get the index of task to delete + * + * @return the index of task to delete + */ + @Override + public int getIndex() { + return this.index; + } + + /** + * get the type of current delete command + * + * @return command type of delete command, DELETE + */ + @Override + public CommandType getCommandType() { + return commandType; + } +} + diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java index 5d313334..a3e63d3e 100644 --- a/src/main/java/Duke.java +++ b/src/main/java/Duke.java @@ -1,10 +1,62 @@ +import java.io.IOException; +import java.util.ArrayList; +import java.io.File; + public class Duke { + + private static final String DEFAULT_FILE_PATH = System.getProperty("user.dir") + + File.separator + "Saves" + File.separator +"Tasks"; + private final UI ui; + private TaskList tasks = new TaskList(new ArrayList<>()); + private final Storage storage; + + /** + * construct a duke class that contain key functions of software + * + * @param filePath path of saved data file + */ + public Duke(String filePath) { + ui = new UI(); + storage = new Storage(filePath); + try { + tasks = new TaskList(storage.loadSave()); + } catch (IOException e) { + System.out.format("Create new saved file failed!%n"); + } + } + + /** + * This method illustrates the process of how duke run with each line of input + */ + public void run() { + ui.greet(); + boolean isExit = false; + + while (!isExit) { + String fullCommand = ""; + try { + fullCommand = ui.readCommand(); + ui.separateLine(); + Command c = Parser.parseCommand(fullCommand); + isExit = tasks.execute(c); + storage.writeSave(tasks.getTasks()); + if (isExit) { + ui.bye(); + } + } catch (UnknownCommandException e) { + System.out.format("Exception: Unknown command%n" + + "%s is not a valid command%n", fullCommand.split(" ")[0]); + } catch (NullCommandException e) { + System.out.format("Exception: Null command%n" + + "No command is entered%n"); + } finally { + ui.separateLine(); + } + } + + } + public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); + new Duke(DEFAULT_FILE_PATH).run(); } } diff --git a/src/main/java/ErrorCommand.java b/src/main/java/ErrorCommand.java new file mode 100644 index 00000000..27ee0264 --- /dev/null +++ b/src/main/java/ErrorCommand.java @@ -0,0 +1,13 @@ +public class ErrorCommand extends Command{ + private final static CommandType commandType = CommandType.ERROR; + + /** + * get the type of current error command + * + * @return command type of error command, ERROR + */ + @Override + public CommandType getCommandType() { + return commandType; + } +} diff --git a/src/main/java/Event.java b/src/main/java/Event.java new file mode 100644 index 00000000..8bea9d0f --- /dev/null +++ b/src/main/java/Event.java @@ -0,0 +1,64 @@ +public class Event extends Task implements FormatChecker { + private static final String EVENT_MARK = "[E]"; + private static final TaskType taskType = TaskType.EVENT; + private final String time; + + /** + * Default constructor for Event instance + * + * @param description description of Event + * @param time time of event + */ + public Event(String description, String time) { + super(description); + this.time = time; + } + + /** + * Construct a deadline object given additional status of completion + * + * @param description description of event + * @param isDone whether the event is marked as done + * @param time time of the event + */ + public Event(String description, Boolean isDone, String time) { + this.description = description; + this.isDone = isDone; + this.time = time; + } + + /** + * get the icon that indicates whether the event is marked + * + * @return string of icon for mark status + */ + @Override + public String getStatusIcon() { + return EVENT_MARK + super.getStatusIcon(); + } + + /** + * get type of task, which is event + * + * @return type of task + */ + @Override + public TaskType getTaskType() { + return taskType; + } + + /** + * get the time of event + * + * @return time of event + */ + @Override + public String getTime() { + return this.time; + } + + @Override + public String toString() { + return String.format("%s %s(at: %s)", this.getStatusIcon(), description, this.time); + } +} diff --git a/src/main/java/EventCommand.java b/src/main/java/EventCommand.java new file mode 100644 index 00000000..fc1c180d --- /dev/null +++ b/src/main/java/EventCommand.java @@ -0,0 +1,45 @@ +public class EventCommand extends Command implements FormatChecker { + + private final String description; + private final String time; + private final static CommandType commandType = CommandType.TASK; + + /** + * Construct an event given an almost unprocessed input command + * + * @param complexString a complex string that contains both event description and time + * @throws WrongCommandFormatException when input command is not in correct format + */ + public EventCommand(String complexString) throws WrongCommandFormatException { + try { + int identifierIndex = FormatChecker.findIdentifierIndex(EVENT_IDENTIFIER, complexString); + this.description = complexString.substring(0, identifierIndex); + FormatChecker.checkNullString(this.description); + this.time = complexString.substring(identifierIndex + TIME_IDENTIFIER_LENGTH); + FormatChecker.checkNullString(this.time); + } catch (WrongCommandFormatException e) { + throw new WrongCommandFormatException(); + } + } + + /** + * get the task that can be constructed from current event command + * + * @return the event task from current command + */ + @Override + public Task getTask() { + return new Event(description, time); + } + + /** + * get the command type of current event command + * + * @return command type of event command, EVENT + */ + @Override + public CommandType getCommandType() { + return commandType; + } + +} diff --git a/src/main/java/ExcessArgumentException.java b/src/main/java/ExcessArgumentException.java new file mode 100644 index 00000000..8233ae02 --- /dev/null +++ b/src/main/java/ExcessArgumentException.java @@ -0,0 +1,6 @@ +/** + * an ExcessArgumentException occurs when an input of command contains excess arguments + * for the current command type + */ +public class ExcessArgumentException extends Exception { +} diff --git a/src/main/java/FileIO.java b/src/main/java/FileIO.java new file mode 100644 index 00000000..09fe75c5 --- /dev/null +++ b/src/main/java/FileIO.java @@ -0,0 +1,78 @@ +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Scanner; +import java.io.FileWriter; +import java.io.IOException; + +public interface FileIO { + + /** + * Load the saved data file from a given path + * + * @param filePath path name of the saved data file + * @return the saved tasks in ArrayList format + * @throws CorruptedDataFileException when data file is not in correct format + * @throws FileNotFoundException when data file is not found in given path + */ + static ArrayList loadFile(String filePath) throws CorruptedDataFileException, FileNotFoundException { + File file = new File(filePath); + Scanner sc = new Scanner(file); + + ArrayList tasks = new ArrayList<>(); + while (sc.hasNext()) { + String line = sc.nextLine(); + + String[] args = line.split("//"); + for (String i : args) { + System.out.print(i + ' '); + } + System.out.println(); + + TaskType taskType = TaskType.valueOf(args[0]); + String description = args[2]; + String time = (args.length > 3 ? args[3] : ""); + Boolean isDone = (Integer.parseInt(args[1]) == 1); + + switch (taskType) { + case TODO: + tasks.add(new Todo(description, isDone)); + break; + case DEADLINE: + tasks.add(new Deadline(description, isDone, time)); + break; + case EVENT: + tasks.add(new Event(description, isDone, time)); + break; + default: + throw new CorruptedDataFileException(); + } + } + + return tasks; + } + + /** + * Write the current tasks in Duke to saved data file + * + * @param Tasks the current tasks in Duke + * @param filePath the path name of the saved data file + * @throws IOException when exception in writing files occurs + */ + static void writeFile(ArrayList Tasks, String filePath) throws IOException { + FileWriter fw = new FileWriter(filePath); + StringBuilder taskFileFormat = new StringBuilder(); + + for (Task task : Tasks) { + taskFileFormat.append(task.getTaskType()).append("//") + .append(task.getIsDone() ? "1" : "0").append("//").append(task.getDescription()); + if (!task.getTaskType().equals(TaskType.TODO)) { + taskFileFormat.append("//").append(task.getTime()); + } + taskFileFormat.append(String.format("%n")); + } + fw.write(taskFileFormat.toString()); + fw.close(); + } + +} diff --git a/src/main/java/FindCommand.java b/src/main/java/FindCommand.java new file mode 100644 index 00000000..33e730c0 --- /dev/null +++ b/src/main/java/FindCommand.java @@ -0,0 +1,23 @@ +public class FindCommand extends Command { + private final CommandType commandType = CommandType.FIND; + private final String key; + + public FindCommand(String key) { + this.key = key; + } + + /** + * get the keyword to search for in the find command + * + * @return the keyword + */ + @Override + public String getKey() { + return key; + } + + @Override + public CommandType getCommandType() { + return commandType; + } +} diff --git a/src/main/java/FormatChecker.java b/src/main/java/FormatChecker.java new file mode 100644 index 00000000..f467f714 --- /dev/null +++ b/src/main/java/FormatChecker.java @@ -0,0 +1,49 @@ +public interface FormatChecker { + + int TIME_IDENTIFIER_LENGTH = 5; + int MODIFICATION_COMMAND_LENGTH = 2; + String DEADLINE_IDENTIFIER = "/by: "; + String EVENT_IDENTIFIER = "/at: "; + + /** + * Check if the given string is empty + * + * @param string the string to check + * @throws WrongCommandFormatException when the given string is empty + */ + static void checkNullString(String string) throws WrongCommandFormatException { + if (string.replaceAll("\\s+", "").equals("")) { + throw new WrongCommandFormatException(); + } + } + + /** + * Find the location of time identifier in a given event or deadline command input + * + * @param identifier the identifier of event or deadline + * @param complexString the command input of event or deadline + * @return the index of time identifier in the command input string + * @throws WrongCommandFormatException when time identifier is missing in the command input + */ + static int findIdentifierIndex(String identifier, String complexString) throws WrongCommandFormatException { + int separatorIndex = complexString.indexOf(identifier); + if (separatorIndex == -1) { + throw new WrongCommandFormatException(); + } + return separatorIndex; + } + + /** + * check if there are excess arguments in commands that modifies the task list, which are + * delete, mark and unmark command + * + * @param cmd the full string of command input + * @throws ExcessArgumentException when there are excess arguments in the command input + */ + static void checkExcessArgument(String cmd) throws ExcessArgumentException { + if (cmd.split(" ").length > MODIFICATION_COMMAND_LENGTH) { + throw new ExcessArgumentException(); + } + } + +} diff --git a/src/main/java/ListCommand.java b/src/main/java/ListCommand.java new file mode 100644 index 00000000..df4c8ab9 --- /dev/null +++ b/src/main/java/ListCommand.java @@ -0,0 +1,14 @@ +public class ListCommand extends Command { + private final CommandType commandType = CommandType.LIST; + + /** + * get the type of current list command + * + * @return command type of list command, LIST + */ + @Override + public CommandType getCommandType() { + return this.commandType; + } + +} diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 00000000..d2ffd5b4 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: Duke + diff --git a/src/main/java/MarkCommand.java b/src/main/java/MarkCommand.java new file mode 100644 index 00000000..c3209556 --- /dev/null +++ b/src/main/java/MarkCommand.java @@ -0,0 +1,28 @@ +public class MarkCommand extends Command { + private final int index; //true for mark, false for unmark + private final static CommandType commandType = CommandType.MARK; + + public MarkCommand(int index) { + this.index = index; + } + + /** + * get the index of the task to mark as done + * + * @return the index of the task + */ + @Override + public int getIndex() { + return index; + } + + /** + * get the type of current mark command + * + * @return command type of mark command, MARK + */ + @Override + public CommandType getCommandType() { + return commandType; + } +} diff --git a/src/main/java/NullCommandException.java b/src/main/java/NullCommandException.java new file mode 100644 index 00000000..5b93418e --- /dev/null +++ b/src/main/java/NullCommandException.java @@ -0,0 +1,5 @@ +/** + * NullArgumentException occurs when the input command is empty + */ +public class NullCommandException extends Exception { +} diff --git a/src/main/java/Parser.java b/src/main/java/Parser.java new file mode 100644 index 00000000..dea48ece --- /dev/null +++ b/src/main/java/Parser.java @@ -0,0 +1,113 @@ +public class Parser implements FormatChecker { + + private static final int TODO_LENGTH = 5; + private static final int DEADLINE_LENGTH = 9; + private static final int EVENT_LENGTH = 6; + private static final int FIND_LENGTH = 4; + private static final String COMMAND_SEPARATOR = " "; + private static final String COMMAND_LIST = "list"; + private static final String COMMAND_MARK = "mark"; + private static final String COMMAND_UNMARK = "unmark"; + private static final String COMMAND_TODO = "todo"; + private static final String COMMAND_DEADLINE = "deadline"; + private static final String COMMAND_EVENT = "event"; + private static final String COMMAND_DELETE = "delete"; + private static final String COMMAND_NULL = ""; + private static final String COMMAND_BYE = "bye"; + private static final String COMMAND_FIND = "find"; + + /** + * Parse the full command input into different type of command + * + * @param input the string of full command input + * @return a command that contains information of input + * @throws NullCommandException when the input is a null command + * @throws UnknownCommandException when the input is not any known command + */ + public static Command parseCommand(String input) throws NullCommandException, UnknownCommandException { + String[] args = input.split(COMMAND_SEPARATOR); + + switch (args[0]) { + case COMMAND_LIST: + return new ListCommand(); + case COMMAND_MARK: + case COMMAND_UNMARK: + try { + String commandType = args[0]; + int index = Integer.parseInt(args[1]) - 1; + FormatChecker.checkExcessArgument(input); + if (commandType.equalsIgnoreCase(COMMAND_MARK)) { + return new MarkCommand(index); + } + return new UnmarkCommand(index); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException | ExcessArgumentException e) { + System.out.format("Exception: Wrong command Format%n" + + "Try the command in correct format: mark %n"); + } + break; + case COMMAND_TODO: + try { + if (input.length() == TODO_LENGTH - 1) { + throw new WrongCommandFormatException(); + } + return new TodoCommand(input.substring(TODO_LENGTH)); + } catch (WrongCommandFormatException e) { + System.out.format("Exception: Wrong Command Format%n" + + "Try the correct command format: " + + "todo %n"); + } + break; + case COMMAND_DEADLINE: + try { + if (input.length() == DEADLINE_LENGTH - 1) { + throw new WrongCommandFormatException(); + } + return new DeadlineCommand(input.substring(DEADLINE_LENGTH)); + } catch (WrongCommandFormatException e) { + System.out.format("Exception: Wrong Command Format%n" + + "Try the correct command format: " + + "deadline /by: