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