diff --git a/README.md b/README.md index 9d95025bce..3e769f7d81 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Prerequisites: JDK 11, update Intellij to the most recent version. 1. Click `Open or Import`. 1. Select the project directory, and click `OK` 1. If there are any further prompts, accept the defaults. -1. After the importing is complete, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()`. If the setup is correct, you should see something like the below: +1. After the importing is complete, locate the `src/dukemain/java/Duke.java` file, right-click it, and choose `Run Duke.dukemain()`. If the setup is correct, you should see something like the below: ``` Hello from ____ _ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..4bb8956801 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.junit.jupiter:junit-jupiter:5.4.2' + 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' + + String javaFxVersion = '11' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClassName = "dukemain.Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.36.1' +} + +run{ + standardInput = System.in +} diff --git a/data.txt b/data.txt new file mode 100644 index 0000000000..825f1840ba --- /dev/null +++ b/data.txt @@ -0,0 +1,5 @@ +done t buy groceries +incomplete d math homework timeOfTask: 22/09/2020 2359 +done e David's birthday timeOfTask: 26/09/2020 +incomplete t journal entry +incomplete d science hw assignment 3 timeOfTask: 23/09/2020 2359 diff --git a/docs/README.md b/docs/README.md index fd44069597..eaea167e82 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,20 +1,226 @@ -# User Guide +# Duke User Guide ## Features -### Feature 1 -Description of feature. +### ToDo +Adds a ToDo task to your list. -## Usage +#### Usage -### `Keyword` - Describe action +### `todo [task description]` -Describe action and its outcome. +Creates a ToDo task with specified description and adds +it to your list. Example of usage: -`keyword (optional arguments)` +`todo homework` Expected outcome: -`outcome` +`Got it! Task added to your list.`
+    `[T][X] homework`
+`Now you have x tasks in your list.` + +*** + +### Deadline +Adds a Deadline task to your list. + +#### Usage + +### `deadline [task description] /by [date]` + +Creates a Deadline task with specified description and time +and adds it to your list. + +Example of usage: + +`deadline assignment /by 19/04/2020 2359` + +Expected outcome: + +`Got it! Task added to your list.`
+    `[D][X] assignment (by: 19 Apr 2020 11:59pm)`
+`Now you have x tasks in your list.` + +Note:
+Valid date formats : `dd-mm-yyyy`, `dd-mm-yyyy hhhh`, +`dd/mm/yyyy`, `dd/mm/yyyy hhhh`. +Any other formats will be read as String. + +*** + +### Event +Adds an Event task to your list. + +#### Usage + +### `event [task description] /at [date]` + +Creates an Event task with specified description and time +and adds it to your list. + +Example of usage: + +`event concert /at 19/04/2020 1159` + +Expected outcome: + +`Got it! Task added to your list.`
+    `[E][X] concert (at: 19 Apr 2020 11:59am)`
+`Now you have x tasks in your list.`
+ +Note:
+Valid date formats : `dd-mm-yyyy`, `dd-mm-yyyy hhhh`, +`dd/mm/yyyy`, `dd/mm/yyyy hhhh`. +Any other formats will be read as String. + +*** + +### List +Shows a list of all your tasks. + +#### Usage + +### `list` + +Lists all tasks. + +Example of usage: + +`list` + +Expected outcome: + +`Here are your tasks:`
+`1. [T][X] homework`
+`2. [D][X] assignment (by: 19 Apr 2020 11:59pm)`
+`3. [E][X] concert (at: 19 Apr 2020 11:59am)`
+ +*** + +### Done +Marks task as completed with a [ / ]. + +#### Usage + +### `done [index]` + +Marks task at the specified index as done. + +Example of usage: + +`done 1` + +Expected outcome: + +`Nice! I have marked this task as done:`
+    `[T][/] homework`
+ +Note:
+Index is as specified by `list` function. + +*** + +### Delete +Removes specified task from list. + +#### Usage + +### `delete [index]` + +Deletes task at the specified index. + +Example of usage: + +`delete 1` + +Expected outcome: + +`Okay! I have removed this task:`
+    `[T] homework`
+`Now you have x tasks in your list` + +Note:
+Index is as specified by `list` function. + +*** + +### Clear +Deletes task list. + +#### Usage + +### `clear` + +Deletes entire task list. + +Example of usage: + +`clear` + +Expected outcome: + +`Task list cleared!`
+ +*** + +### Find +Finds task list using keyword/phrase. + +#### Usage + +### `find [keyword/phrase]` + +Prints a sublist of tasks that contain the specified
+keyword/phrase. + +Example of usage: + +`find math` + +Expected outcome: + +`Here are your matching tasks:`
+`1. [D][X] math homework (by: Sep 28 2020)`
+`2. [D][X] math assignment (by: Sep 30 2020)`
+`3. [E][X] math lecture (at: Sep 25 2020, 03:00pm)`
+ +If unsuccessful find:
+`Sorry! There are no tasks that match that description.` + +*** + +### Edit +Edits a specified task with a new field. + +#### Usage + +### `edit 1 [/d or /t] [new field]` + +Edits a task at the specified index with a new field.
+Tag "/d" to edit description.
+Tag "/t" to edit time. + +Example of usage: + +`edit 1 /d math hw 1 section b` + +Expected outcome: + +`Okay! I have edited this task:`
+    `===> [D][X] math homework (by: Sep 28 2020)`
+    `<=== [D][X] math hw 1 section b (by: Sep 28 2020)`
+ +Example of usage: + +`edit 1 /t 29/09/2020 2359` + +Expected outcome: + +`Okay! I have edited this task:`
+    `===> [D][X] math homework (by: Sep 28 2020)`
+    `<=== [D][X] math homework (by: Sep 29 2020, 11:59pm)`
+ +*** diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..6b0da7bd4c Binary files /dev/null and b/docs/Ui.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..f3d88b1c2f 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 0000000000..b7c8c5dbf5 --- /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 0000000000..2fe81a7d95 --- /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 0000000000..62bd9b9cce --- /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 5d313334cc..0000000000 --- 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/command/Command.java b/src/main/java/command/Command.java new file mode 100644 index 0000000000..07a7e1bf03 --- /dev/null +++ b/src/main/java/command/Command.java @@ -0,0 +1,324 @@ +package command; + +import exception.InvalidInputException; +import task.Deadline; +import task.Event; +import task.Task; +import task.ToDo; +import ui.Ui; + +import java.util.ArrayList; + +/** + * Command object has a CommandType to determine which specific command to execute + * and a description which contains the user input String. Command objects to be executed + * are always valid as input validation is done by the Parser object. + * Command object also handles manipulation of list of Task objects. + * + * @author Hakiem Rasid + */ +public class Command { + + public CommandType type; + public String description; + + /** + * Constructor for Command object. + * + * @param type Type of command to be executed as CommandType. + * @param description User input instruction as String. + */ + public Command(CommandType type, String description) { + this.type = type; + this.description = description; + } + + /** + * Returns list of Task objects after executing command. + * + * @param tasks List of Task objects to be manipulated. + * @param sb StringBuilder to append messages. + * @return List of Task objects after changes made from executing command. + * @throws IndexOutOfBoundsException If index specified in DONE or DELETE command + * does not lie within range of list of Task objects. + * @throws InvalidInputException If use requests to edit time field of ToDo. + */ + public ArrayList executeCommand(ArrayList tasks, StringBuilder sb) throws + IndexOutOfBoundsException, InvalidInputException { + + switch (this.type) { + case BYE: + sb.append(Ui.byeMessage()); + return tasks; + case CLEAR: + sb.append(Ui.clearedListMessage()); + return new ArrayList<>(); + + /* + *****Don't know how to implement in GUI***** + if (Ui.promptConfirm(new Scanner(System.in))) { + // Reference to empty ArrayList + Ui.clearedListMessage(); + return new ArrayList<>(); + } else { + // do nothing + Ui.didNotClearListMessage(); + return tasks; + } + */ + case LIST: + if (tasks.size() == 0) { + // do nothing + sb.append("There are no tasks in your list!"); + return new ArrayList<>(); + } else { + sb.append(Ui.printList(tasks, "print")); + return tasks; + } + case DONE: + return markDone(tasks, this.description, sb); + case DELETE: + return deleteTask(tasks, this.description, sb); + case TODO: + case DEADLINE: + case EVENT: + return addTask(tasks, this.description, sb); + case FIND: + return findTask(tasks, this.description, sb); + case EDIT: + return editTask(tasks, this.description, sb); + case UNKNOWN: + sb.append(this.description); + return tasks; + } + return tasks; + } + + /** + * Returns a list of task objects with specified task edited. + * @param tasks List of tasks. + * @param description User input for edit command. + * @param sb StringBuilder to append messages. + * @return Lists of task objects after editing. + * @throws IndexOutOfBoundsException If index specified does not lie within + * range of list of Task objects. + * @throws InvalidInputException If user requests to edit time field of ToDo object. + */ + public ArrayList editTask(ArrayList tasks, String description, StringBuilder sb) + throws IndexOutOfBoundsException, InvalidInputException { + + // Format eg: 1 /d newName + String[] descriptionArr = description.split(" "); + int index = Integer.parseInt(descriptionArr[0]); + String editType = descriptionArr[1].trim(); + String newField; + + // initialize newField + StringBuilder newFieldBuilder = new StringBuilder(); + for (int i = 2; i < descriptionArr.length; i++) { + newFieldBuilder.append(descriptionArr[i]); + newFieldBuilder.append(" "); + } + newField = newFieldBuilder.toString().trim(); + + // initialize oldTask with correct type and handle editing + Task current = tasks.get(index - 1); + if (current.printTask().startsWith("[T]")) { + // case: edit ToDo + if (editType.equals("/d")) { + // case: edit ToDo description + ToDo newTask = new ToDo(newField); + + // mark done + if (current.isDone()) { + newTask.completeTask(); + } + + // replace element in list + tasks.set(index - 1, newTask); + } else { + // throw exception: ToDo task does not have field for time + throw new InvalidInputException("Cannot edit time for todo task!"); + } + } else if (current.printTask().startsWith("[D]")) { + // case: edit Deadline + Deadline oldTask = (Deadline) current; + Deadline newTask; + if (editType.equals("/d")) { + // case: edit Deadline description + newTask = new Deadline(newField, oldTask.getDeadline()); + + // mark done + + // replace element in list + } else { + // case: edit Deadline time (deadline) + newTask = new Deadline(oldTask.getName(), newField); + + // mark done + + // replace element in list + } + if (current.isDone()) { + newTask.completeTask(); + } + tasks.set(index - 1, newTask); + } else { + // case; edit Event + Event oldTask = (Event) current; + Event newTask; + if (editType.equals("/d")) { + // case: edit Event description + newTask = new Event(newField, oldTask.getName()); + + // mark done + + // replace element in list + } else { + // case: edit Event time + newTask = new Event(oldTask.getName(), newField); + + // mark done + + // replace element in list + } + if (current.isDone()) { + newTask.completeTask(); + } + tasks.set(index - 1, newTask); + } + + sb.append(Ui.editMessage(current, tasks.get(index - 1))); + return tasks; + + } + + /** + * Returns a list of Task objects with a description that contains the key. + * + * @param tasks List of Task objects of which to find matching Tasks. + * @param key Key used to find matching Task objects. + * @param sb StringBuilder to append message. + * @return List of matching Task objects. + */ + public ArrayList findTask(ArrayList tasks, String key, StringBuilder sb) { + assert !key.equals(" "); + ArrayList matchedTasks = new ArrayList<>(); + for (Task task : tasks) { + if (task.getName().toLowerCase().contains(key.toLowerCase())) { + matchedTasks.add(task); + } + } + if (matchedTasks.size() == 0) { + sb.append(Ui.noMatchMessage()); + } else { + sb.append(Ui.printList(matchedTasks, "find")); + } + return tasks; + } + + /** + * Returns list of Task objects with specified Task object marked as done. + * + * @param tasks List of Task objects. + * @param input Keyword "done" followed by index of Task object to be marked done. + * @param sb StringBuilder to append message. + * @return List of updated Task objects. + * @throws IndexOutOfBoundsException If index specified in DONE Command + * does not lie within range of list of Task objects. + */ + public ArrayList markDone(ArrayList tasks, String input, StringBuilder sb) throws + IndexOutOfBoundsException { + + // parse int for index of task to be marked as done + int index = Integer.valueOf(input.split(" ")[1]); + + Task current = tasks.get(index - 1); + current.completeTask(); + sb.append(Ui.markDoneMessage(current.printTask())); + return tasks; + } + + /** + * Returns updated list of Task objects after deleting Task at specified index. + * + * @param tasks List of Task objects to be manipulated. + * @param input Keyword "delete" followed by index of Task to be deleted. + * @param sb StringBuilder used to append message. + * @return Updated list of Task objects. + * @throws IndexOutOfBoundsException If index specified in DELETE Command + * does not lie within range of list of Task objects. + */ + public ArrayList deleteTask(ArrayList tasks, String input, StringBuilder sb) throws + IndexOutOfBoundsException { + + // parse int for index of task to be deleted + int index = Integer.valueOf(input.split(" ")[1]); + + Task current = tasks.remove(index - 1); + sb.append(Ui.deleteTaskMessage(current.printTask(), tasks.size())); + return tasks; + } + + // Adds Task to list. Checks inputs and throws exceptions for invalid inputs + + /** + * Returns updated list of Task objects after adding the specified Task. + * + * @param tasks List of Task objects to be manipulated. + * @param input Keyword of specified Task type followed by details of the Task. + * @param sb StringBuilder to append message. + * @return Updated List of Task objects. + */ + public ArrayList addTask(ArrayList tasks, String input, StringBuilder sb) { + + StringBuilder todoDescription = new StringBuilder(); + String[] splitSpace = input.split(" "); + Task task; + + if (this.type.equals(CommandType.TODO)) { + // case: todo + for (String str : splitSpace) { + if (str.toLowerCase().equals("todo")) { + continue; + } else { + todoDescription.append(str + " "); + } + } // end for loop + task = new ToDo(todoDescription.toString().trim()); + } else if (this.type.equals(CommandType.DEADLINE)) { + // case: deadline + + String name = input.split("/by")[0].trim().substring(9); + String deadline = input.split("/by")[1].trim(); + task = new Deadline(name, deadline); + } else { + // case: event + String name = input.split("/at")[0].trim().substring(6); + String time = input.split("/at")[1].trim(); + task = new Event(name, time); + } + + tasks.add(task); + sb.append(Ui.addTaskMessage(task.printTask(), tasks.size())); + return tasks; + } + + /** + * Returns the type of this Command object. + * + * @return Type of this Command object as CommandType. + */ + public CommandType getType() { + return this.type; + } + + /** + * Returns description(user input instruction) of this Command object. + * @return Description of Command object as String. + */ + public String getDescription() { + return this.description; + } + +} \ No newline at end of file diff --git a/src/main/java/command/CommandType.java b/src/main/java/command/CommandType.java new file mode 100644 index 0000000000..751cb2c6c1 --- /dev/null +++ b/src/main/java/command/CommandType.java @@ -0,0 +1,5 @@ +package command; + +public enum CommandType { + TODO, DEADLINE, EVENT, LIST, DONE, BYE, DELETE, CLEAR, UNKNOWN, FIND, EDIT +} diff --git a/src/main/java/data/DateManager.java b/src/main/java/data/DateManager.java new file mode 100644 index 0000000000..d58fdd1ec6 --- /dev/null +++ b/src/main/java/data/DateManager.java @@ -0,0 +1,181 @@ +package data; + +import java.util.Date; +import java.util.Optional; +import java.text.SimpleDateFormat; +import java.text.ParseException; + +/** + * data.DateManager object parses valid String inputs from Deadline and Event + * objects to be stored as Date objects. + * + * @author Hakiem Rasid + */ +public class DateManager { + + private static final String[] DATE_INPUT_FORMATS = + {"invalid", "dd-MM-yyyy", "dd/MM/yyyy", "dd-MM-yyyy kkmm", "dd/MM/yyyy kkmm"}; + private static final String[] DATE_OUTPUT_FORMATS = {"MMM dd yyyy", "MMM dd yyyy',' hh:mma"}; + + /** + * Returns Optional containing Date if String is valid. + * + * @param str Input String to be parsed as Date object. + * @return Optional containing Date if String is valid. Empty Optional otherwise. + */ + public Optional getDate(String str) { + try { + if (getDateFormat(str).equals(DATE_INPUT_FORMATS[0])) { + // returns empty Optional if str is not of valid format + return Optional.empty(); + } else { + SimpleDateFormat formatter = new SimpleDateFormat(getDateFormat(str)); + return Optional.of(formatter.parse(str)); + } + } catch (ParseException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + /** + * Returns String representation of valid Date object. + * + * @param str Valid input String to be parsed as Date. + * @return String representation of Date. + */ + public String getDateAsString(String str) { + // assumes that str input has valid date format + // input checks done in Deadline and Event + + SimpleDateFormat sdf; + + if (getDateFormat(str).equals(DATE_INPUT_FORMATS[1]) || + getDateFormat(str).equals(DATE_INPUT_FORMATS[2])) { + + sdf = new SimpleDateFormat(DATE_OUTPUT_FORMATS[0]); + } else { + sdf = new SimpleDateFormat(DATE_OUTPUT_FORMATS[1]); + } + return sdf.format(getDate(str).get()); + } + + /** + * Returns format of String input of a date. + * + * @param str String representation of a date/ + * @return Format of Date object if input is valid. Returns "invalid" otherwise. + */ + public String getDateFormat(String str) { + if (str.length() != DATE_INPUT_FORMATS[1].length() && + str.length() != DATE_INPUT_FORMATS[3].length()) { + // input string has invalid format if it is not of correct length + return DATE_INPUT_FORMATS[0]; + } + + if (str.length() == DATE_INPUT_FORMATS[1].length()) { + // either of format dd-mm-yyyy or dd/mm/yyyy + if (str.substring(2, 3).equals("-")) { + // case: dd-mm-yyyy + String[] date = str.split("-"); + return (isValidDateNumerals(date)) + ? DATE_INPUT_FORMATS[1] + : DATE_INPUT_FORMATS[0]; + } else if (str.substring(2, 3).equals("/")) { + // case: dd/mm/yyyy + String[] date = str.split("/"); + return (isValidDateNumerals(date)) + ? DATE_INPUT_FORMATS[2] + : DATE_INPUT_FORMATS[0]; + } else { + // invalid format + return DATE_INPUT_FORMATS[0]; + } + } else { + // case: dd-mm-yyyy hhhh or dd/mm/yyyy hhhh + if (str.substring(2, 3).equals("-")) { + // case: dd-mm-yyyy hhhh + String hrs = str.split(" ")[1]; + String[] date = str.split(" ")[0].split("-"); + String[] dateTime = new String[4]; + + // assign values to dateTime array + for (int i = 0; i < 3; i++) { + dateTime[i] = date[i]; + } + dateTime[3] = hrs; + return (isValidDateNumerals(dateTime)) + ? DATE_INPUT_FORMATS[3] + : DATE_INPUT_FORMATS[0]; + } else if (str.substring(2, 3).equals("/")) { + // case: dd/mm/yyyy hhhh + String hrs = str.split(" ")[1]; + String[] date = str.split(" ")[0].split("/"); + String[] dateTime = new String[4]; + + // assign values to dateTime array + for (int i = 0; i < 3; i++) { + dateTime[i] = date[i]; + } + dateTime[3] = hrs; + return (isValidDateNumerals(dateTime)) + ? DATE_INPUT_FORMATS[4] + : DATE_INPUT_FORMATS[0]; + } else { + // invalid format + return DATE_INPUT_FORMATS[0]; + } + } + + } + + /** + * Checks validity of integers in String representation of a date. + * + * @param arr Array containing integers for day, month and year. + * @return True if integers form a valid date. False otherwise. + */ + private boolean isValidDateNumerals(String[] arr) { + assert arr.length <= 4; + assert canParseToInt(arr[0]); + assert canParseToInt(arr[1]); + assert canParseToInt(arr[2]); + + boolean isValid = true; + if (Integer.parseInt(arr[0]) > 31) { + // invalid if day > 31 + isValid = false; + } + if (Integer.parseInt(arr[1]) > 12) { + // invalid if month > 12 + isValid = false; + } + if (Integer.parseInt(arr[2]) < 2020) { + // invalid if year < 2020 + isValid = false; + } + + if (arr.length == 4) { + // case dd mm yyyy hhhh + if (Integer.parseInt(arr[3]) > 2359 || Integer.parseInt(arr[3]) < 0) { + isValid = false; + } + } + + return isValid; + } + + /** + * Checks if String can be parsed to Integer. + * @param str Input String. + * @return True if String can be parsed as Integer, false otherwise. + */ + private boolean canParseToInt(String str) { + try { + Integer.parseInt(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/data/Parser.java b/src/main/java/data/Parser.java new file mode 100644 index 0000000000..a6bf69dcd6 --- /dev/null +++ b/src/main/java/data/Parser.java @@ -0,0 +1,267 @@ +package data; + +import command.Command; +import command.CommandType; +import exception.InvalidInputException; + +/** + * data.Parser object processes and makes sense of user input to be executed. + * Input validation is also done as inputs are parsed to ensure command + * Strings follow a specific format before they can be executed. + * + * @author Hakiem Rasid + */ +public class Parser { + + public static final String[] COMMANDS = {"todo", "deadline", "event", + "list", "done", "bye", "delete", "clear", "unknown", "find", "edit"}; + + /** + * Returns Command object by processing user input. + * + * @param input User inputs as String. + * @return Command object. + * @throws InvalidInputException If inputs are of incorrect format + * and cannot be parsed. + */ + public Command parseCommand(String input) throws InvalidInputException { + String[] strings = input.split(" "); + Command command; + + if (input.trim().toLowerCase().equals(Parser.COMMANDS[5])) { + // returns BYE Command + command = new Command(CommandType.BYE, input); + } else if (input.trim().toLowerCase().equals(Parser.COMMANDS[7])) { + // returns CLEAR Command + command = new Command(CommandType.CLEAR, input); + } else if (input.trim().toLowerCase().equals(Parser.COMMANDS[3])) { + // return LIST Command + command = new Command(CommandType.LIST, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[4])) { + // returns DONE Command + command = new Command(CommandType.DONE, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[6])) { + // returns DELETE Command + command = new Command(CommandType.DELETE, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[0])) { + // returns TODO Command + command = new Command(CommandType.TODO, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[1])) { + // returns DEADLINE Command + command = new Command(CommandType.DEADLINE, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[2])) { + // returns EVENT Command + command = new Command(CommandType.EVENT, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[9])) { + // returns FIND Command + command = new Command(CommandType.FIND, input); + } else if (strings[0].toLowerCase().equals(Parser.COMMANDS[10])) { + // returns EDIT Command + command = new Command(CommandType.EDIT, input); + } else { + // returns UNKNOWN Command + command = new Command(CommandType.UNKNOWN, + "Sorry, I don't understand!"); + } + return validateCommand(command); + } + + /** + * Returns Command object after validating its contents. + * + * @param cmd Command object to be validated. + * @return Valid Command object that can be executed. + * @throws InvalidInputException If description of input Command + * is of incorrect format and cannot be parsed. + */ + public Command validateCommand(Command cmd) throws InvalidInputException { + Command validCommand = new Command(CommandType.UNKNOWN, + "Sorry, I don't understand!"); + switch(cmd.getType()) { + case BYE: + case CLEAR: + case LIST: + case UNKNOWN: + // no need to validate these Command types + validCommand = cmd; + break; + case DONE: + case DELETE: + validCommand = checkDoneAndDeleteValidity(cmd); + break; + case TODO: + validCommand = checkToDoValidity(cmd); + break; + case DEADLINE: + case EVENT: + validCommand = checkDeadlineAndEventValidity(cmd); + break; + case FIND: + validCommand = checkFindValidity(cmd); + break; + case EDIT: + validCommand = checkEditValidity(cmd); + } + return validCommand; + } + + /** + * Returns valid Command object after input validity checks of + * EDIT Command. + * + * @param cmd Command object with CommandType EDIT. + * @return Input Command object if description passes input validity checks. + * @throws InvalidInputException If description of Command object is of + * incorrect format. + */ + public Command checkEditValidity(Command cmd) throws InvalidInputException { + // Correct format eg: + // - edit 1 /d newName (edits name) + // - edit 1 /t 12/12/2020 1230 (edits time for Deadline/Event) + String commandString = cmd.getDescription().trim(); + String[] commandStringArr = commandString.split("\\s+"); + + if (!commandString.contains("/d") && !commandString.contains("/t")) { + // throws Exception for missing identifier + throw new InvalidInputException("Command is missing \"/d\" or \"/t\" keyword."); + } + + if (commandStringArr[commandStringArr.length - 1].equals("/t") || + commandStringArr[commandStringArr.length - 1].equals("/d")) { + // throws Exception if missing fields after /d or /t identifier + throw new InvalidInputException("Incomplete edit command."); + } + + try { + // throws Exception if an integer does not follow "edit" + Integer.parseInt(commandStringArr[1]); + } catch (NumberFormatException e) { + throw new InvalidInputException("Please enter a valid integer index."); + } + + StringBuilder editCommand = new StringBuilder(); + for (int i = 1; i < commandStringArr.length; i++) { + editCommand.append(commandStringArr[i]); + editCommand.append(" "); + } + return new Command(CommandType.EDIT, editCommand.toString().trim()); + } + + /** + * Returns valid Command object after input validity checks of + * FIND Command. + * + * @param cmd Command object with CommandType FIND. + * @return Input Command object if description passes input validity checks. + * @throws InvalidInputException If description of Command object is of + * incorrect format. + */ + public Command checkFindValidity(Command cmd) throws InvalidInputException { + String description = cmd.getDescription(); + String key; + + if (description.trim().equals("find")) { + // throws exception for invalid input i.e. "find", "find " + throw new InvalidInputException("Incomplete find command"); + } + + key = description.trim().substring(5).trim(); + return new Command(CommandType.FIND, key); + } + + /** + * Returns valid Command object after input validity checks of + * DEADLINE and EVENT Commands. + * + * @param cmd Command object with CommandType DEADLINE or EVENT. + * @return Input Command object if description passes input validity checks. + * @throws InvalidInputException If description of Command object is of + * incorrect format. + */ + public Command checkDeadlineAndEventValidity(Command cmd) throws InvalidInputException { + String cmdIdentifier; + + String description = cmd.getDescription(); + if (cmd.getType().equals(CommandType.DEADLINE)) { + // case: DEADLINE + cmdIdentifier = "by"; + } else { + // case: EVENT + cmdIdentifier = "at"; + } + + if (!description.contains(" /" + cmdIdentifier)) { + // throws exception if invalid input format: does not contain "/by" or "/at" + // must have space before /by or /at keywords + throw new InvalidInputException("Command is missing \"/" + cmdIdentifier + + "\" keyword"); + } + + if (description.split(" /")[0].toLowerCase().equals("deadline") || + description.split(" /")[0].toLowerCase().equals("event")) { + + // throws exception if invalid input format: "deadline /by taskDeadline" + // throws exception if invalid input format: "event /by eventDate" + throw new InvalidInputException("Missing task description"); + } + + if (description.split(" ")[(description.split(" ").length - 1)] + .equals("/" + cmdIdentifier)) { + // throws exception if invalid input format: "deadline taskName /by" + throw new InvalidInputException("Missing task deadline/time"); + } + return cmd; + } + + /** + * Returns valid Command object after input validity checks of TODO Command. + * + * @param cmd Command object with CommandType TODO. + * @return Input Command object if description passes input validity checks. + * @throws InvalidInputException If description of Command object is of incorrect format. + */ + public Command checkToDoValidity(Command cmd) throws InvalidInputException { + if (cmd.getDescription().split(" ").length == 1) { + // throws exception if invalid input format: "todo" (missing task name) + throw new InvalidInputException("Todo command incomplete"); + } + return cmd; + } + + /** + * Returns valid Command object after input validity checks of DONE or + * DELETE Commands. + * + * @param cmd Command object with CommandType DONE or DELETE. + * @return Input Command object if description passes input validity checks. + * @throws InvalidInputException If description of Command object is of + * incorrect format. + */ + public Command checkDoneAndDeleteValidity(Command cmd) throws InvalidInputException { + String description = cmd.getDescription(); + if (description.split(" ").length == 1) { + // throws exception if invalid input format: "done"/"delete" (missing index) + throw new InvalidInputException("Task index not specified"); + } + + if (description.split(" ").length > 2) { + // throws exception if invalid input format: > 2 strings separated by " " + // e.g "done/delete 1 2 3", "done/delete 12 text" + throw new InvalidInputException("Sorry, command unclear!" + + " Please specify only one index"); + } + + int index; + try { + // parse int for index of task to be marked as done/deleted + index = Integer.valueOf(description.split(" ")[1]); + } catch (NumberFormatException e) { + // throws exception if invalid input format: Invalid integer + // e.g "done/delete abc" + throw new InvalidInputException("Please enter a valid integer"); + } + + return cmd; + } + +} \ No newline at end of file diff --git a/src/main/java/data/Storage.java b/src/main/java/data/Storage.java new file mode 100644 index 0000000000..59f63db222 --- /dev/null +++ b/src/main/java/data/Storage.java @@ -0,0 +1,147 @@ +package data; + +import task.Deadline; +import task.Event; +import task.Task; +import task.ToDo; + +import java.util.ArrayList; +import java.io.File; +import java.io.FileWriter; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Storage object handles saving and loading of data for list of Task objects. + * Task objects are stored as String representations in specified .txt file. + * + * @author Hakiem Rasid + */ +public class Storage { + + private String filePath; + + /** + * Constructor of Storage object. + * + * @param filePath Target .txt file for saving and loading of data. + */ + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * Sends data of list of Task objects as String to be saved onto + * target .txt file. + * Format of each line in txt file: done/incomplete t/d/e description [time: time] + * + * @param taskList List of Task objects to be saved. + */ + public void saveData(ArrayList taskList) { + + try { + File file = new File(this.filePath); + + // if file doesn't exists, then create it + if (!file.exists()) { + file.createNewFile(); + } + + FileWriter writer = new FileWriter(file, false); + + for (Task task : taskList) { + StringBuilder sb = new StringBuilder(); + if (task.isDone()) { + sb.append("done "); + } else { + sb.append("incomplete "); + } + + if (task instanceof ToDo) { + ToDo currentTask = (ToDo) task; + sb.append("t " + currentTask.getName()); + } else if (task instanceof Deadline) { + Deadline currentTask = (Deadline) task; + sb.append("d " + currentTask.getName() + " timeOfTask: " + + currentTask.getDeadline()); + } else if (task instanceof Event) { + Event currentTask = (Event) task; + sb.append("e " + currentTask.getName() + " timeOfTask: " + + currentTask.getTime()); + } else { + // do nothing + } + sb.append("\n"); + writer.write(sb.toString()); + } // end for loop + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Returns list of Task objects after loading data from .txt file. + * Returns empty list if no data is found. + * + * @return List of Task objects if data is found and loaded. Empty list + * if no data is found. + */ + public ArrayList loadData() { + ArrayList list = new ArrayList<>(); + try { + BufferedReader br = new BufferedReader(new FileReader(filePath)); + String currentLine; + + while ((currentLine = br.readLine()) != null) { + StringBuilder sb = new StringBuilder(); + String[] lineArray = currentLine.split(" "); + boolean isDoneTask = lineArray[0].equals("done"); + String type = lineArray[1]; + String description; + String time; + Task newTask; + + // eg. done d this is a description timeOfTask: 12-12-2020 1455 + if (type.equals("t")) { + // case ToDo + for (int i = 2; i < lineArray.length; i++) { + sb.append(lineArray[i] + " "); + } + description = sb.toString().trim(); + newTask = new ToDo(description); + } else { + // case Deadline/Event + for (int i = 2; i < lineArray.length; i++) { + if (lineArray[i].equals("timeOfTask:")) { + break; + } + sb.append(lineArray[i] + " "); + } + description = sb.toString().trim(); + time = currentLine.split("timeOfTask:")[1].trim(); + newTask = (type.equals("d")) + ? new Deadline(description, time) + : new Event(description, time); + } + + if (isDoneTask) { + newTask.completeTask(); + } + list.add(newTask); + } // end while loop + + } catch (FileNotFoundException e) { + // returns empty ArrayList if savedata text file not found + return new ArrayList<>(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + + return list; + } + +} \ No newline at end of file diff --git a/src/main/java/data/TaskList.java b/src/main/java/data/TaskList.java new file mode 100644 index 0000000000..70eef88ec5 --- /dev/null +++ b/src/main/java/data/TaskList.java @@ -0,0 +1,100 @@ +package data; + +import command.Command; +import command.CommandType; +import exception.InvalidInputException; +import ui.Ui; +import task.Task; + +import java.util.ArrayList; +import java.util.Scanner; + +/** + * TaskList object contains a list of Task objects that are added and edited + * by the user. This object has a Storage object to manage saving and + * loading of data. + * + * @author Hakiem Rasid + */ +public class TaskList { + + private final Storage storage; + private ArrayList list; + + /** + * Constructor for TaskList object. + * @param filePath Destination file for saving and loading of data. + */ + public TaskList(String filePath) { + this.storage = new Storage(filePath); + this.list = storage.loadData(); + } + + /** + * Reads input from user and executes the appropriate commands to manipulate + * the list of Task objects or provide instructions to the program. + */ + public void runCommands() { + Parser parser = new Parser(); + Scanner sc = new Scanner(System.in); + + while (true) { + StringBuilder sb = new StringBuilder(); + System.out.println(Ui.horizontalLine()); + try { + String input = sc.nextLine(); + System.out.println(Ui.horizontalLine()); + Command cmd = parser.parseCommand(input); + this.list = cmd.executeCommand(this.list, sb); + if (cmd.getType().equals(CommandType.BYE)) { + // exit program if user inputs "bye" + break; + } + } catch (InvalidInputException e) { + sb.append(e.getMessage() + "\n"); + sb.append(Ui.invalidInputMessage()); + } catch (IndexOutOfBoundsException obe) { + sb.append(Ui.invalidIndexMessage()); + } finally { + System.out.println(sb.toString()); + } + } // end while loop + } + + /** + * Returns a message after executing a Command. + * + * @param input User input as String. + * @return Message as String. + */ + public String runSingleCommand(String input) { + Parser parser = new Parser(); + StringBuilder sb = new StringBuilder(); + try { + Command cmd = parser.parseCommand(input); + this.list = cmd.executeCommand(this.list, sb); + } catch (InvalidInputException e) { + sb.append(e.getMessage()); + sb.append("\n"); + sb.append(Ui.invalidInputMessage()); + } catch (IndexOutOfBoundsException obe) { + sb.append(Ui.invalidIndexMessage()); + } + return sb.toString(); + } + + /** + * Returns List of Task objects. + * @return List of Task objects. + */ + public ArrayList getList() { + return this.list; + } + + /** + * Saves list of Task objects onto specified txt file. + */ + public void save() { + storage.saveData(this.list); + } +} \ No newline at end of file diff --git a/src/main/java/dukemain/DukeMain.java b/src/main/java/dukemain/DukeMain.java new file mode 100644 index 0000000000..b6e9103e2e --- /dev/null +++ b/src/main/java/dukemain/DukeMain.java @@ -0,0 +1,32 @@ +package dukemain; + +import data.TaskList; +import ui.Ui; + +public class DukeMain { + + private TaskList list; + + public DukeMain() { + // data.txt will be created in same directory as duke.jar + // if it does not already exist + this.list = new TaskList("data.txt"); + } + + // For CLI Duke. But does not run anymore not sure why?? + public static void main(String[] args) { + TaskList taskList = new TaskList("src/savedata/data.txt"); + Ui.startUpMessage(); + taskList.runCommands(); + taskList.save(); + } + + public String getResponse(String input) { + StringBuilder sb = new StringBuilder(); + sb.append(this.list.runSingleCommand(input)); + this.list.save(); + return sb.toString(); + } + +} + diff --git a/src/main/java/dukemain/Launcher.java b/src/main/java/dukemain/Launcher.java new file mode 100644 index 0000000000..cdb68fd943 --- /dev/null +++ b/src/main/java/dukemain/Launcher.java @@ -0,0 +1,14 @@ +package dukemain; + +import javafx.application.Application; +import ui.Main; + +/** + * A launcher class to workaround classpath issues. + * Credits: CS2103 JavaFX Tutorial + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/exception/InvalidInputException.java b/src/main/java/exception/InvalidInputException.java new file mode 100644 index 0000000000..100107961c --- /dev/null +++ b/src/main/java/exception/InvalidInputException.java @@ -0,0 +1,7 @@ +package exception; + +public class InvalidInputException extends Exception { + public InvalidInputException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/task/Deadline.java b/src/main/java/task/Deadline.java new file mode 100644 index 0000000000..9e478ca7d3 --- /dev/null +++ b/src/main/java/task/Deadline.java @@ -0,0 +1,66 @@ +package task; + +import data.DateManager; + +import java.util.Date; +import java.util.Optional; + +/** + * Deadline object is a subclass of Task object. It contains a deadline String, + * data.DateManager to process the deadline and an Optional to store a Date object + * if deadline is of a valid format. + * + * @author Hakiem Rasid + * + */ +public class Deadline extends Task { + + private final String deadline; + private final Optional optDate; + private final DateManager dateManager; + + /** + * Constructor for Deadline object. + * + * @param name Description of task. + * @param deadline Description of deadline for this task. + */ + public Deadline(String name, String deadline) { + super(name); + this.deadline = deadline; + this.dateManager = new DateManager(); + this.optDate = dateManager.getDate(deadline); + } + + /** + * Returns Deadline for this task to be completed. + * + * @return Deadline as a String. + */ + public String getDeadline() { + return this.deadline; + } + + /** + * Prints a String representation of Deadline object and processes validity of deadline + * format to determine format of output. + * Clearly labels the Deadline object to be easily distinguishable from other + * Task objects. + * + * @return String representation of Deadline. + */ + @Override + public String printTask() { + StringBuilder sb = new StringBuilder(); + sb.append("[D]"); + sb.append(super.printTask()); + if (!optDate.isPresent()) { + sb.append(" (by: " + this.deadline + ")"); + } else { + sb.append(" (by: " + dateManager.getDateAsString(deadline) + ")"); + } + + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/task/Event.java b/src/main/java/task/Event.java new file mode 100644 index 0000000000..d384ca53e0 --- /dev/null +++ b/src/main/java/task/Event.java @@ -0,0 +1,65 @@ +package task; + +import data.DateManager; + +import java.util.Date; +import java.util.Optional; + +/** + * Event object is a subclass of Task object. It contains a time as String, + * data.DateManager to process the time and an Optional to store a Date object + * if time is of a valid format. + * + * @author Hakiem Rasid + * + */ +public class Event extends Task { + + private final String time; + private final Optional optTime; + private final DateManager dateManager; + + /** + * Constructor for Event object. + * + * @param name Description of event. + * @param time Description of time of this event. + */ + public Event(String name, String time) { + super(name); + this.time = time; + this.dateManager = new DateManager(); + this.optTime = dateManager.getDate(time); + } + + /** + * Returns time of this event. + * + * @return Time as a String. + */ + public String getTime() { + return this.time; + } + + /** + * Prints a String representation of Event object and processes validity of time + * format to determine format of output. + * Clearly labels the Deadline object to be easily distinguishable from other + * Task objects. + * + * @return String representation of Event. + */ + @Override + public String printTask() { + StringBuilder sb = new StringBuilder(); + sb.append("[E]"); + sb.append(super.printTask()); + if (!optTime.isPresent()) { + sb.append(" (at: " + this.time + ")"); + } else { + sb.append(" (at: " + dateManager.getDateAsString(time) + ")"); + } + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/task/Task.java b/src/main/java/task/Task.java new file mode 100644 index 0000000000..27c936b47d --- /dev/null +++ b/src/main/java/task/Task.java @@ -0,0 +1,67 @@ +package task; + +/** + * Task object contains a name or description of the task to be done and + * an indicator if it has been completed. + * + * @author Hakiem Rasid + */ + +public class Task { + private final String name; + private boolean done; + + /** + * Constructor for Task object + * + * @param name Description or name of the task. + */ + public Task(String name) { + this.name = name; + this.done = false; + + } + + /** + * Marks the task as completed. + */ + public void completeTask() { + this.done = true; + } + + /** + * Returns name/description of the task. + * + * @return + */ + public String getName() { + return this.name; + } + + /** + * Returns boolean value to determine if task has been completed. + * + * @return True if task is completed. False otherwise. + */ + public boolean isDone() { + return this.done; + } + + /** + * Prints a string representation of the Task object with a tick if the + * task has been competed and a cross otherwise. + * + * @return String representation of Task object. + */ + public String printTask() { + StringBuilder out = new StringBuilder(); + + if (this.isDone()) { + out.append("[/] "); + } else { + out.append("[X] "); + } + out.append(this.getName()); + return out.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/task/ToDo.java b/src/main/java/task/ToDo.java new file mode 100644 index 0000000000..eda3d7800c --- /dev/null +++ b/src/main/java/task/ToDo.java @@ -0,0 +1,33 @@ +package task; + +/** + * ToDo object is a subclass of Task object but does not contain any extra information. + * + * @author Hakiem Rasid + */ +public class ToDo extends Task { + + /** + * Constructor for ToDo object. + * + * @param name Description of this task. + */ + public ToDo(String name) { + super(name); + } + + /** + * Returns a String representation of ToDo object. Clearly labels + * the ToDo object to be distinguishable from other Task objects. + * + * @return String representation of ToDo object. + */ + @Override + public String printTask() { + StringBuilder sb = new StringBuilder(); + sb.append("[T]"); + sb.append(super.printTask()); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/ui/DialogBox.java b/src/main/java/ui/DialogBox.java new file mode 100644 index 0000000000..5fa5869e28 --- /dev/null +++ b/src/main/java/ui/DialogBox.java @@ -0,0 +1,79 @@ +package ui; + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; + +/** + * An example of a custom control using FXML. + * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label + * containing text from the speaker. + * Credits: CS2103 JavaFX Tutorial. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + + // set background colour + this.setBackground(new Background(new BackgroundFill(Color.LAVENDER, + new CornerRadii(8), Insets.EMPTY))); + + // set padding and spacing + this.setPadding(new Insets(15, 12, 15, 12)); + this.setSpacing(10); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + public static DialogBox getUserDialog(String text, Image img) { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append(text); + sb.append("\n"); + return new DialogBox(sb.toString(), img); + } + + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} diff --git a/src/main/java/ui/Main.java b/src/main/java/ui/Main.java new file mode 100644 index 0000000000..6f8762d41d --- /dev/null +++ b/src/main/java/ui/Main.java @@ -0,0 +1,35 @@ +package ui; + +import java.io.IOException; + +import dukemain.DukeMain; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + * Credits: CS2103 JavaFX Tutorial. + */ +public class Main extends Application { + + private final DukeMain duke = new DukeMain(); + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + stage.setTitle("Duke"); + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/ui/MainWindow.java b/src/main/java/ui/MainWindow.java new file mode 100644 index 0000000000..c74c532e1e --- /dev/null +++ b/src/main/java/ui/MainWindow.java @@ -0,0 +1,70 @@ +package ui; + +import dukemain.DukeMain; + +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + +/** + * Controller for MainWindow. Provides the layout for the other controls. + * Credits: CS2103 JavaFX Tutorial. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private DukeMain duke; + + // Credit: https://www.pexels.com/photo/person-wearing-vr-goggles-2007647/ + private final Image userImage = new Image(this.getClass() + .getResourceAsStream("/images/DaVrGuy.jpeg")); + // Credit: https://www.pexels.com/photo/selective-focus-photography-of-black-cat-2071881/ + private final Image dukeImage = new Image(this.getClass() + .getResourceAsStream("/images/DaCat.jpeg")); + + @FXML + public void initialize() { + dialogContainer.getChildren().addAll(DialogBox.getDukeDialog(Ui.chatStartMessage(), + dukeImage)); + dialogContainer.setSpacing(15); + dialogContainer.setBackground(new Background(new BackgroundFill(Color.BEIGE, + CornerRadii.EMPTY, Insets.EMPTY))); + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + public void setDuke(DukeMain d) { + duke = d; + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing + * Duke's reply and then appends them to the dialog container. Clears the + * user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = duke.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/ui/Ui.java b/src/main/java/ui/Ui.java new file mode 100644 index 0000000000..9d42fd9cc7 --- /dev/null +++ b/src/main/java/ui/Ui.java @@ -0,0 +1,213 @@ +package ui; + +import task.Task; + +import java.util.Scanner; +import java.util.ArrayList; + +/** + * Ui class handles any text or String representations to be viewed by the user. + * + * @author Hakiem Rasid + */ +public class Ui { + + private static final String HORIZONTAL_LINE = + "______________________________________________________"; + + + /** + * Returns and prints start-up message upon program execution. + * @return Start-up message. + */ + public static String startUpMessage() { + StringBuilder sb = new StringBuilder(); + String logo = " ____ _ \n" + + "| _ \\ _ _| | _____ \n" + + "| | | | | | | |/ / _ \\\n" + + "| |_| | |_| | < __/\n" + + "|____/ \\__,_|_|\\_\\___|\n"; + sb.append("Hello from\n" + logo); + sb.append(Ui.HORIZONTAL_LINE + "\n"); + sb.append("Hello I'm Duke\nWhat can I do for you?"); + System.out.println(sb.toString()); + return sb.toString(); + } + + public static String chatStartMessage() { + StringBuilder sb = new StringBuilder(); + sb.append("Hello I'm Duke! How may I assist you?\n"); + sb.append("Here are a list of commands you can try:\n\n"); + sb.append("1. [todo] (description) - "); + sb.append("creates new todo task\n"); + sb.append("2. [deadline] (description) [/by] (date) - "); + sb.append("creates new deadline task\n"); + sb.append("3. [event] (description) [/at] (date) - "); + sb.append("creates new event task\n"); + sb.append("4. [list] - shows list of tasks\n"); + sb.append("5. [done] (index) - marks specified task as done\n"); + sb.append("6. [delete] (index) - deletes specified task\n"); + sb.append("7. [clear] - deletes all tasks\n"); + sb.append("8. [find] [key] - prints sublist of tasks that match specified key\n"); + sb.append("9. [edit] (index) [/d or /t] (new field) - "); + sb.append("edits task at specified index with the new field\n\n"); + sb.append("Format: [command] (user input)"); + + return sb.toString(); + } + + /** + * Returns horizontal line + * + * @return Horizontal line. + */ + public static String horizontalLine() { + return Ui.HORIZONTAL_LINE; + } + + /** + * + */ + /** + * Returns goodbye message upon exiting program. + * + * @return Goodbye message as String; + */ + public static String byeMessage() { + return "Bye. Hope to see you again soon!"; + } + + /** + * Prints message upon clearing list of Task objects. + */ + public static String clearedListMessage() { + return "Task list cleared!"; + } + + /** + * Prints message upon failing to confirm clearing of list. + */ + public static void didNotClearListMessage() { + System.out.println("Did NOT clear your task list! " + + "Is there anything else?"); + } + + /** + * Returns message upon successful marking of Task as done. + * + * @param task String representation of Task marked as done. + * @return Message as String. + */ + public static String markDoneMessage(String task) { + return ("Nice! I have marked this task as done:\n\t" + task); + } + + /** + * Returns message upon successful deletion of specified task and current size + * of list of Task objects. + * + * @param task String representation of Task deleted. + * @param size Size of list of Task objects after deletion. + * @return Message as String. + */ + public static String deleteTaskMessage(String task, int size) { + StringBuilder sb = new StringBuilder(); + sb.append("Okay! I have removed this task:\n\t" + task + "\n"); + sb.append("Now you have " + size + " tasks in your list"); + return sb.toString(); + } + + /** + * Returns message upon sucecssful editing of task. + * @param oldTask Old Task before editing. + * @param newTask New Task after editing. + * @return Message as String. + */ + public static String editMessage(Task oldTask, Task newTask) { + StringBuilder sb = new StringBuilder(); + sb.append("Okay! I have edited this task:\n\t ===>"); + sb.append(oldTask.printTask()); + sb.append("\n\t<=== "); + sb.append(newTask.printTask()); + return sb.toString(); + } + + /** + * Returns message upon successful adding of new Task object to list and current + * size of list of Task objects. + * + * @param task String representation of Task added to list. + * @param size Size of list of Task objects after adding new Task. + * @return Message as String. + */ + public static String addTaskMessage(String task, int size) { + StringBuilder sb = new StringBuilder(); + sb.append("Got it! Task added to list.\n"); + sb.append("\t" + task + "\n"); + sb.append("Now you have " + size + " tasks in your list."); + return sb.toString(); + } + + /** + * Returns String representation of all Task objects in the input list. + * @param tasks List of Task objects. + * @param printOrFind Indicator if caller is a print or find method. + * @return List of all Task objects as String. + */ + public static String printList(ArrayList tasks, String printOrFind) { + StringBuilder sb = new StringBuilder(); + if (printOrFind.equals("print")) { + sb.append("Here are your tasks:\n"); + } else { + sb.append("Here are your matching tasks:\n"); + } + + for (int i = 0; i < tasks.size(); i++) { + sb.append(i + 1 + ". " + tasks.get(i).printTask()); + if (i != tasks.size() - 1) { + sb.append("\n"); + } + } + return sb.toString(); + } + + /** + * Returns message if FIND command does not return any matching Task objects. + * + * @return Message as String. + */ + public static String noMatchMessage() { + return ("Sorry! There are no tasks " + + "that match that description.\n"); + } + + /** + * Prints message upon reading input of invalid format. + */ + public static String invalidInputMessage() { + return "Please enter valid input"; + } + + /** + * Prints message upon reading DONE, DELETE input command + * of invalid format. + */ + public static String invalidIndexMessage() { + return "Please enter valid index"; + } + + /** + * Returns a boolean value after user has confirmed or denied a previous + * instruction. + * + * @param sc Scanner object to read inputs. + * @return True if user confirms previous instruction. False if otherwise. + */ + public static boolean promptConfirm(Scanner sc) { + System.out.println("Are you sure? (Y/N)"); + System.out.println(horizontalLine()); + String input = sc.nextLine(); + System.out.println(horizontalLine()); + return input.toLowerCase().equals("y"); + } +} \ No newline at end of file diff --git a/src/main/resources/images/DaCat.jpeg b/src/main/resources/images/DaCat.jpeg new file mode 100644 index 0000000000..ab34b55024 Binary files /dev/null and b/src/main/resources/images/DaCat.jpeg differ diff --git a/src/main/resources/images/DaVrGuy.jpeg b/src/main/resources/images/DaVrGuy.jpeg new file mode 100644 index 0000000000..bacb27dcac Binary files /dev/null and b/src/main/resources/images/DaVrGuy.jpeg differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..91011d3660 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..71424885f5 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + +