diff --git a/README.md b/README.md
index 8715d4d9..0f2208ab 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Duke project template
+# duke.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.
@@ -13,7 +13,7 @@ Prerequisites: JDK 11, update Intellij to the most recent version.
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:
+3. After that, locate the `src/main/java/duke.Duke.java` file, right-click it, and choose `Run duke.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
____ _
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..d4929950
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,46 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'checkstyle'
+ id 'com.github.johnrengelman.shadow' version '5.1.0'
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.0'
+ testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.0'
+}
+
+test {
+ useJUnitPlatform()
+
+ testLogging {
+ events "passed", "skipped", "failed"
+
+ showExceptions true
+ exceptionFormat "full"
+ showCauses true
+ showStackTraces true
+ showStandardStreams = false
+ }
+}
+
+application {
+ mainClassName = "duke.Duke"
+}
+
+shadowJar {
+ archiveBaseName = "DingMan"
+ archiveClassifier = null
+}
+
+checkstyle {
+ toolVersion = '8.29'
+}
+
+run{
+ standardInput = System.in
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f3d88b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..b7c8c5db
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..2fe81a7d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..62bd9b9c
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,103 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java
deleted file mode 100644
index 5d313334..00000000
--- 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/duke/Duke.java b/src/main/java/duke/Duke.java
new file mode 100644
index 00000000..ca6eba92
--- /dev/null
+++ b/src/main/java/duke/Duke.java
@@ -0,0 +1,60 @@
+package duke;
+
+import duke.app.Parser;
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.command.AbstractCommand;
+import duke.exceptions.FileProcessError;
+import duke.exceptions.InvalidUserInputException;
+import duke.storage.Storage;
+
+import java.io.IOException;
+
+public class Duke {
+
+ private Storage storage;
+ private TaskList tasks;
+ private final Ui ui;
+ private final Parser parser;
+
+ public Duke(String filePath) {
+ parser = new Parser();
+ ui = new Ui();
+ try {
+ storage = new Storage(filePath);
+ tasks = new TaskList(storage.loadFile());
+ assert tasks.getTaskList() != null;
+ } catch (FileProcessError e) {
+ ui.showErrorMessage(e.getMessage());
+ }
+ }
+
+ public void run() throws IOException {
+ ui.printGreeting();
+ boolean isExit = false;
+ while (!isExit) {
+ String inputCommand = ui.readCommand();
+ ui.showLine();
+ parser.setUserInput(inputCommand);
+ try {
+ tasks.setParsedUserInputs(parser.parseAndCheckInput());
+ AbstractCommand command = parser.parseCommand();
+ command.execute(tasks, storage, ui);
+ isExit = command.isExit();
+ } catch (InvalidUserInputException e) {
+ ui.showErrorMessage(e.getMessage());
+ }
+ ui.showLine();
+ }
+ }
+
+ public static void main(String[] args) {
+ try {
+ Duke app = new Duke("data/tasks.txt");
+ app.run();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
diff --git a/src/main/java/duke/app/Parser.java b/src/main/java/duke/app/Parser.java
new file mode 100644
index 00000000..9ed34815
--- /dev/null
+++ b/src/main/java/duke/app/Parser.java
@@ -0,0 +1,149 @@
+package duke.app;
+
+import duke.command.*;
+import duke.exceptions.InvalidUserInputException;
+import duke.task.TaskFactory;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Check and parse the user input and throw exceptions if the input is invalid.
+ */
+public class Parser {
+
+ public enum validActions {
+ ADD, LIST, DONE, DELETE, BYE, REMIND, FIND, TAG
+ }
+
+ private String userInput;
+ private Map parsedInput;
+
+ public void setUserInput(String userInput) {
+ this.userInput = userInput;
+ parsedInput = new HashMap<>();
+ }
+
+ public static LocalDateTime parseDateTime(String time) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+ return LocalDateTime.parse(time, formatter);
+ }
+
+ /**
+ * format LocalDateTime object using a user-friendly format for printing
+ * @param time, a LocalDateTime object
+ * @return formatted time, String
+ */
+ public static String formatDatetimeToString(LocalDateTime time) {
+ return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(time);
+ }
+
+ /**parse user command to be passed to commandFactory
+ * @return a command object created by CommandFactory
+ * @throws InvalidUserInputException if command is not valid.
+ */
+ public AbstractCommand parseCommand() throws InvalidUserInputException {
+ validActions validAction = getAction();
+ return CommandFactory.create(validAction);
+ }
+
+ public validActions getAction() {
+ return validActions.valueOf(parsedInput.get("Action"));
+ }
+
+ /**
+ * Parse and check user input
+ * - classify into different commands, i.e. list, add, date, delete, bye, done
+ * - for add task, the first word must be event/todo/deadline.
+ * - for all the task, description cannot be empty
+ * - for event/deadline, time of format "yyyy-mm-dd hh:mm" must be provided
+ * @return parsed user input
+ * @throws InvalidUserInputException if the command is invalid
+ */
+ public Map parseAndCheckInput() throws InvalidUserInputException {
+ try {
+ parseValidAction();
+ parseUserInput();
+ assert parsedInput.get("Action") != null;
+ return parsedInput;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new InvalidUserInputException("Oops the task description/time is empty");
+ }
+ }
+
+ private void parseValidAction() throws InvalidUserInputException {
+ String action = userInput.split(" ")[0].trim().toUpperCase();
+ for (validActions a : validActions.values()) {
+ if (a.name().equals(action)) {
+ parsedInput.put("Action", action);
+ }
+ }
+ for (TaskFactory.validTaskTypes type : TaskFactory.validTaskTypes.values()) {
+ if (type.name().equals(action)) {
+ parsedInput.put("Action", validActions.ADD.name());
+ parsedInput.put("TaskType", action);
+ }
+ }
+ if (!parsedInput.containsKey("Action")) {
+ throw new InvalidUserInputException("Command invalid, please enter a valid command");
+ }
+ }
+
+ private void parseUserInput() throws InvalidUserInputException {
+
+ if (!validActions.LIST.name().equals(parsedInput.get("Action")) &&
+ !validActions.BYE.name().equals(parsedInput.get("Action"))) {
+ parseNameOrIndex();
+ }
+ if (parsedInput.containsKey("TaskType") && !parsedInput.get("TaskType").equals("TODO")) {
+ String time = userInput.split("/")[1].substring(2).trim();
+ parseTime(time);
+ }
+ if (parsedInput.get("Action").equals(validActions.TAG.name())) {
+ parseTag();
+ }
+ }
+
+ private void parseNameOrIndex() throws InvalidUserInputException {
+ String nameOrIndex = userInput.split("/")[0].split(" ", 2)[1].trim().split("#")[0].trim();
+ if ("".equals(nameOrIndex)) {
+ throw new InvalidUserInputException("Oops the task description/index is empty");
+ }
+ checkValidIndex(nameOrIndex);
+ parsedInput.put("NameOrIndex", nameOrIndex);
+ }
+
+ private void checkValidIndex(String nameOrIndex) throws InvalidUserInputException {
+ String[] actionWithIndex = new String[]{validActions.DONE.name(), validActions.DELETE.name(),
+ validActions.REMIND.name(), validActions.TAG.name()};
+ for (String action: actionWithIndex) {
+ if (action.equals(parsedInput.get("Action"))) {
+ try {
+ Integer.parseInt(nameOrIndex);
+ } catch (NumberFormatException e) {
+ throw new InvalidUserInputException("done/delete/remind/tag command must follow by an integer index");
+ }
+ }
+ }
+ }
+
+ private void parseTime(String time) throws InvalidUserInputException {
+ try {
+ Parser.parseDateTime(time);
+ parsedInput.put("Time", time);
+ } catch (Exception e) {
+ throw new InvalidUserInputException("DateTime format should be yyyy-MM-dd HH:mm");
+ }
+ }
+
+ private void parseTag() throws InvalidUserInputException {
+ if (!userInput.contains("#")) {
+ throw new InvalidUserInputException("use #tagname to indicate a tag, e.g. #fun");
+ }
+ String tag = userInput.split("#")[1].trim();
+ parsedInput.put("Tag", tag);
+ }
+}
diff --git a/src/main/java/duke/app/TaskList.java b/src/main/java/duke/app/TaskList.java
new file mode 100644
index 00000000..c7316f2b
--- /dev/null
+++ b/src/main/java/duke/app/TaskList.java
@@ -0,0 +1,122 @@
+package duke.app;
+
+import duke.task.Task;
+import duke.task.TaskFactory;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represent a list of tasks including todo, event and deadline.
+ */
+public class TaskList {
+
+ private List taskList;
+ private Map parsedUserInputs;
+
+ public TaskList(List taskList) {
+ if (taskList != null) {
+ this.taskList = taskList;
+ } else {
+ this.taskList = new ArrayList<>();
+ }
+ parsedUserInputs = new HashMap<>();
+ }
+
+ public TaskList() {
+ this(new ArrayList<>());
+ }
+
+ public List getTaskList() {
+ return taskList;
+ }
+
+ public void setParsedUserInputs(Map parsedUserInputs) {
+ this.parsedUserInputs.clear();
+ this.parsedUserInputs = parsedUserInputs;
+ }
+
+ public void printTask(List tasks) {
+ for (int i=0; i< tasks.size(); i++){
+ System.out.println(i + 1 + "." + tasks.get(i));
+ }
+ }
+
+ public String deleteTask() {
+ int idx = Integer.parseInt(parsedUserInputs.get("NameOrIndex")) - 1;
+ Task removedTask = taskList.remove(idx);
+ return removedTask.toString();
+ }
+
+ /**
+ * mark the task as done.
+ * @return string representation of the task marked as done
+ */
+ public String changeTaskStatus() {
+
+ int idx = Integer.parseInt(parsedUserInputs.get("NameOrIndex")) - 1;
+ Task taskDone = taskList.get(idx);
+ taskDone.setDone(true);
+ return taskDone.toString();
+ }
+
+ public List findMatchTasks() {
+ List matchedTasks = new ArrayList<>();
+ String keyword = parsedUserInputs.get("NameOrIndex").toLowerCase();
+
+ for (Task task: taskList) {
+ if (task.getTaskName().toLowerCase().contains(keyword)) {
+ matchedTasks.add(task);
+ }
+ }
+ return matchedTasks;
+ }
+
+ public Task setTagForOneTask() {
+ String tag = parsedUserInputs.get("Tag").toLowerCase();
+ int index = Integer.parseInt(parsedUserInputs.get("NameOrIndex")) - 1;
+ Task task = taskList.get(index);
+ task.setTag(tag);
+ return task;
+ }
+
+ /**
+ * find out a list of tasks due before a specified date.
+ * @return list of tasks
+ */
+ public List getTasksReminder() {
+ List criticalTasks = new ArrayList<>();
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime remindDate = now.plusDays(Integer.parseInt(parsedUserInputs.get("NameOrIndex")));
+
+ for (Task task: taskList) {
+ if (task.getTime() != null) {
+ LocalDateTime taskDate = Parser.parseDateTime(task.getTime());
+ if (taskDate.isAfter(now) && taskDate.isBefore(remindDate)) {
+ criticalTasks.add(task);
+ }
+ }
+ }
+ return criticalTasks;
+ }
+
+ /** Add a new task to the list of tasks, only unique task will be added
+ * @return task, Task
+ */
+ public boolean addTask() {
+ Task newTask = TaskFactory.creatTask(parsedUserInputs);
+ return addUniqueTask(newTask);
+ }
+
+ private boolean addUniqueTask(Task newTask) {
+ if (!taskList.contains(newTask)) {
+ taskList.add(newTask);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/duke/app/Ui.java b/src/main/java/duke/app/Ui.java
new file mode 100644
index 00000000..fc72d3d7
--- /dev/null
+++ b/src/main/java/duke/app/Ui.java
@@ -0,0 +1,95 @@
+package duke.app;
+
+import java.util.Scanner;
+
+public class Ui {
+
+ private final Scanner scanner;
+
+ public Ui() {
+ scanner = new Scanner(System.in);
+ }
+
+ public String readCommand() {
+ return scanner.nextLine();
+ }
+
+ public void printAddTask(boolean success, String newTask) {
+ if (!success) {
+ System.out.println("The same task has been added previously, please enter another task");
+ } else {
+ System.out.println("Got it. I've added this task: " + " " + newTask);
+ }
+ }
+
+ public void printTotalNumber(int size) {
+ System.out.println("Now you have " + size + " tasks in the list.");
+ }
+
+ public void printDeleteTask(String removedTask) {
+ System.out.println("Noted. I've removed this task: \n" + removedTask);
+ }
+
+ public void printIndexWrongMessage() {
+ System.out.println("Oops the index is wrong, please enter a valid index");
+ }
+
+ public void printGreeting() {
+ String logo = " ____ _ \n"
+ + "| _ \\ _ _| | _____ \n"
+ + "| | | | | | | |/ / _ \\\n"
+ + "| |_| | |_| | < __/\n"
+ + "|____/ \\__,_|_|\\_\\___|\n";
+ System.out.println("Hello from\n" + logo);
+
+ String greeting = "Hey this is Blanc, welcome to your universe\n"
+ + "I will provide everything you need:))\n";
+ String instruction = "Please follow the instructions below\n"
+ + "1. \"list\": print a list of tasks\n"
+ + "2. Mark the task as done using \"done index\" (e.g. done 2)\n"
+ + "3. add task using \"todo\" \"deadline\" \"event\" task description /by or /at datetime \n"
+ + " Note: for datetime (necessary for deadline and event), follow the format yyyy-MM-dd HH:mm\n"
+ + " e.g. -> todo reading book;\n"
+ + " -> deadline homework submission /by 2021-11-17 18:00\n"
+ + " -> event project meeting /at 2021-11-22 12:00\n"
+ + "4. Delete task using \"delete index\" (e.g. delete 2): delete the 2nd task on the list\n"
+ + "5. Find a list of task using a keyword, \"find keyword\" (e.g. find book)\n"
+ + "6. Remind the list of tasks due in a specified number of days, \"remind number\" (e.g. remind 7)\n"
+ + "7. \"bye\": end the program\n"
+ +"Please type any command to start: \n";
+ System.out.println(greeting);
+ System.out.println(instruction);
+ }
+
+ public void printTask() {
+ System.out.println("Here are the tasks in your list:");
+ }
+
+ public void printDoneMessage(String taskDone) {
+ System.out.println("Nice! I've marked this task as done: \n" + taskDone);
+ }
+
+ public void showLine() {
+ System.out.println("__________________________________");
+ }
+
+ public void printExitMessage() {
+ System.out.println("Bye. Have a nice day!");
+ }
+
+ public void showErrorMessage(String message) {
+ System.out.println(message);
+ }
+
+ public void showRemindCommandMessage() {
+ System.out.println("Here is the list of the tasks due soon: ");
+ }
+
+ public void printFindCommand() {
+ System.out.println("Here are the matching tasks in your list:");
+ }
+
+ public void printTagCommand(String task) {
+ System.out.println("Got it, I've tagged this task:" + task);
+ }
+}
diff --git a/src/main/java/duke/command/AbstractCommand.java b/src/main/java/duke/command/AbstractCommand.java
new file mode 100644
index 00000000..e8783734
--- /dev/null
+++ b/src/main/java/duke/command/AbstractCommand.java
@@ -0,0 +1,12 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.storage.Storage;
+
+public abstract class AbstractCommand {
+
+ public abstract void execute(TaskList taskList, Storage storage, Ui ui);
+
+ public boolean isExit() {return false;}
+}
diff --git a/src/main/java/duke/command/AddCommand.java b/src/main/java/duke/command/AddCommand.java
new file mode 100644
index 00000000..13e4eed4
--- /dev/null
+++ b/src/main/java/duke/command/AddCommand.java
@@ -0,0 +1,29 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.exceptions.FileProcessError;
+import duke.storage.Storage;
+
+/**
+ * Add a new Task to TaskList object and save the records in the text file.
+ */
+public class AddCommand extends AbstractCommand{
+
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+
+ boolean success = taskList.addTask();
+ int taskSize = taskList.getTaskList().size();
+ String newTaskStr = taskList.getTaskList().get(taskSize - 1).toString();
+ ui.printAddTask(success, newTaskStr);
+ if (success) {
+ try {
+ storage.saveToFile(taskList.getTaskList());
+ ui.printTotalNumber(taskSize);
+ } catch (FileProcessError e) {
+ ui.showErrorMessage(e.getMessage());
+ }
+ }
+ }
+}
diff --git a/src/main/java/duke/command/CommandFactory.java b/src/main/java/duke/command/CommandFactory.java
new file mode 100644
index 00000000..70990b19
--- /dev/null
+++ b/src/main/java/duke/command/CommandFactory.java
@@ -0,0 +1,32 @@
+package duke.command;
+
+import duke.app.Parser;
+
+/**
+ * Initialise a command object based on specified user action, which includes list, done, delete, add, bye, date.
+ */
+public class CommandFactory {
+
+ public static AbstractCommand create(Parser.validActions validAction) {
+ switch (validAction) {
+ case LIST:
+ return new ListCommand();
+ case DONE:
+ return new DoneCommand();
+ case DELETE:
+ return new DeleteCommand();
+ case ADD:
+ return new AddCommand();
+ case BYE:
+ return new ExitCommand();
+ case REMIND:
+ return new ReminderCommand();
+ case FIND:
+ return new FindComment();
+ case TAG:
+ return new TagCommand();
+ default:
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/duke/command/DeleteCommand.java b/src/main/java/duke/command/DeleteCommand.java
new file mode 100644
index 00000000..fe7d2190
--- /dev/null
+++ b/src/main/java/duke/command/DeleteCommand.java
@@ -0,0 +1,22 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.exceptions.FileProcessError;
+import duke.storage.Storage;
+
+public class DeleteCommand extends AbstractCommand {
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ try {
+ String removedTask = taskList.deleteTask();
+ ui.printDeleteTask(removedTask);
+ ui.printTotalNumber(taskList.getTaskList().size());
+ storage.saveToFile(taskList.getTaskList());
+ } catch (IndexOutOfBoundsException e) {
+ ui.printIndexWrongMessage();
+ } catch (FileProcessError e) {
+ ui.showErrorMessage(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/duke/command/DoneCommand.java b/src/main/java/duke/command/DoneCommand.java
new file mode 100644
index 00000000..8b3a0104
--- /dev/null
+++ b/src/main/java/duke/command/DoneCommand.java
@@ -0,0 +1,24 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.exceptions.FileProcessError;
+import duke.storage.Storage;
+
+/**
+ * Change the specific task status to done.
+ */
+public class DoneCommand extends AbstractCommand{
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ try {
+ String taskDone = taskList.changeTaskStatus();
+ ui.printDoneMessage(taskDone);
+ storage.saveToFile(taskList.getTaskList());
+ } catch (NullPointerException | IndexOutOfBoundsException e) {
+ ui.printIndexWrongMessage();
+ } catch (FileProcessError e) {
+ ui.showErrorMessage(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/duke/command/ExitCommand.java b/src/main/java/duke/command/ExitCommand.java
new file mode 100644
index 00000000..baf60a68
--- /dev/null
+++ b/src/main/java/duke/command/ExitCommand.java
@@ -0,0 +1,17 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.storage.Storage;
+
+public class ExitCommand extends AbstractCommand{
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ ui.printExitMessage();
+ }
+
+ @Override
+ public boolean isExit() {
+ return true;
+ }
+}
diff --git a/src/main/java/duke/command/FindComment.java b/src/main/java/duke/command/FindComment.java
new file mode 100644
index 00000000..305582c7
--- /dev/null
+++ b/src/main/java/duke/command/FindComment.java
@@ -0,0 +1,18 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.storage.Storage;
+import duke.task.Task;
+import java.util.List;
+
+public class FindComment extends AbstractCommand{
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ ui.printFindCommand();
+ List matchTasks = taskList.findMatchTasks();
+ if (!matchTasks.isEmpty()) {
+ taskList.printTask(matchTasks);
+ }
+ }
+}
diff --git a/src/main/java/duke/command/ListCommand.java b/src/main/java/duke/command/ListCommand.java
new file mode 100644
index 00000000..07402944
--- /dev/null
+++ b/src/main/java/duke/command/ListCommand.java
@@ -0,0 +1,17 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.storage.Storage;
+
+/**
+ * Print all the tasks in the list
+ */
+public class ListCommand extends AbstractCommand{
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ ui.printTask();
+ taskList.printTask(taskList.getTaskList());
+ }
+}
+
diff --git a/src/main/java/duke/command/ReminderCommand.java b/src/main/java/duke/command/ReminderCommand.java
new file mode 100644
index 00000000..66267323
--- /dev/null
+++ b/src/main/java/duke/command/ReminderCommand.java
@@ -0,0 +1,21 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.storage.Storage;
+import duke.task.Task;
+
+import java.util.List;
+
+/**
+ * print all the tasks before a user-specified date
+ */
+public class ReminderCommand extends AbstractCommand{
+
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ List criticalTasks = taskList.getTasksReminder();
+ ui.showRemindCommandMessage();
+ taskList.printTask(criticalTasks);
+ }
+}
diff --git a/src/main/java/duke/command/TagCommand.java b/src/main/java/duke/command/TagCommand.java
new file mode 100644
index 00000000..34778dcb
--- /dev/null
+++ b/src/main/java/duke/command/TagCommand.java
@@ -0,0 +1,20 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.exceptions.FileProcessError;
+import duke.storage.Storage;
+import duke.task.Task;
+
+public class TagCommand extends AbstractCommand{
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ Task task = taskList.setTagForOneTask();
+ try {
+ storage.saveToFile(taskList.getTaskList());
+ ui.printTagCommand(task.toString());
+ } catch (FileProcessError e) {
+ ui.showErrorMessage(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/duke/exceptions/FileProcessError.java b/src/main/java/duke/exceptions/FileProcessError.java
new file mode 100644
index 00000000..b58de16e
--- /dev/null
+++ b/src/main/java/duke/exceptions/FileProcessError.java
@@ -0,0 +1,10 @@
+package duke.exceptions;
+
+/**
+ * Represent exceptions occur when writing/reading from the storage file.
+ */
+public class FileProcessError extends Exception{
+ public FileProcessError(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/duke/exceptions/InvalidStorageInput.java b/src/main/java/duke/exceptions/InvalidStorageInput.java
new file mode 100644
index 00000000..ccf80664
--- /dev/null
+++ b/src/main/java/duke/exceptions/InvalidStorageInput.java
@@ -0,0 +1,10 @@
+package duke.exceptions;
+
+/**
+ * represent exceptions related to data in the txt file.
+ */
+public class InvalidStorageInput extends Exception {
+ public InvalidStorageInput(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/duke/exceptions/InvalidUserInputException.java b/src/main/java/duke/exceptions/InvalidUserInputException.java
new file mode 100644
index 00000000..09c56f45
--- /dev/null
+++ b/src/main/java/duke/exceptions/InvalidUserInputException.java
@@ -0,0 +1,11 @@
+package duke.exceptions;
+
+/**
+ * Represent the exceptions related to user input
+ */
+public class InvalidUserInputException extends Exception {
+
+ public InvalidUserInputException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/duke/storage/FileDecoder.java b/src/main/java/duke/storage/FileDecoder.java
new file mode 100644
index 00000000..71681bd2
--- /dev/null
+++ b/src/main/java/duke/storage/FileDecoder.java
@@ -0,0 +1,136 @@
+package duke.storage;
+
+import duke.app.Parser;
+import duke.exceptions.InvalidStorageInput;
+import duke.task.Task;
+import duke.task.TaskFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Decode the text from file into a list of existing before running the app.
+ */
+public class FileDecoder {
+
+ /**decode the list of task from String to task object
+ * @param originalTasks, list of Strings read from the txt file
+ * @return list of tasks, list of task objects
+ */
+ public static List decodeTasks(List originalTasks) {
+ List tasks = new ArrayList<>();
+
+ for (String task: originalTasks) {
+ try {
+ Map parsedInput = decodeIndividualTask(task);
+ if (!parsedInput.isEmpty()) {
+ Task newTask = getNewTaskWithStatusTag(parsedInput);
+ tasks.add(newTask);
+ }
+ } catch (InvalidStorageInput e) {
+ System.out.println("Error occurred when decoding task\n"
+ + "reason:" + e.getMessage());
+ }
+ }
+ return tasks;
+ }
+
+ private static Task getNewTaskWithStatusTag(Map parsedInput) {
+ Task newTask = TaskFactory.creatTask(parsedInput);
+ setTaskStatus(parsedInput.get("TaskStatus"), newTask);
+ if (parsedInput.containsKey("Tag")) {
+ setTaskTag(parsedInput.get("Tag"), newTask);
+ }
+ return newTask;
+ }
+
+ private static void setTaskTag(String tag, Task newTask) {
+ newTask.setTag(tag);
+ }
+
+
+ private static void setTaskStatus(String status, Task newTask) {
+ boolean taskStatus = !"0".equals(status);
+ newTask.setDone(taskStatus);
+ }
+
+ private static Map decodeIndividualTask(String task) throws InvalidStorageInput {
+ Map parsedInputs = new HashMap<>();
+ String[] taskComponents = task.replace("|", ",").split(",");
+
+ checkValidTaskComponents(taskComponents);
+ parsedInputs.put("TaskType", taskComponents[0].trim());
+ parsedInputs.put("TaskStatus", taskComponents[1].trim());
+ parsedInputs.put("NameOrIndex", taskComponents[2].trim());
+
+ getDatetime(parsedInputs, taskComponents);
+ getTagIfPresent(parsedInputs, taskComponents);
+ return parsedInputs;
+ }
+
+ private static void getDatetime(Map parsedInputs, String[] taskComponents) throws InvalidStorageInput {
+ if ("EVENT".equals(parsedInputs.get("TaskType")) || "DEADLINE".equals(parsedInputs.get("TaskType"))) {
+ String time = taskComponents[3].trim();
+ checkValidTime(time);
+ parsedInputs.put("Time", time);
+ }
+ }
+
+ private static void checkValidTime(String time) throws InvalidStorageInput {
+ try {
+ Parser.parseDateTime(time);
+ } catch (Exception e) {
+ throw new InvalidStorageInput("DateTime format incorrect");
+ }
+ }
+
+ private static void checkValidTaskComponents(String[] taskComponents) throws InvalidStorageInput {
+ checkValidLength(taskComponents);
+ checkAndAddValidTaskType(taskComponents[0].trim());
+ checkValidTaskStatus(taskComponents[1].trim());
+ checkValidTaskName(taskComponents[2].trim());
+ }
+
+ private static void checkValidLength(String[] taskComponents) throws InvalidStorageInput {
+ int compulsoryComponents = 3;
+ if (taskComponents.length < compulsoryComponents) {
+ throw new InvalidStorageInput("Incomplete task components, must have type, description and status");
+ }
+
+ if ("EVENT".equals(taskComponents[0].trim()) || "DEADLINE".equals(taskComponents[0].trim())){
+ if (taskComponents.length < compulsoryComponents + 1) {
+ throw new InvalidStorageInput("Incomplete task components, must have time for event and deadline");
+ }
+ }
+ }
+
+ private static void checkAndAddValidTaskType(String taskType) throws InvalidStorageInput {
+ for (TaskFactory.validTaskTypes type : TaskFactory.validTaskTypes.values()) {
+ if (type.name().equals(taskType)) {
+ return;
+ }
+ }
+ throw new InvalidStorageInput("Wrong type of task in the file");
+ }
+
+ private static void checkValidTaskName(String taskName) throws InvalidStorageInput {
+ if (taskName == null || "".equals(taskName)) {
+ throw new InvalidStorageInput("Missing task description");
+ }
+ }
+
+ private static void checkValidTaskStatus(String taskStatus) throws InvalidStorageInput {
+ if (!"01".contains(taskStatus)) {
+ throw new InvalidStorageInput("wrong task status, must be 0 or 1");
+ }
+ }
+
+ private static void getTagIfPresent(Map parsedInputs, String[] taskComponents) {
+ String tagIfPresent = taskComponents[taskComponents.length - 1].trim();
+ if (tagIfPresent.contains("#") && !"".equals(tagIfPresent.split("#")[1].trim())) {
+ parsedInputs.put("Tag", tagIfPresent.split("#")[1].trim().toLowerCase());
+ }
+ }
+}
diff --git a/src/main/java/duke/storage/FileEncoder.java b/src/main/java/duke/storage/FileEncoder.java
new file mode 100644
index 00000000..f92f5a3c
--- /dev/null
+++ b/src/main/java/duke/storage/FileEncoder.java
@@ -0,0 +1,34 @@
+package duke.storage;
+
+import duke.task.Task;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Encode the tasklist object to a list of strings in order to be saved in a txt file
+ */
+public class FileEncoder {
+
+ public static List encodeTasks(List tasksToSave) {
+ List toSave = new ArrayList<>();
+ for (Task task: tasksToSave) {
+ toSave.add(encodeIndividual(task));
+ }
+ return toSave;
+ }
+
+ private static String encodeIndividual(Task task) {
+ StringBuilder taskString = new StringBuilder();
+ taskString.append(task.getTaskType()).append(" | ");
+ taskString.append(task.isDone() ? 1 : 0).append(" | ");
+ taskString.append(task.getTaskName());
+ if (task.getTime() != null) {
+ taskString.append(" | ").append(task.getTime());
+ }
+ if (task.getTag() != null) {
+ taskString.append(" | ").append(task.getTag());
+ }
+ return taskString.toString();
+ }
+}
diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java
new file mode 100644
index 00000000..8832eca2
--- /dev/null
+++ b/src/main/java/duke/storage/Storage.java
@@ -0,0 +1,66 @@
+package duke.storage;
+
+import duke.exceptions.FileProcessError;
+import duke.task.Task;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Perform necessary actions to save the list of tasks to txt file or read the existing
+ * records from a txt file.
+ */
+public class Storage {
+
+ public static final String DEFAULT_FILE_PATH = "./data/duke.txt";
+ public String filePath;
+
+ public Storage(String filePath) throws FileProcessError {
+ this.filePath = filePath;
+ try {
+ createFileIfNotExists();
+ } catch (IOException e) {
+ throw new FileProcessError("Can't create a new file based on file path defined");
+ }
+ }
+
+ public Storage() throws FileProcessError {
+ this(DEFAULT_FILE_PATH);
+ }
+
+ private void createFileIfNotExists() throws IOException {
+ File file = new File(filePath);
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ }
+
+ public void saveToFile(List tasks) throws FileProcessError {
+ List toSave = FileEncoder.encodeTasks(tasks);
+ try (FileWriter writer = new FileWriter(filePath)){
+ for(String item: toSave) {
+ writer.write(item + System.lineSeparator());
+ }
+ } catch (IOException e) {
+ throw new FileProcessError("Error occurred when saving to file");
+ }
+ }
+
+ public List loadFile() throws FileProcessError {
+ List originalTasks;
+ try (Scanner scanner = new Scanner(new File(filePath))) {
+ originalTasks = new ArrayList<>();
+ while (scanner.hasNext()) {
+ originalTasks.add(scanner.nextLine());
+ }
+ } catch (FileNotFoundException e) {
+ throw new FileProcessError("Error occurred when loading the file");
+ }
+ if (originalTasks.isEmpty()) {
+ return null;
+ } else {
+ return FileDecoder.decodeTasks(originalTasks);
+ }
+ }
+}
diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java
new file mode 100644
index 00000000..bd319e82
--- /dev/null
+++ b/src/main/java/duke/task/Deadline.java
@@ -0,0 +1,47 @@
+package duke.task;
+
+import duke.app.Parser;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+
+/**
+ * Represent a task with a specific deadline, represented by a dateTime object.
+ */
+public class Deadline extends Task {
+
+ private LocalDateTime by;
+
+ public Deadline(String taskName, String by) {
+ super(taskName);
+ this.by = Parser.parseDateTime(by);
+ }
+
+ public String getTaskType() { return "DEADLINE"; }
+
+ @Override
+ public String getTime() {
+ return by.toString().replace("T", " ");
+ }
+
+ @Override
+ public String toString() {
+ return "[D]" + super.toString() +
+ " (by: " + Parser.formatDatetimeToString(by) + ") " + getTag();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Deadline)) return false;
+ if (!super.equals(o)) return false;
+ Deadline deadline = (Deadline) o;
+ return getTime().equals(deadline.getTime());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), getTime());
+ }
+}
diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java
new file mode 100644
index 00000000..b6190d88
--- /dev/null
+++ b/src/main/java/duke/task/Event.java
@@ -0,0 +1,47 @@
+package duke.task;
+
+import duke.app.Parser;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * Represent a type of task with a specific time, represented by a dateTime object.
+ *
+ */
+public class Event extends Task {
+
+ private LocalDateTime at;
+
+ public Event(String taskName, String at) {
+ super(taskName);
+ this.at = Parser.parseDateTime(at);
+ }
+
+ public String getTaskType() { return "EVENT"; }
+
+ @Override
+ public String getTime() {
+ return at.toString().replace("T", " ");
+ }
+
+ @Override
+ public String toString() {
+ return "[E]" + super.toString() +
+ " (at: " + Parser.formatDatetimeToString(at) + ") " + getTag();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Event)) return false;
+ if (!super.equals(o)) return false;
+ Event event = (Event) o;
+ return Objects.equals(getTime(), event.getTime());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), getTime());
+ }
+}
diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java
new file mode 100644
index 00000000..6c1de54f
--- /dev/null
+++ b/src/main/java/duke/task/Task.java
@@ -0,0 +1,69 @@
+package duke.task;
+
+import java.util.Objects;
+
+public abstract class Task {
+
+ private final String taskName;
+ private boolean isDone;
+ private String tag;
+
+ public Task(String taskName) {
+ this.taskName = taskName;
+ this.isDone = false;
+ this.tag = null;
+ }
+
+ public Task(String taskName, boolean isDone, String tag) {
+ this.taskName = taskName;
+ this.isDone = isDone;
+ this.tag = tag;
+ }
+
+ public String getStatusIcon() {
+ return (isDone ? "X" : " "); // mark done task with X
+ }
+
+ public String getTaskName() {
+ return taskName;
+ }
+
+ public boolean isDone() {
+ return isDone;
+ }
+
+ public void setDone(boolean done) {
+ isDone = done;
+ }
+
+ public String getTag() {
+ return tag != null ? "#" + tag : "";
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public abstract String getTaskType();
+
+ public abstract String getTime();
+
+ @Override
+ public String toString() {
+
+ return "[" + getStatusIcon() + "] " + taskName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Task)) return false;
+ Task task = (Task) o;
+ return isDone() == task.isDone() && getTaskName().equals(task.getTaskName());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getTaskName(), isDone());
+ }
+}
diff --git a/src/main/java/duke/task/TaskFactory.java b/src/main/java/duke/task/TaskFactory.java
new file mode 100644
index 00000000..75b47f10
--- /dev/null
+++ b/src/main/java/duke/task/TaskFactory.java
@@ -0,0 +1,30 @@
+package duke.task;
+
+import java.util.Map;
+
+
+/**
+ * Initiate task objects based on specified task type
+ */
+public class TaskFactory {
+
+ public enum validTaskTypes {
+ TODO, DEADLINE, EVENT
+ }
+
+ public static Task creatTask(Map inputs) {
+
+ validTaskTypes taskType = validTaskTypes.valueOf(inputs.get("TaskType"));
+ switch (taskType) {
+ case TODO:
+ return new Todo(inputs.get("NameOrIndex"));
+ case DEADLINE:
+ return new Deadline(inputs.get("NameOrIndex"), inputs.get("Time"));
+ case EVENT:
+ return new Event(inputs.get("NameOrIndex"), inputs.get("Time"));
+ default:
+ System.out.println("Wrong type of task");
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java
new file mode 100644
index 00000000..f437b9fd
--- /dev/null
+++ b/src/main/java/duke/task/Todo.java
@@ -0,0 +1,20 @@
+package duke.task;
+
+public class Todo extends Task {
+
+ public Todo(String taskName) {
+ super(taskName);
+ }
+
+ public String getTaskType() { return "TODO"; }
+
+ @Override
+ public String getTime() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "[T]" + super.toString() + " " + getTag();
+ }
+}
diff --git a/src/test/java/duke/app/TaskListTest.java b/src/test/java/duke/app/TaskListTest.java
new file mode 100644
index 00000000..52464dc3
--- /dev/null
+++ b/src/test/java/duke/app/TaskListTest.java
@@ -0,0 +1,101 @@
+package duke.app;
+
+import duke.task.Deadline;
+import duke.task.Event;
+import duke.task.Task;
+import duke.task.Todo;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TaskListTest {
+
+ Map parsedUserInputs;
+ TaskList taskList;
+ List beforeTasks;
+
+ @BeforeEach
+ void setUp() {
+ parsedUserInputs = new HashMap<>();
+
+ beforeTasks = new ArrayList<>();
+ beforeTasks.add(new Deadline("return book", "2021-11-25 18:00"));
+ beforeTasks.add(new Todo("reading"));
+ beforeTasks.add(new Event("project meeting", "2021-11-22 15:00"));
+
+ taskList = new TaskList(beforeTasks);
+ }
+
+ @Test
+ void deleteTask_success() {
+ //note that the index is checked when parsing the input
+ parsedUserInputs.put("NameOrIndex", "2");
+ taskList.setParsedUserInputs(parsedUserInputs);
+ taskList.deleteTask();
+ beforeTasks.remove(1);
+ assertEquals(beforeTasks, taskList.getTaskList());
+ }
+
+ @Test
+ void changeTaskStatus_success() {
+ parsedUserInputs.put("NameOrIndex", "2");
+ taskList.setParsedUserInputs(parsedUserInputs);
+ taskList.changeTaskStatus();
+
+ beforeTasks.get(1).setDone(true);
+ assertEquals(beforeTasks.get(1).isDone(), taskList.getTaskList().get(1).isDone());
+ }
+
+ @Test
+ void getTasksReminder_success() {
+ parsedUserInputs.put("NameOrIndex", "7");
+ taskList.setParsedUserInputs(parsedUserInputs);
+ List criticalTasks = taskList.getTasksReminder();
+
+ beforeTasks.remove(0);
+ beforeTasks.remove(0);
+
+ assertEquals(beforeTasks, criticalTasks);
+ }
+
+ @Test
+ void addTask_success() {
+ boolean expectedSuccess = true;
+
+ parsedUserInputs.put("TaskType", "TODO");
+ parsedUserInputs.put("NameOrIndex", "shopping");
+ taskList.setParsedUserInputs(parsedUserInputs);
+ boolean actualSuccess = taskList.addTask();
+
+ assertEquals(expectedSuccess, actualSuccess);
+ }
+
+ @Test
+ void addTask_notUnique_fail() {
+ boolean expectedSuccess = false;
+
+ parsedUserInputs.put("TaskType", "TODO");
+ parsedUserInputs.put("NameOrIndex", "reading");
+ taskList.setParsedUserInputs(parsedUserInputs);
+ boolean actualSuccess = taskList.addTask();
+
+ assertEquals(expectedSuccess, actualSuccess);
+ }
+
+ @Test
+ void setTagForOneTask_success() {
+ parsedUserInputs.put("NameOrIndex", "2");
+ parsedUserInputs.put("Tag", "fun");
+ taskList.setParsedUserInputs(parsedUserInputs);
+ taskList.setTagForOneTask();
+
+ beforeTasks.get(1).setTag("fun");
+ assertEquals("#fun", taskList.getTaskList().get(1).getTag());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/duke/command/AddCommandTest.java b/src/test/java/duke/command/AddCommandTest.java
new file mode 100644
index 00000000..76034f07
--- /dev/null
+++ b/src/test/java/duke/command/AddCommandTest.java
@@ -0,0 +1,65 @@
+package duke.command;
+
+import duke.app.TaskList;
+import duke.app.Ui;
+import duke.exceptions.FileProcessError;
+import duke.storage.Storage;
+import duke.task.Task;
+import duke.task.Todo;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+class AddCommandTest {
+ List tasksBefore;
+ TaskList tasklist;
+ Ui ui = new Ui();
+ Storage storage;
+ AbstractCommand addCommand;
+ Map parsedUserInput;
+
+ @BeforeEach
+ void setUp() {
+ tasksBefore = new ArrayList<>();
+ tasklist = new TaskList();
+ parsedUserInput = new HashMap<>();
+ addCommand = new AddCommand();
+ try {
+ storage = new Storage("./src/test/resource/testAddCommand.txt");
+ } catch (FileProcessError e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ void execute_success() {
+ tasksBefore.add(new Todo("reading"));
+
+ parsedUserInput.put("TaskType", "TODO");
+ parsedUserInput.put("NameOrIndex", "reading");
+ tasklist.setParsedUserInputs(parsedUserInput);
+
+ addCommand.execute(tasklist, storage, ui);
+ assertEquals(tasksBefore, tasklist.getTaskList());
+ }
+
+ @Test
+ void execute_redundantTask_fail() {
+ tasksBefore.add(new Todo("reading"));
+ tasksBefore.add(new Todo("reading"));
+
+ parsedUserInput.put("TaskType", "TODO");
+ parsedUserInput.put("NameOrIndex", "reading");
+ tasklist.setParsedUserInputs(parsedUserInput);
+
+ addCommand.execute(tasklist, storage, ui);
+ assertNotEquals(tasksBefore, tasklist.getTaskList());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/duke/storage/FileDecoderTest.java b/src/test/java/duke/storage/FileDecoderTest.java
new file mode 100644
index 00000000..043688ce
--- /dev/null
+++ b/src/test/java/duke/storage/FileDecoderTest.java
@@ -0,0 +1,67 @@
+package duke.storage;
+
+import duke.task.Deadline;
+import duke.task.Task;
+import duke.task.Todo;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class FileDecoderTest {
+ List beforeTasks;
+ List afterTasks;
+
+ @BeforeEach
+ void setUp() {
+ beforeTasks = new ArrayList<>();
+ afterTasks = new ArrayList<>();
+ }
+
+ @Test
+ void decodeTasks_success() {
+ beforeTasks.add("TODO | 1 | read");
+ beforeTasks.add("DEADLINE | 0 | discussion | 2021-11-17 12:00");
+
+ Task task1 = new Todo("read");
+ task1.setDone(true);
+ afterTasks.add(task1);
+ afterTasks.add(new Deadline("discussion", "2021-11-17 12:00"));
+
+ assertEquals(afterTasks, FileDecoder.decodeTasks(beforeTasks));
+ }
+
+ @Test
+ void decodeTasks_withTag_success() {
+ beforeTasks.add("TODO | 0 | read | #fun");
+ beforeTasks.add("DEADLINE | 0 | discussion | 2021-11-17 12:00");
+
+ Task task1 = new Todo("read");
+ task1.setTag("fun");
+ afterTasks.add(task1);
+ afterTasks.add(new Deadline("discussion", "2021-11-17 12:00"));
+
+ assertEquals(afterTasks, FileDecoder.decodeTasks(beforeTasks));
+ }
+
+ @Test
+ void decodeTasks_noDescription_fail() {
+ beforeTasks.add("DEADLINE | 0 | | 2021-11-11 12:00");
+ assertEquals(true, FileDecoder.decodeTasks(beforeTasks).isEmpty());
+ }
+
+ @Test
+ void decodeTasks_invalidStatus_fail() {
+ beforeTasks.add("DEADLINE | 5 | homework | 2021-11-11 12:00");
+ assertEquals(true, FileDecoder.decodeTasks(beforeTasks).isEmpty());
+ }
+
+ @Test
+ void decodeTasks_invalidDate_fail() {
+ beforeTasks.add("DEADLINE | 5 | homework | 2021- 12:00");
+ assertEquals(true, FileDecoder.decodeTasks(beforeTasks).isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat
index 08737446..62752b88 100644
--- a/text-ui-test/runtest.bat
+++ b/text-ui-test/runtest.bat
@@ -15,7 +15,7 @@ IF ERRORLEVEL 1 (
REM no error here, errorlevel == 0
REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT
-java -classpath ..\bin Duke < input.txt > ACTUAL.TXT
+java -classpath ..\bin duke.Duke < input.txt > ACTUAL.TXT
REM compare the output to the expected output
FC ACTUAL.TXT EXPECTED.TXT
diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh
old mode 100644
new mode 100755
index c9ec8700..02bf4d50
--- a/text-ui-test/runtest.sh
+++ b/text-ui-test/runtest.sh
@@ -6,7 +6,7 @@ then
mkdir ../bin
fi
-# delete output from previous run
+# delete output from previous execute
if [ -e "./ACTUAL.TXT" ]
then
rm ACTUAL.TXT
@@ -19,7 +19,7 @@ then
exit 1
fi
-# run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT
+# execute the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT
java -classpath ../bin Duke < input.txt > ACTUAL.TXT
# convert to UNIX format