diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
new file mode 100644
index 0000000000..fbe40591c9
--- /dev/null
+++ b/.github/workflows/gradle.yml
@@ -0,0 +1,34 @@
+name: Java CI
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ platform: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.platform }}
+
+ steps:
+ - name: Set up repository
+ uses: actions/checkout@master
+
+ - name: Set up repository
+ uses: actions/checkout@master
+ with:
+ ref: master
+
+ - name: Merge to master
+ run: git checkout --progress --force ${{ github.sha }}
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Setup JDK 17
+ uses: actions/setup-java@v1
+ with:
+ java-version: '17'
+ java-package: jdk+fx
+
+ - name: Build and check with Gradle
+ run: ./gradlew check
diff --git a/.gitignore b/.gitignore
index 2873e189e1..b8c129b663 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
# Gradle build files
/.gradle/
/build/
+*.jar
src/main/resources/docs/
# MacOS custom attributes files created by Finder
@@ -13,5 +14,7 @@ src/main/resources/docs/
*.iml
bin/
-/text-ui-test/ACTUAL.TXT
-text-ui-test/EXPECTED-UNIX.TXT
+# Other
+*.class
+data/
+/text-ui-test/**/ACTUAL*.TXT
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000000..d44833ee31
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,62 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'com.github.johnrengelman.shadow' version '7.1.2'
+ id 'checkstyle'
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0'
+ testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0'
+ String javaFxVersion = '17.0.7'
+
+ 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'
+
+ implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2'
+ implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.15.2'
+}
+
+test {
+ useJUnitPlatform()
+
+ testLogging {
+ events "passed", "skipped", "failed"
+
+ showExceptions true
+ exceptionFormat "full"
+ showCauses true
+ showStackTraces true
+ showStandardStreams = false
+ }
+}
+
+application {
+ mainClass.set("rover.main.Launcher")
+}
+
+shadowJar {
+ archiveFileName = "Rover.jar"
+}
+
+checkstyle {
+ toolVersion = "10.2"
+}
+
+run{
+ standardInput = System.in
+}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000000..acac1a8e28
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,434 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
new file mode 100644
index 0000000000..135ea49ee0
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 47b9f984f7..d2e3601063 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,30 +1,174 @@
-# Duke User Guide
+# Rover User Guide
-// Update the title above to match the actual product name
+Rover is a simple desktop app that act as a command-based to-do list manager with an intuitive interface for adding, listing, and managing tasks efficiently.
-// Product screenshot goes here
+## Quick Start
+1. Ensure you have Java 17 or above installed in your Computer.
+**Mac users**: Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html).
-// Product intro goes here
+2. Download the latest Rover.jar file from [here](https://github.com/kumar2215/ip/releases/download/A-Release/Rover.jar).
-## Adding deadlines
+3. Copy the file to the folder you want to use as the home folder for Rover.
-// Describe the action and its outcome.
+4. Open a command terminal, cd into the folder you put the jar file in, and use the java -jar Rover.jar command to run the application.
+A GUI similar to the below should appear in a few seconds.
-// Give examples of usage
+
-Example: `keyword (optional arguments)`
+## Features
-// A description of the expected outcome goes here
+### Adding Deadlines
+You can add a deadline task by using the following command format:
+
+**Example:**
+```plaintext
+deadline Finish report /by 21-02-25 23:59
+```
+
+**Expected Outcome:**
+- A new deadline task is added with the specified due date.
+- The task will be displayed in the task list.
+
+### Adding To-Do Tasks
+
+To add a simple to-do task without deadlines, use:
+
+**Example:**
+```plaintext
+todo Buy groceries
+```
+
+**Expected Outcome:**
+- The task is added without any time constraints.
+
+### Adding Events
+
+You can add an event by specifying a start and end time:
+
+**Example:**
+```plaintext
+event Team meeting /from 20-02-25 10:00 /to 20-02-25 12:00
+```
+
+**Expected Outcome:**
+- An event task is created with a specified time range.
+
+### Listing Tasks
+
+To list all existing tasks, use:
+
+```plaintext
+list
+```
+
+**Expected Outcome:**
+- All saved tasks will be displayed with their statuses.
+
+## Managing Tasks
+
+### Marking a Task as Done
+
+**Example:**
+```plaintext
+mark 1
```
-expected output
+
+**Expected Outcome:**
+- Marks the task at index 1 in the list as done.
+
+### Marking a Task as Not Done
+
+**Example:**
+```plaintext
+unmark 1
+```
+
+**Expected Outcome:**
+- Marks the task at index 1 in the list as not done.
+
+### Deleting a Task
+
+**Example:**
+```plaintext
+delete 1
```
-## Feature ABC
+### Expected Outcome:
+- Deletes the task at index 1 in the list.
-// Feature details
+## Filtering Tasks
+### Find Tasks by Keyword
-## Feature XYZ
+**Example:**
+```plaintext
+find report
+```
+
+**Expected Outcome:**
+- Displays all tasks containing the keyword "report".
+
+### Show Tasks Before a Certain Date
+
+**Example:**
+```plaintext
+show before 01-03-25
+```
+
+**Expected Outcome:**
+- Displays all tasks with deadlines before the specified date.
+
+### Show Tasks After a Certain Date
+
+**Example:**
+```plaintext
+show after 15-02-25
+```
+
+**Expected Outcome:**
+- Displays all tasks with deadlines after the specified date.
+
+## Setting User Preferences
+
+### Set Your Name
+
+**Example:**
+```plaintext
+set name John
+```
+
+**Expected Outcome:**
+- Your name will be updated.
+
+### Set User Image
+
+**Example:**
+```plaintext
+set userImage path/to/image.png (relative to the current directory)
+```
+
+**Expected Outcome:**
+- Your profile picture used in the chat will be updated.
+
+### Set Rover's Image
+
+**Example:**
+```plaintext
+set roverImage path/to/rover.png (relative to the current directory)
+```
+
+**Expected Outcome:**
+- Rover's profile picture used in the chat will be updated.
+
+## Exiting the Application
+
+To exit the program, use:
+
+**Example:**
+```plaintext
+bye
+```
-// Feature details
\ No newline at end of file
+**Expected Outcome:**
+- The program closes safely.
\ No newline at end of file
diff --git a/docs/Ui.png b/docs/Ui.png
new file mode 100644
index 0000000000..6e9d96b8a3
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..033e24c4cd
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..66c01cfeba
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000000..fcb6fca147
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# 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 ;; #(
+ MSYS* | 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# 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"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000000..6689b85bee
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@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=.
+@rem This is normally unused
+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% equ 0 goto execute
+
+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 execute
+
+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
+
+: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 %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 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!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+: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/rover/command/AddCommand.java b/src/main/java/rover/command/AddCommand.java
new file mode 100644
index 0000000000..8671729893
--- /dev/null
+++ b/src/main/java/rover/command/AddCommand.java
@@ -0,0 +1,43 @@
+package rover.command;
+import java.time.format.DateTimeParseException;
+
+import rover.exceptions.RoverException;
+import rover.parser.Parser;
+import rover.task.Task;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command that adds a task to the task list.
+ */
+public final class AddCommand extends Command {
+
+ /**
+ * Constructs an add command with the given arguments.
+ *
+ * @param args The arguments of the command.
+ */
+ public AddCommand(String args) {
+ super(args);
+ }
+
+ /**
+ * Executes the add command to add a task to the task list.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ try {
+ Task newTask = parser.parseTaskDescription(args);
+ taskList.addTask(newTask, ui);
+ } catch (RoverException e) {
+ ui.displayError(e.getMessage());
+ } catch (DateTimeParseException e) {
+ if (e.getMessage().contains("date")) {
+ ui.displayError("The date format should be 'dd/mm/yy'.");
+ }
+ if (e.getMessage().contains("time")) {
+ ui.displayError("The time format should be 'hh:mm'.");
+ }
+ }
+ }
+}
diff --git a/src/main/java/rover/command/Command.java b/src/main/java/rover/command/Command.java
new file mode 100644
index 0000000000..ddfcdaa19c
--- /dev/null
+++ b/src/main/java/rover/command/Command.java
@@ -0,0 +1,62 @@
+package rover.command;
+
+import rover.parser.Parser;
+import rover.storage.Storage;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command that can be executed by the user.
+ */
+public abstract class Command {
+
+ protected final String args;
+
+ /**
+ * Constructs a command with the given arguments.
+ *
+ * @param args The arguments of the command.
+ */
+ public Command(String args) {
+ this.args = args;
+ }
+
+ /**
+ * Executes the command.
+ *
+ * @param taskList The task list to execute the command on.
+ * @param parser The parser to parse the command.
+ * @param ui The user interface to interact with the user.
+ */
+ public abstract void execute(TaskList taskList, Parser parser, Ui ui);
+
+ /**
+ * Executes the command.
+ *
+ * @param taskList The task list to execute the command on.
+ * @param storage The storage to save the task list.
+ * @param ui The user interface to interact with the user.
+ */
+ public void execute(TaskList taskList, Storage storage, Ui ui) {}
+
+ /**
+ * Returns true if the command is an exit command and is false by default.
+ */
+ public boolean isExit() {
+ return false;
+ }
+
+ /**
+ * Checks equality of commands based on their arguments.
+ * Returns true if the argument of the command is equal to the argument of the other command.
+ *
+ * @param obj The object to compare with.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Command other) {
+ return this.args.equals(other.args);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/rover/command/DeleteCommand.java b/src/main/java/rover/command/DeleteCommand.java
new file mode 100644
index 0000000000..1dadc70d80
--- /dev/null
+++ b/src/main/java/rover/command/DeleteCommand.java
@@ -0,0 +1,35 @@
+package rover.command;
+
+import rover.exceptions.RoverException;
+import rover.parser.Parser;
+import rover.task.TaskAction;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to delete a task from the task list.
+ */
+public final class DeleteCommand extends Command {
+
+ /**
+ * Constructs a DeleteCommand object.
+ *
+ * @param args The arguments to the command.
+ */
+ public DeleteCommand(String args) {
+ super(args.substring(6).trim());
+ }
+
+ /**
+ * Executes the command to delete a task from the task list.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ try {
+ int index = parser.parseTaskNumber(args, taskList.getNumberOfTasks(), TaskAction.DELETE);
+ taskList.deleteTask(index, ui);
+ } catch (RoverException e) {
+ ui.displayError(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/rover/command/EmptyCommand.java b/src/main/java/rover/command/EmptyCommand.java
new file mode 100644
index 0000000000..9d6b9cb543
--- /dev/null
+++ b/src/main/java/rover/command/EmptyCommand.java
@@ -0,0 +1,28 @@
+package rover.command;
+
+import rover.parser.Parser;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command that does nothing.
+ */
+public final class EmptyCommand extends Command {
+
+ /**
+ * Constructs an EmptyCommand object.
+ *
+ * @param args The arguments to be passed to the command.
+ */
+ public EmptyCommand(String args) {
+ super(args);
+ }
+
+ /**
+ * Just displays an error message to the user.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ ui.displayError("Please enter a command.");
+ }
+}
diff --git a/src/main/java/rover/command/ExitCommand.java b/src/main/java/rover/command/ExitCommand.java
new file mode 100644
index 0000000000..287c7b3ce0
--- /dev/null
+++ b/src/main/java/rover/command/ExitCommand.java
@@ -0,0 +1,34 @@
+package rover.command;
+
+import rover.parser.Parser;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to exit the program.
+ */
+public final class ExitCommand extends Command {
+
+ /**
+ * Constructs an ExitCommand object.
+ *
+ * @param args The arguments passed to the command.
+ */
+ public ExitCommand(String args) {
+ super(args);
+ }
+
+ /**
+ * Executes nothing as the program is exiting.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {}
+
+ /**
+ * Returns true as the command is an exit command.
+ */
+ @Override
+ public boolean isExit() {
+ return true;
+ }
+}
diff --git a/src/main/java/rover/command/FindCommand.java b/src/main/java/rover/command/FindCommand.java
new file mode 100644
index 0000000000..05609ff8be
--- /dev/null
+++ b/src/main/java/rover/command/FindCommand.java
@@ -0,0 +1,32 @@
+package rover.command;
+
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to find tasks by keyword.
+ */
+public class FindCommand extends ShowCommand {
+
+ /**
+ * Constructor for a FindCommand.
+ *
+ * @param args The arguments for the command.
+ */
+ public FindCommand(String args) {
+ super(args.substring(4).trim());
+ }
+
+ /**
+ * Shows the tasks that contain the keyword.
+ */
+ @Override
+ protected void show(TaskList taskList, Ui ui) {
+ if (args.isEmpty()) {
+ ui.displayError("The keyword to find cannot be empty.");
+ return;
+ }
+ taskList.showTasks(ui, (task, ignore) -> task.toString().contains(args),
+ "with the keyword '" + args + "'");
+ }
+}
diff --git a/src/main/java/rover/command/InvalidCommand.java b/src/main/java/rover/command/InvalidCommand.java
new file mode 100644
index 0000000000..59939bb11a
--- /dev/null
+++ b/src/main/java/rover/command/InvalidCommand.java
@@ -0,0 +1,28 @@
+package rover.command;
+
+import rover.parser.Parser;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents an invalid command.
+ */
+public final class InvalidCommand extends Command {
+
+ /**
+ * Constructs an InvalidCommand object.
+ *
+ * @param args The arguments of the command.
+ */
+ public InvalidCommand(String args) {
+ super(args);
+ }
+
+ /**
+ * Displays the help message to the user.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ ui.showHelpMessage();
+ }
+}
diff --git a/src/main/java/rover/command/ListCommand.java b/src/main/java/rover/command/ListCommand.java
new file mode 100644
index 0000000000..17d91c45b6
--- /dev/null
+++ b/src/main/java/rover/command/ListCommand.java
@@ -0,0 +1,28 @@
+package rover.command;
+
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to list all tasks in the task list.
+ */
+public final class ListCommand extends ShowCommand {
+
+ /**
+ * Constructs a ListCommand object.
+ *
+ * @param args The arguments to be passed to the command.
+ */
+ public ListCommand(String args) {
+ super(args);
+ }
+
+ /**
+ * Executes the command to list all tasks in the task list.
+ */
+ @Override
+ protected void show(TaskList taskList, Ui ui) {
+ taskList.showTasks(ui, (task, ignore) -> true, "in your list");
+ }
+
+}
diff --git a/src/main/java/rover/command/MarkCommand.java b/src/main/java/rover/command/MarkCommand.java
new file mode 100644
index 0000000000..f176781e25
--- /dev/null
+++ b/src/main/java/rover/command/MarkCommand.java
@@ -0,0 +1,35 @@
+package rover.command;
+
+import rover.exceptions.RoverException;
+import rover.parser.Parser;
+import rover.task.TaskAction;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to mark a task as done.
+ */
+public final class MarkCommand extends Command {
+
+ /**
+ * Constructs a MarkCommand object.
+ *
+ * @param args The arguments to the command.
+ */
+ public MarkCommand(String args) {
+ super(args.substring(4).trim());
+ }
+
+ /**
+ * Marks a task as done in the task list.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ try {
+ int index = parser.parseTaskNumber(args, taskList.getNumberOfTasks(), TaskAction.MARK_DONE);
+ taskList.markTask(index, ui);
+ } catch (RoverException e) {
+ ui.displayError(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/rover/command/RetrySaveCommand.java b/src/main/java/rover/command/RetrySaveCommand.java
new file mode 100644
index 0000000000..c77b3551a8
--- /dev/null
+++ b/src/main/java/rover/command/RetrySaveCommand.java
@@ -0,0 +1,48 @@
+package rover.command;
+
+import rover.parser.Parser;
+import rover.storage.Storage;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to retry saving the task list.
+ */
+public final class RetrySaveCommand extends Command {
+
+ /**
+ * Constructs a RetrySaveCommand.
+ *
+ * @param args The user input arguments.
+ */
+ public RetrySaveCommand(String args) {
+ super(args);
+ }
+
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {}
+
+ /**
+ * Retries saving the task list.
+ *
+ * @param taskList The task list.
+ * @param storage The storage.
+ * @param ui The user interface.
+ */
+ @Override
+ public void execute(TaskList taskList, Storage storage, Ui ui) {
+ if (args.equals("y") || args.equals("yes")) {
+ storage.saveAll(taskList, ui.getUserPreferences(), ui);
+ } else {
+ ui.showMessage("Exiting without saving...");
+ }
+ }
+
+ /**
+ * Returns true if the user wants to exit the application.
+ */
+ @Override
+ public boolean isExit() {
+ return args.equals("n") || args.equals("no");
+ }
+}
diff --git a/src/main/java/rover/command/SetCommand.java b/src/main/java/rover/command/SetCommand.java
new file mode 100644
index 0000000000..4996739974
--- /dev/null
+++ b/src/main/java/rover/command/SetCommand.java
@@ -0,0 +1,73 @@
+package rover.command;
+
+import java.nio.file.Paths;
+
+import rover.exceptions.RoverException;
+import rover.parser.Parser;
+import rover.preferences.PreferenceOption;
+import rover.task.TaskList;
+import rover.ui.Gui;
+import rover.ui.TextUi;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to set the user preferences.
+ */
+public class SetCommand extends Command {
+
+ /**
+ * Creates a new SetCommand with the given arguments.
+ *
+ * @param args The arguments for the command.
+ */
+ public SetCommand(String args) {
+ super(args.substring(3).trim());
+ }
+
+ /**
+ * Executes the set command to set the given user preferences.
+ *
+ * @param taskList The task list to be operated on.
+ * @param parser The parser to parse the user input.
+ * @param ui The user interface to interact with the user.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ String[] preferences = args.split(" ");
+ if (preferences.length == 2) {
+ preferences[0] = preferences[0].trim();
+ preferences[1] = preferences[1].trim();
+
+ PreferenceOption preferenceOption;
+ try {
+ preferenceOption = parser.parsePreferenceOption(preferences[0]);
+ } catch (RoverException e) {
+ ui.displayError("Invalid preference! Preference options can only be: name, userImage or roverImage "
+ + "and the value should be a valid file path if setting image.");
+ return;
+ }
+
+ String optionName = String.join(" ", preferenceOption.name().split("_")).toLowerCase();
+ optionName = optionName.substring(0, 1).toUpperCase() + optionName.substring(1);
+ if (ui instanceof TextUi && optionName.contains("image")) {
+ ui.displayError(String.format("%s cannot be set in text mode!", optionName));
+ return;
+ }
+
+ boolean isSuccessful = setOption(ui, preferenceOption, preferences[1]);
+ if (isSuccessful) {
+ ui.showMessage(String.format("%s successfully set to %s", optionName, preferences[1]));
+ }
+ } else {
+ ui.displayError("Invalid input! Please enter the command in the format: set [preference] [value]");
+ }
+ }
+
+ private boolean setOption(Ui ui, PreferenceOption preferenceOption, String value) {
+ return switch (preferenceOption) {
+ case NAME -> ui.setUsername(value);
+ case USER_IMAGE -> ((Gui) ui).setUserImage(Paths.get(value));
+ case ROVER_IMAGE -> ((Gui) ui).setRoverImage(Paths.get(value));
+ };
+ }
+}
diff --git a/src/main/java/rover/command/ShowAfterCommand.java b/src/main/java/rover/command/ShowAfterCommand.java
new file mode 100644
index 0000000000..36c6aa9fb9
--- /dev/null
+++ b/src/main/java/rover/command/ShowAfterCommand.java
@@ -0,0 +1,39 @@
+package rover.command;
+import java.time.format.DateTimeParseException;
+
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to show tasks after a specified date.
+ */
+public final class ShowAfterCommand extends ShowCommand {
+
+ /**
+ * Constructs a ShowAfterCommand.
+ *
+ * @param args The date to show tasks after.
+ */
+ public ShowAfterCommand(String args) {
+ super(args.substring(10).trim());
+ }
+
+ /**
+ * Shows the tasks after the specified date and/or time.
+ */
+ @Override
+ protected void show(TaskList taskList, Ui ui) {
+ taskList.showTasks(ui, (task, wasExceptionThrown) -> {
+ try {
+ if (wasExceptionThrown.get()) {
+ return false;
+ }
+ return task.isAfter(args);
+ } catch (DateTimeParseException e) {
+ ui.displayError("The date format should be 'dd/mm/yy' and the time format should be 'hh:mm'.");
+ wasExceptionThrown.set(true);
+ return false;
+ }
+ }, "after " + args);
+ }
+}
diff --git a/src/main/java/rover/command/ShowBeforeCommand.java b/src/main/java/rover/command/ShowBeforeCommand.java
new file mode 100644
index 0000000000..0d3d1f9e0a
--- /dev/null
+++ b/src/main/java/rover/command/ShowBeforeCommand.java
@@ -0,0 +1,39 @@
+package rover.command;
+import java.time.format.DateTimeParseException;
+
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to show tasks before a specified date.
+ */
+public final class ShowBeforeCommand extends ShowCommand {
+
+ /**
+ * Constructs a ShowBeforeCommand.
+ *
+ * @param args The arguments to the command.
+ */
+ public ShowBeforeCommand(String args) {
+ super(args.substring(11).trim());
+ }
+
+ /**
+ * Shows the tasks before the specified date and/or time.
+ */
+ @Override
+ protected void show(TaskList taskList, Ui ui) {
+ taskList.showTasks(ui, (task, wasExceptionThrown) -> {
+ try {
+ if (wasExceptionThrown.get()) {
+ return false;
+ }
+ return task.isBefore(args);
+ } catch (DateTimeParseException e) {
+ ui.displayError("The date format should be 'dd/mm/yy' and the time format should be 'hh:mm'.");
+ wasExceptionThrown.set(true);
+ return false;
+ }
+ }, "before " + args);
+ }
+}
diff --git a/src/main/java/rover/command/ShowCommand.java b/src/main/java/rover/command/ShowCommand.java
new file mode 100644
index 0000000000..c49862eaba
--- /dev/null
+++ b/src/main/java/rover/command/ShowCommand.java
@@ -0,0 +1,31 @@
+package rover.command;
+
+import rover.parser.Parser;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Abstract class for show commands.
+ */
+public abstract class ShowCommand extends Command {
+
+ public ShowCommand(String args) {
+ super(args);
+ }
+
+ /**
+ * Executes the show command.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ show(taskList, ui);
+ }
+
+ /**
+ * Shows the task list.
+ *
+ * @param taskList The task list to show.
+ * @param ui The user interface to display the task list.
+ */
+ protected abstract void show(TaskList taskList, Ui ui);
+}
diff --git a/src/main/java/rover/command/UnmarkCommand.java b/src/main/java/rover/command/UnmarkCommand.java
new file mode 100644
index 0000000000..7ba8d9abb4
--- /dev/null
+++ b/src/main/java/rover/command/UnmarkCommand.java
@@ -0,0 +1,35 @@
+package rover.command;
+
+import rover.exceptions.RoverException;
+import rover.parser.Parser;
+import rover.task.TaskAction;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Represents a command to unmark a task as done.
+ */
+public final class UnmarkCommand extends Command {
+
+ /**
+ * Constructs a new UnmarkCommand object.
+ *
+ * @param args The arguments to the command.
+ */
+ public UnmarkCommand(String args) {
+ super(args.substring(6).trim());
+ }
+
+ /**
+ * Marks the task as not done in the task list.
+ */
+ @Override
+ public void execute(TaskList taskList, Parser parser, Ui ui) {
+ try {
+ int index = parser.parseTaskNumber(args, taskList.getNumberOfTasks(), TaskAction.MARK_UNDONE);
+ taskList.unmarkTask(index, ui);
+ } catch (RoverException e) {
+ ui.displayError(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/rover/exceptions/RoverException.java b/src/main/java/rover/exceptions/RoverException.java
new file mode 100644
index 0000000000..f52028ce2a
--- /dev/null
+++ b/src/main/java/rover/exceptions/RoverException.java
@@ -0,0 +1,21 @@
+package rover.exceptions;
+
+/**
+ * RoverException is a custom exception class that extends the Exception class.
+ * It is used to throw exceptions when the rover chatBot is not able to function properly.
+ */
+public final class RoverException extends Exception {
+ public RoverException(String message) {
+ super(message);
+ }
+
+ /**
+ * Overridden toString method to include the class name in the exception message.
+ *
+ * @return String
+ */
+ @Override
+ public String toString() {
+ return "RoverException: " + this.getMessage();
+ }
+}
diff --git a/src/main/java/rover/main/Launcher.java b/src/main/java/rover/main/Launcher.java
new file mode 100644
index 0000000000..75ed9fa5e4
--- /dev/null
+++ b/src/main/java/rover/main/Launcher.java
@@ -0,0 +1,12 @@
+package rover.main;
+import javafx.application.Application;
+
+/**
+ * A launcher class to workaround classpath issues.
+ */
+public class Launcher {
+ public static void main(String[] args) {
+ assert args != null : "Arguments array should not be null";
+ Application.launch(Main.class, args);
+ }
+}
diff --git a/src/main/java/rover/main/Main.java b/src/main/java/rover/main/Main.java
new file mode 100644
index 0000000000..4d64003168
--- /dev/null
+++ b/src/main/java/rover/main/Main.java
@@ -0,0 +1,35 @@
+package rover.main;
+import java.io.IOException;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.AnchorPane;
+import javafx.stage.Stage;
+import rover.ui.Gui;
+
+/**
+ * A GUI for Rover using FXML.
+ */
+public class Main extends Application {
+
+ private final Rover rover = new Rover();
+
+ @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("Rover - Your Personal Task Manager");
+ stage.setMinHeight(220);
+ stage.setMinWidth(417);
+ fxmlLoader.getController().setRover(rover);
+ stage.show();
+ rover.startSession();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/rover/main/Rover.java b/src/main/java/rover/main/Rover.java
new file mode 100644
index 0000000000..bc6133af2d
--- /dev/null
+++ b/src/main/java/rover/main/Rover.java
@@ -0,0 +1,115 @@
+package rover.main;
+import java.time.format.DateTimeParseException;
+
+import rover.command.Command;
+import rover.exceptions.RoverException;
+import rover.parser.Parser;
+import rover.preferences.UserPreferences;
+import rover.storage.Storage;
+import rover.task.TaskList;
+import rover.ui.TextUi;
+import rover.ui.Ui;
+
+/**
+ * Rover is a personal task manager that helps users keep track of their tasks.
+ **/
+public final class Rover {
+
+ private static final String DEFAULT_TASKS_FILE_PATH = "data/Tasks.txt";
+ private static final String DEFAULT_PREFERENCES_FILE_PATH = "data/Preferences.json";
+
+ private UserPreferences userPreferences;
+ private final Storage storage;
+ private TaskList taskList;
+ private final Parser parser;
+ private Ui ui;
+
+ /**
+ * Creates a new Rover instance with the default file path.
+ */
+ public Rover() {
+ this(DEFAULT_TASKS_FILE_PATH, DEFAULT_PREFERENCES_FILE_PATH);
+ }
+
+ /**
+ * Creates a new Rover instance by loading tasks from the specified file path.
+ *
+ * @param tasksFilePath The file path to save and load tasks from.
+ * @param preferencesFilePath The file path to save and load preferences from.
+ */
+ private Rover(String tasksFilePath, String preferencesFilePath) {
+ parser = new Parser();
+ ui = new TextUi();
+ storage = new Storage(tasksFilePath, preferencesFilePath);
+ }
+
+ /**
+ * Sets the Ui instance for Rover.
+ */
+ public void setUi(Ui ui) {
+ this.ui = ui;
+ this.userPreferences = new UserPreferences(storage.loadPreferences(ui));
+ this.ui.setUserPreferences(userPreferences);
+ }
+
+ /**
+ * Starts the Rover session by displaying the welcome message.
+ */
+ public void startSession() {
+ ui.showWelcome();
+ try {
+ taskList = new TaskList(ui, storage.loadTasks(ui));
+ } catch (RoverException | DateTimeParseException e) {
+ ui.displayError("Could not load saved tasks properly. Saved tasks could be corrupted.");
+ taskList = new TaskList();
+ }
+ }
+
+ /**
+ * Handles the response from Rover based on the user input.
+ */
+ public boolean handleResponse(String input) {
+ Command command = parser.parseCommand(input);
+ command.execute(taskList, parser, ui);
+ return command.isExit();
+ }
+
+ /**
+ * Ends the Rover session by saving the tasks and displaying the goodbye message.
+ */
+ public void endSession() {
+ storage.saveAll(taskList, userPreferences, ui);
+ while (!storage.isSavedSuccessfully()) {
+ ui.displayError("Could not save tasks. Try again? (Y/N)");
+ String input = ui.readCommand();
+ Command command = parser.parseCommand(input);
+ command.execute(taskList, storage, ui);
+ if (command.isExit()) {
+ break;
+ }
+ }
+ ui.sayBye();
+ }
+
+ /**
+ * Runs the Rover program.
+ */
+ private void run() {
+ setUi(ui);
+ startSession();
+ boolean isExit = false;
+ while (!isExit) {
+ isExit = handleResponse(ui.readCommand());
+ }
+ endSession();
+ }
+
+ /**
+ * Creates a new Rover instance and runs the program.
+ *
+ * @param args Command line arguments.
+ */
+ public static void main(String[] args) {
+ new Rover(DEFAULT_TASKS_FILE_PATH, DEFAULT_PREFERENCES_FILE_PATH).run();
+ }
+}
diff --git a/src/main/java/rover/parser/DateTimeParser.java b/src/main/java/rover/parser/DateTimeParser.java
new file mode 100644
index 0000000000..cc92f181ae
--- /dev/null
+++ b/src/main/java/rover/parser/DateTimeParser.java
@@ -0,0 +1,91 @@
+package rover.parser;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+/**
+ * A utility class for parsing date and time strings.
+ */
+public final class DateTimeParser {
+
+ public static final String[] DATE_FORMATS = {
+ "yyyy/MM/dd",
+ "yyyy-MM-dd",
+ "dd-MM-yyyy",
+ "dd/MM/yyyy",
+ "dd/MM/yy",
+ "dd-MM-yy"
+ };
+
+ public static final String[] TIME_FORMATS = {
+ "HH:mm",
+ "HHmm",
+ "h:mma",
+ "h.mma",
+ "hha",
+ "ha"
+ };
+
+ /**
+ * Parses a date and time string using the supported formats.
+ *
+ * @param dateTimeString the date and time string to parse
+ * @return LocalDateTime object representing the parsed date and time
+ * @throws DateTimeParseException if the string cannot be parsed
+ */
+ public static LocalDateTime parseDateTime(String dateTimeString) throws DateTimeParseException {
+ assert dateTimeString != null : "Date and time string should not be null";
+ for (String dateFormat : DATE_FORMATS) {
+ for (String timeFormat : TIME_FORMATS) {
+ String format = dateFormat + " " + timeFormat;
+ try {
+ return LocalDateTime.parse(dateTimeString, DateTimeFormatter.ofPattern(format));
+ } catch (DateTimeParseException e) {
+ // Continue to the next formatter
+ }
+ }
+ }
+ throw new DateTimeParseException("Unable to parse date and time: " + dateTimeString, dateTimeString, 0);
+ }
+
+ /**
+ * Parses a date string using the supported formats.
+ *
+ * @param dateString the date string to parse
+ * @return LocalDate object representing the parsed date
+ * @throws DateTimeParseException if the string cannot be parsed
+ */
+ public static LocalDate parseDate(String dateString) throws DateTimeParseException {
+ assert dateString != null : "Date string should not be null";
+ for (String format : DATE_FORMATS) {
+ try {
+ return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(format));
+ } catch (DateTimeParseException e) {
+ // Continue to the next formatter
+ }
+ }
+ throw new DateTimeParseException("Unable to parse date: " + dateString, dateString, 0);
+ }
+
+ /**
+ * Parses a time string using the supported formats.
+ *
+ * @param timeString the time string to parse
+ * @return LocalTime object representing the parsed time
+ * @throws DateTimeParseException if the string cannot be parsed
+ */
+ public static LocalTime parseTime(String timeString) throws DateTimeParseException {
+ assert timeString != null : "Time string should not be null";
+ for (String format : TIME_FORMATS) {
+ try {
+ return LocalTime.parse(timeString, DateTimeFormatter.ofPattern(format));
+ } catch (DateTimeParseException e) {
+ // Continue to the next formatter
+ }
+ }
+ throw new DateTimeParseException("Unable to parse time: " + timeString, timeString, 0);
+ }
+}
diff --git a/src/main/java/rover/parser/Parser.java b/src/main/java/rover/parser/Parser.java
new file mode 100644
index 0000000000..cb72b7cd02
--- /dev/null
+++ b/src/main/java/rover/parser/Parser.java
@@ -0,0 +1,156 @@
+package rover.parser;
+
+import java.time.format.DateTimeParseException;
+
+import rover.command.AddCommand;
+import rover.command.Command;
+import rover.command.DeleteCommand;
+import rover.command.EmptyCommand;
+import rover.command.ExitCommand;
+import rover.command.FindCommand;
+import rover.command.InvalidCommand;
+import rover.command.ListCommand;
+import rover.command.MarkCommand;
+import rover.command.RetrySaveCommand;
+import rover.command.SetCommand;
+import rover.command.ShowAfterCommand;
+import rover.command.ShowBeforeCommand;
+import rover.command.UnmarkCommand;
+import rover.exceptions.RoverException;
+import rover.preferences.PreferenceOption;
+import rover.task.Deadline;
+import rover.task.Event;
+import rover.task.Task;
+import rover.task.TaskAction;
+import rover.task.Todo;
+
+/**
+ * Parser class to parse user input
+ */
+public final class Parser {
+
+ private boolean isPreviousCommandBye = false;
+
+ /**
+ * Parses the user input and returns the corresponding command
+ *
+ * @param args User input
+ * @return Command object
+ */
+ public Command parseCommand(String args) {
+ String stdInput = args.trim().toLowerCase();
+ if (stdInput.isEmpty()) {
+ return new EmptyCommand(args);
+ } else if (stdInput.equals("bye")) {
+ isPreviousCommandBye = true;
+ return new ExitCommand(args);
+ } else if (isPreviousCommandBye && (stdInput.equals("y") || stdInput.equals("n")
+ || stdInput.equals("yes") || stdInput.equals("no"))) {
+ return new RetrySaveCommand(args);
+ } else if (stdInput.equals("list")) {
+ return new ListCommand(args);
+ } else if (stdInput.startsWith("find")) {
+ return new FindCommand(args);
+ } else if (stdInput.startsWith("mark")) {
+ return new MarkCommand(args);
+ } else if (stdInput.startsWith("unmark")) {
+ return new UnmarkCommand(args);
+ } else if (stdInput.startsWith("delete")) {
+ return new DeleteCommand(args);
+ } else if (stdInput.startsWith("set")) {
+ return new SetCommand(args);
+ } else if (stdInput.startsWith("show before")) {
+ return new ShowBeforeCommand(args);
+ } else if (stdInput.startsWith("show after")) {
+ return new ShowAfterCommand(args);
+ } else if (stdInput.startsWith("todo") || stdInput.startsWith("deadline") || stdInput.startsWith("event")) {
+ return new AddCommand(args);
+ } else {
+ return new InvalidCommand(args);
+ }
+ }
+
+ /**
+ * Parses the preference option and returns the corresponding preference option
+ *
+ * @param option Preference option specified by the user
+ * @return Preference option
+ * @throws RoverException If the preference option is invalid
+ */
+ public PreferenceOption parsePreferenceOption(String option) throws RoverException {
+ return switch (option) {
+ case "name" -> PreferenceOption.NAME;
+ case "userImage" -> PreferenceOption.USER_IMAGE;
+ case "roverImage" -> PreferenceOption.ROVER_IMAGE;
+ default -> throw new RoverException("Invalid preference option");
+ };
+ }
+
+ /**
+ * Parses the task description and returns the corresponding task object
+ *
+ * @param description Task description
+ * @return Task object
+ * @throws RoverException If the task description is invalid
+ * @throws DateTimeParseException If the date and time format is invalid
+ */
+ public Task parseTaskDescription(String description) throws RoverException, DateTimeParseException {
+ Task newTask;
+ description = description.trim();
+ if (description.toLowerCase().startsWith("deadline")) {
+ newTask = new Deadline(description.substring(8).trim());
+ } else if (description.toLowerCase().startsWith("event")) {
+ newTask = new Event(description.substring(5).trim());
+ } else if (description.toLowerCase().startsWith("todo")) {
+ newTask = new Todo(description.substring(4).trim());
+ } else {
+ throw new RoverException("Not a valid task type.");
+ }
+ return newTask;
+ }
+
+ /**
+ * Parses the task number and returns the corresponding task index
+ *
+ * @param taskNumber Task number
+ * @param numberOfTasks Number of tasks
+ * @param taskAction Task action used to determine error message
+ * @return Task index
+ * @throws RoverException If the task number is invalid
+ */
+ public int parseTaskNumber(String taskNumber, int numberOfTasks, TaskAction taskAction) throws RoverException {
+ String action = getAction(taskAction);
+ int index = getIndex(taskNumber, action);
+ checkValidIndex(numberOfTasks, index, action);
+ return index;
+ }
+
+ private String getAction(TaskAction taskAction) {
+ return switch (taskAction) {
+ case MARK_DONE -> "marked as done";
+ case MARK_UNDONE -> "marked as not done";
+ case DELETE -> "deleted";
+ default -> throw new AssertionError("Invalid task action");
+ };
+ }
+
+ private int getIndex(String taskNumber, String action) throws RoverException {
+ if (taskNumber.isEmpty()) {
+ throw new RoverException("Please specify the task number to be " + action + ".");
+ }
+ int index;
+ try {
+ index = Integer.parseInt(taskNumber) - 1;
+ } catch (NumberFormatException e) {
+ throw new RoverException("Please specify a valid task number to be " + action + ".");
+ }
+ return index;
+ }
+
+ private void checkValidIndex(int numberOfTasks, int index, String action) throws RoverException {
+ if (index < 0 || index >= numberOfTasks) {
+ throw new RoverException("Please specify a valid task number to be " + action + ".\n"
+ + "You only have " + numberOfTasks + " tasks in total.");
+ }
+ }
+}
diff --git a/src/main/java/rover/preferences/PreferenceOption.java b/src/main/java/rover/preferences/PreferenceOption.java
new file mode 100644
index 0000000000..2e10833403
--- /dev/null
+++ b/src/main/java/rover/preferences/PreferenceOption.java
@@ -0,0 +1,10 @@
+package rover.preferences;
+
+/**
+ * Represents the options for user preferences.
+ */
+public enum PreferenceOption {
+ NAME,
+ USER_IMAGE,
+ ROVER_IMAGE
+}
diff --git a/src/main/java/rover/preferences/UserPreferences.java b/src/main/java/rover/preferences/UserPreferences.java
new file mode 100644
index 0000000000..db9886691b
--- /dev/null
+++ b/src/main/java/rover/preferences/UserPreferences.java
@@ -0,0 +1,78 @@
+package rover.preferences;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Represents the user preferences.
+ */
+public class UserPreferences {
+
+ private ObjectNode jsonNode;
+
+ /**
+ * Creates a new UserPreferences object with default values.
+ */
+ public UserPreferences() {
+ ObjectMapper objectMapper = new ObjectMapper();
+ this.jsonNode = objectMapper.createObjectNode();
+ this.jsonNode.put("name", "");
+ this.jsonNode.put("userImage", "default");
+ this.jsonNode.put("roverImage", "default");
+ }
+
+ /**
+ * Creates a new UserPreferences object.
+ *
+ * @param jsonNode The JSON node representing the user preferences.
+ */
+ public UserPreferences(ObjectNode jsonNode) {
+ this();
+ if (jsonNode != null) {
+ this.jsonNode = jsonNode;
+ }
+ }
+
+ /**
+ * Sets the name of the user.
+ *
+ * @param name The name of the user.
+ * @return True if the name is successfully set, false otherwise.
+ */
+ public boolean setName(String name) {
+ jsonNode.put("name", name);
+ return jsonNode.get("name").asText().equals(name);
+ }
+
+ /**
+ * Sets the image of the user.
+ *
+ * @param userImagePath The path to the image of the user.
+ * @return True if the image is successfully set, false otherwise.
+ */
+ public boolean setUserImage(String userImagePath) {
+ jsonNode.put("userImage", userImagePath);
+ return jsonNode.get("userImage").asText().equals(userImagePath);
+ }
+
+ /**
+ * Sets the image of the rover.
+ *
+ * @param roverImagePath The path to the image of the rover.
+ * @return True if the image is successfully set, false otherwise.
+ */
+ public boolean setRoverImage(String roverImagePath) {
+ jsonNode.put("roverImage", roverImagePath);
+ return jsonNode.get("roverImage").asText().equals(roverImagePath);
+ }
+
+ /**
+ * Returns the JSON node representing the user preferences.
+ *
+ * @return The JSON node representing the user preferences.
+ */
+ public JsonNode getJsonNode() {
+ return jsonNode;
+ }
+}
diff --git a/src/main/java/rover/storage/JsonFileManager.java b/src/main/java/rover/storage/JsonFileManager.java
new file mode 100644
index 0000000000..b64c03effc
--- /dev/null
+++ b/src/main/java/rover/storage/JsonFileManager.java
@@ -0,0 +1,45 @@
+package rover.storage;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Represents a writer that writes tasks to a JSON file.
+ */
+public class JsonFileManager {
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Creates a new JsonFileManager object.
+ */
+ public JsonFileManager() {
+ this.objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * Loads the JSON node from the specified file path.
+ *
+ * @param preferencesFilePath The file path to load the JSON node from.
+ * @return The JSON node loaded from the file path.
+ * @throws IOException If an I/O error occurs.
+ */
+ public JsonNode load(String preferencesFilePath) throws IOException {
+ return objectMapper.readTree(new File(preferencesFilePath));
+ }
+
+ /**
+ * Writes the specified JSON node to the specified file path.
+ *
+ * @param jsonNode The JSON node to write.
+ * @param preferencesFilePath The file path to write the JSON node to.
+ * @throws IOException If an I/O error occurs.
+ */
+ public void write(JsonNode jsonNode, String preferencesFilePath) throws IOException {
+ objectMapper.writeValue(new File(preferencesFilePath), jsonNode);
+ }
+
+}
diff --git a/src/main/java/rover/storage/Storage.java b/src/main/java/rover/storage/Storage.java
new file mode 100644
index 0000000000..4519cf35dc
--- /dev/null
+++ b/src/main/java/rover/storage/Storage.java
@@ -0,0 +1,153 @@
+package rover.storage;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import rover.preferences.UserPreferences;
+import rover.task.Task;
+import rover.task.TaskList;
+import rover.ui.Ui;
+
+/**
+ * Handles the storage of the tasks in the file system.
+ */
+public final class Storage {
+
+ private static final String NEW_LINE = System.lineSeparator();
+ private final Path tasksFilePath;
+ private final Path preferencesFilePath;
+ private boolean isTasksSaved = false;
+ private boolean isPreferencesSaved = false;
+ private final JsonFileManager jsonFileManager = new JsonFileManager();
+
+ /**
+ * Returns a new Storage object with the specified file path.
+ *
+ * @param tasksFilePath The file path to save and load tasks from.
+ * @param preferencesFilePath The file path to save and load preferences from.
+ */
+ public Storage(String tasksFilePath, String preferencesFilePath) {
+ String cwd = System.getProperty("user.dir");
+ this.tasksFilePath = Paths.get(cwd, tasksFilePath.split("/"));
+ this.preferencesFilePath = Paths.get(cwd, preferencesFilePath.split("/"));
+ }
+
+ /**
+ * Loads the tasks from the file system.
+ *
+ * @param ui The Ui object to display messages.
+ * @return An array of strings representing the tasks.
+ */
+ public String[] loadTasks(Ui ui) {
+ try {
+ boolean fileExists = Files.exists(tasksFilePath);
+ if (!fileExists) {
+ return new String[0];
+ } else {
+ Stream lines = Files.lines(tasksFilePath).filter(line -> !line.isBlank());
+ String[] tasks = lines.toArray(String[]::new);
+ lines.close();
+ return tasks;
+ }
+ } catch (IOException e) {
+ ui.displayError("Failed to load tasks.");
+ return new String[0];
+ }
+ }
+
+ /**
+ * Loads the preferences from the file system.
+ *
+ * @param ui The Ui object to display messages.
+ * @return The JSON node representing the preferences.
+ */
+ public ObjectNode loadPreferences(Ui ui) {
+ try {
+ boolean fileExists = Files.exists(preferencesFilePath);
+ if (!fileExists) {
+ return (ObjectNode) (new UserPreferences()).getJsonNode();
+ }
+ return (ObjectNode) jsonFileManager.load(preferencesFilePath.toString());
+ } catch (IOException e) {
+ ui.displayError("Failed to load preferences.");
+ return (ObjectNode) (new UserPreferences()).getJsonNode();
+ }
+ }
+
+ /**
+ * Saves tasks and preferences to the file system.
+ *
+ * @param taskList The TaskList object containing the tasks to be saved.
+ * @param userPreferences The UserPreferences object containing the preferences to be saved.
+ * @param ui The Ui object to display messages.
+ */
+ public void saveAll(TaskList taskList, UserPreferences userPreferences, Ui ui) {
+ assert taskList != null : "TaskList should not be null.";
+ assert userPreferences != null : "UserPreferences should not be null.";
+ assert ui != null : "Ui should not be null.";
+ String response = "Saving your tasks...";
+ saveTasks(taskList);
+ response += NEW_LINE + (isTasksSaved ? "Tasks saved successfully!" : "Failed to save tasks.");
+
+ response += NEW_LINE + "Saving your preferences...";
+ savePreferences(userPreferences);
+ response += NEW_LINE + (isPreferencesSaved ? "Preferences saved successfully!" : "Failed to save preferences.");
+
+ if (isTasksSaved && isPreferencesSaved) {
+ ui.showMessageWithoutLineSeparator(response);
+ } else {
+ ui.displayError(response);
+ }
+ }
+
+ /**
+ * Saves the tasks to the file system and updates the isSaved field.
+ *
+ * @param taskList The TaskList object containing the tasks to be saved.
+ */
+ private void saveTasks(TaskList taskList) {
+ assert taskList != null : "TaskList should not be null.";
+ try {
+ Files.createDirectories(tasksFilePath.getParent());
+ Files.deleteIfExists(tasksFilePath);
+ Files.createFile(tasksFilePath);
+ String tasksString = taskList.getTasks().stream()
+ .map(Task::getTaskString)
+ .collect(Collectors.joining("\n"));
+ Files.writeString(tasksFilePath, tasksString, StandardOpenOption.WRITE);
+ isTasksSaved = true;
+ } catch (IOException e) {
+ isTasksSaved = false;
+ }
+ }
+
+ /**
+ * Saves the preferences to the file system and updates the isSaved field.
+ *
+ * @param userPreferences The UserPreferences object containing the preferences to be saved.
+ */
+ private void savePreferences(UserPreferences userPreferences) {
+ assert userPreferences != null : "UserPreferences should not be null.";
+ try {
+ jsonFileManager.write(userPreferences.getJsonNode(), preferencesFilePath.toString());
+ isPreferencesSaved = true;
+ } catch (IOException e) {
+ isPreferencesSaved = false;
+ }
+ }
+
+ /**
+ * Returns whether the tasks were saved successfully.
+ *
+ * @return True if the tasks were saved successfully, false otherwise.
+ */
+ public boolean isSavedSuccessfully() {
+ return isTasksSaved && isPreferencesSaved;
+ }
+}
diff --git a/src/main/java/rover/task/Deadline.java b/src/main/java/rover/task/Deadline.java
new file mode 100644
index 0000000000..6c4813f5ac
--- /dev/null
+++ b/src/main/java/rover/task/Deadline.java
@@ -0,0 +1,168 @@
+package rover.task;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import rover.exceptions.RoverException;
+import rover.parser.DateTimeParser;
+import rover.ui.Ui;
+
+/**
+ * Represents a deadline task that can be added to the task list.
+ * A deadline task has a description, a status that indicates whether it is done, and a deadline.
+ */
+public final class Deadline extends Task {
+
+ private LocalDate byDate;
+ private LocalTime byTime;
+ private String by;
+ private String byFullFormat;
+
+ /**
+ * Constructs a deadline task with the given description.
+ *
+ * @param description The description of the deadline task.
+ * @throws RoverException If the description is not in the correct format.
+ */
+ public Deadline(String description) throws RoverException, DateTimeParseException {
+ this(description, null);
+ }
+
+ /**
+ * Constructs a deadline task with the given description.
+ * * The description must be in the format "task /by (deadline)".
+ * The deadline can be a date, time, or date and time.
+ * If the deadline is a date, the time will be set to 00:00.
+ * If the deadline is a time, the date will be set to the current date.
+ * If the deadline is a date and time, the date and time will be set accordingly.
+ *
+ * @param description The description of the deadline task.
+ * @param ui The ui object to display messages.
+ * @throws RoverException If the description is not in the correct format.
+ */
+ public Deadline(String description, Ui ui) throws RoverException, DateTimeParseException {
+ super(description);
+ setByAndDescription(description);
+ setByDateAndTime(ui);
+ setByFullFormat();
+ }
+
+ private void setByAndDescription(String description) throws RoverException {
+ String[] parts = description.split(" /by ");
+ this.description = parts[0];
+ if (parts.length != 2) {
+ throw new RoverException("A deadline task must be a task followed with '/by (deadline)'.");
+ }
+ this.by = parts[1];
+ }
+
+ private void setByDateAndTime(Ui ui) throws DateTimeParseException, RoverException {
+ String[] dateAndTime = by.split(" ");
+ if (dateAndTime.length == 1) {
+ // Deadline is a date only
+ // The case where only time is given is omitted as it doesn't make sense
+ // to create a deadline task on the same day with only a time
+ this.byDate = DateTimeParser.parseDate(dateAndTime[0]);
+ this.byTime = LocalTime.MAX; // Set to the end of the day
+ if (byDate.isBefore(LocalDate.now())) {
+ handleOverDue(ui, String.format("The following deadline: %s is overdue.", this.description),
+ "The deadline cannot be in the past.");
+ }
+ } else {
+ // Deadline is a date and time
+ this.byDate = DateTimeParser.parseDate(dateAndTime[0]);
+ this.byTime = DateTimeParser.parseTime(dateAndTime[1]);
+ if (byDate.isBefore(LocalDate.now()) || (byDate.isEqual(LocalDate.now())
+ && byTime.isBefore(LocalTime.now()))) {
+ handleOverDue(ui, String.format("The following deadline: %s is overdue.", this.description),
+ "The deadline cannot be in the past.");
+ }
+ }
+ }
+
+ private void handleOverDue(Ui ui, String warning, String error) throws RoverException {
+ if (ui != null) {
+ ui.showMessage(warning);
+ return;
+ }
+ throw new RoverException(error);
+ }
+
+ private void setByFullFormat() {
+ this.byFullFormat = byDate.format(DateTimeFormatter.ofPattern("EEEE, dd MMMM, yyyy"));
+ this.byFullFormat += " " + byTime.format(DateTimeFormatter.ofPattern("h:mm a")).toLowerCase();
+ }
+
+ /**
+ * Checks if the task is due before the given date and time.
+ */
+ @Override
+ public boolean isBefore(String dateTime) throws DateTimeParseException {
+ String[] parts = dateTime.split(" ");
+ if (parts.length == 1) {
+ try { // Interpret as a date only
+ LocalDate otherDate = DateTimeParser.parseDate(dateTime);
+ return byDate.isBefore(otherDate);
+ } catch (DateTimeParseException e) {
+ // Interpret as a time only
+ LocalDateTime otherTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime);
+ return byDate.atTime(byTime).isBefore(otherTime);
+ }
+ } else { // Interpret as a date and time
+ LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime);
+ return byDate.atTime(byTime).isBefore(otherDateTime);
+ }
+ }
+
+ /**
+ * Checks if the task is due after the given date and time.
+ */
+ @Override
+ public boolean isAfter(String dateTime) {
+ String[] parts = dateTime.split(" ");
+ if (parts.length == 1) {
+ try { // Interpret as a date only
+ LocalDate otherDate = DateTimeParser.parseDate(dateTime);
+ return byDate.isAfter(otherDate);
+ } catch (DateTimeParseException e) {
+ // Interpret as a time only
+ LocalDateTime otherTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime);
+ return byDate.atTime(byTime).isAfter(otherTime);
+ }
+ } else { // Interpret as a date and time
+ LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime);
+ return byDate.atTime(byTime).isAfter(otherDateTime);
+ }
+ }
+
+ /**
+ * Compares this deadline task with the specified object for equality.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Deadline other) {
+ return this.description.equals(other.description) && this.byDate.equals(other.byDate)
+ && this.byTime.equals(other.byTime);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the description of the deadline task for saving to the file.
+ */
+ @Override
+ public String getTaskString() {
+ return "D | " + (isDone ? "1" : "0") + " | " + description + " /by " + by;
+ }
+
+ /**
+ * Returns the string representation of the deadline task.
+ */
+ @Override
+ public String toString() {
+ return "[D]" + super.toString() + " (by: " + byFullFormat + ")";
+ }
+
+}
diff --git a/src/main/java/rover/task/Event.java b/src/main/java/rover/task/Event.java
new file mode 100644
index 0000000000..fa76fa4cf0
--- /dev/null
+++ b/src/main/java/rover/task/Event.java
@@ -0,0 +1,227 @@
+package rover.task;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import rover.exceptions.RoverException;
+import rover.parser.DateTimeParser;
+import rover.ui.Ui;
+
+/**
+ * Represents an event task.
+ * An event task is a task that has a start date and/or time and an end date and/or time.
+ */
+public final class Event extends Task {
+
+ private LocalDate startDate;
+ private LocalTime startTime;
+ private LocalDate endDate;
+ private LocalTime endTime;
+ private String start;
+ private String end;
+ private String fromToFullFormat;
+
+ /**
+ * Constructs an event task with the given description.
+ *
+ * @param description The description of the event task.
+ * @throws RoverException If the description is not in the correct format or the start date and time is after
+ * the end date and time.
+ */
+ public Event(String description) throws RoverException, DateTimeParseException {
+ this(description, null);
+ }
+
+ /**
+ * Constructs an event task with the given description.
+ * The description must be in the format "task /from (start) /to (end)".
+ * The start and end can be a date, time, or date and time.
+ * If the start or end is a date, the time will be set to 00:00.
+ * If the start or end is a time, the date will be set to the current date.
+ * If the start or end is a date and time, the date and time will be set accordingly.
+ * The start date and time must be before the end date and time.
+ *
+ * @param description The description of the event task.
+ * @param ui The ui object to display messages.
+ * @throws RoverException If the description is not in the correct format or the start date and time is after
+ * the end date and time.
+ */
+ public Event(String description, Ui ui) throws RoverException, DateTimeParseException {
+ super(description);
+ setStartAndEnd();
+ setStartDateAndTime(ui);
+ setEndDateAndTime(ui);
+ checkIfEndIsAfterStart();
+ setFromToFullFormat();
+ }
+
+ private void setFromToFullFormat() {
+ fromToFullFormat = "from "
+ + startDate.format(DateTimeFormatter.ofPattern("EEEE, dd MMMM, yyyy")) + " "
+ + startTime.format(DateTimeFormatter.ofPattern("h:mm a")).toLowerCase() + " to "
+ + endDate.format(DateTimeFormatter.ofPattern("EEEE, dd MMMM, yyyy")) + " "
+ + endTime.format(DateTimeFormatter.ofPattern("h:mm a")).toLowerCase();
+ }
+
+ private void checkIfEndIsAfterStart() throws RoverException {
+ if (startDate.isAfter(endDate) || (startDate.isEqual(endDate) && startTime.isAfter(endTime))) {
+ throw new RoverException("The start date and time must be before the end date and time.");
+ }
+ }
+
+ private void setStartAndEnd() throws RoverException {
+ String[] parts = description.split(" /from ");
+ if (parts.length != 2) {
+ throw new RoverException("An event task must be a task followed with '/from (start) /to (end)'.");
+ }
+ this.description = parts[0];
+ String[] parts2 = parts[1].split(" /to ");
+ if (parts2.length != 2) {
+ throw new RoverException("An event task must be a task followed with '/from (start) /to (end)'.");
+ }
+ this.start = parts2[0];
+ this.end = parts2[1];
+ }
+
+ private void setStartDateAndTime(Ui ui) throws DateTimeParseException, RoverException {
+ try { // Try to parse the start as a date and time
+ startDate = DateTimeParser.parseDateTime(start).toLocalDate();
+ startTime = DateTimeParser.parseDateTime(start).toLocalTime();
+ if (startDate.isBefore(LocalDate.now()) || (startDate.isEqual(LocalDate.now())
+ && startTime.isBefore(LocalTime.now()))) {
+ handleOverDue(ui, String.format("The following event: %s has already transpired.", this.description),
+ "The start date and time cannot be in the past.");
+ }
+ } catch (DateTimeParseException e) {
+ try { // Try to parse the start as a date only
+ this.startDate = DateTimeParser.parseDate(start);
+ this.startTime = LocalTime.of(0, 0);
+ if (startDate.isBefore(LocalDate.now())) {
+ handleOverDue(ui, String.format("The following event: %s has already transpired.",
+ this.description), "The start date cannot be in the past.");
+ }
+ } catch (DateTimeParseException e2) {
+ // Try to parse the start as a time only
+ this.startDate = LocalDate.now();
+ this.startTime = DateTimeParser.parseTime(start);
+ if (startTime.isBefore(LocalTime.now())) {
+ handleOverDue(ui, String.format("The following event: %s has already transpired.",
+ this.description), "The start time cannot be in the past.");
+ }
+ }
+ }
+ }
+
+ private void setEndDateAndTime(Ui ui) throws DateTimeParseException, RoverException {
+ try { // Try to parse the end as a date and time
+ endDate = DateTimeParser.parseDateTime(end).toLocalDate();
+ endTime = DateTimeParser.parseDateTime(end).toLocalTime();
+ if (endDate.isBefore(LocalDate.now()) || (endDate.isEqual(LocalDate.now())
+ && endTime.isBefore(LocalTime.now()))) {
+ handleOverDue(ui, String.format("The following event: %s has already transpired.", this.description),
+ "The end date and time cannot be in the past.");
+ }
+ } catch (DateTimeParseException e) {
+ try { // Try to parse the end as a time only
+ endDate = startDate;
+ endTime = DateTimeParser.parseTime(end);
+ if (startDate.equals(LocalDate.now()) && endTime.isBefore(LocalTime.now())) {
+ handleOverDue(ui, String.format("The following event: %s has already transpired.",
+ this.description), "The end time cannot be in the past.");
+ }
+ } catch (DateTimeParseException e2) {
+ // Try to parse the end as a date only
+ endDate = DateTimeParser.parseDate(end);
+ endTime = LocalTime.of(23, 59);
+ if (endDate.isBefore(LocalDate.now())) {
+ handleOverDue(ui, String.format("The following event: %s has already transpired.",
+ this.description), "The end date cannot be in the past.");
+ }
+ }
+ }
+ }
+
+ private void handleOverDue(Ui ui, String warning, String error) throws RoverException {
+ if (ui != null) {
+ ui.showMessage(warning);
+ return;
+ }
+ throw new RoverException(error);
+ }
+
+ /**
+ * Checks if the task is due before the given date and time.
+ * Event tasks are due before the start date and time.
+ */
+ @Override
+ public boolean isBefore(String dateTime) {
+ String[] parts = dateTime.split(" ");
+ if (parts.length == 1) {
+ try { // Interpret as a date only
+ LocalDate otherDate = DateTimeParser.parseDate(dateTime);
+ return startDate.isBefore(otherDate);
+ } catch (DateTimeParseException e) {
+ // Interpret as a time only
+ LocalDateTime otherDateTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime);
+ return startDate.atTime(startTime).isBefore(otherDateTime);
+ }
+ } else { // Interpret as a date and time
+ LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime);
+ return startDate.atTime(startTime).isBefore(otherDateTime);
+ }
+ }
+
+ /**
+ * Checks if the task is due after the given date and time.
+ * Event tasks are due before the end date and time.
+ */
+ @Override
+ public boolean isAfter(String dateTime) {
+ String[] parts = dateTime.split(" ");
+ if (parts.length == 1) {
+ try { // Interpret as a date only
+ LocalDate otherDate = DateTimeParser.parseDate(dateTime);
+ return startDate.isAfter(otherDate);
+ } catch (DateTimeParseException e) {
+ // Interpret as a time only
+ LocalDateTime otherDateTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime);
+ return startDate.atTime(startTime).isAfter(otherDateTime);
+ }
+ } else { // Interpret as a date and time
+ LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime);
+ return startDate.atTime(startTime).isAfter(otherDateTime);
+ }
+ }
+
+ /**
+ * Compares this event task with the specified object for equality.
+ *
+ * @param obj The object to compare with.
+ */
+ public boolean equals(Object obj) {
+ if (obj instanceof Event other) {
+ return this.description.equals(other.description) && this.startDate.equals(other.startDate)
+ && this.startTime.equals(other.startTime) && this.endDate.equals(other.endDate)
+ && this.endTime.equals(other.endTime);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the description of the task for saving to the file.
+ */
+ @Override
+ public String getTaskString() {
+ return "E | " + (isDone ? "1" : "0") + " | " + description + " /from " + start + " /to " + end;
+ }
+
+ /**
+ * Returns the string representation of the task.
+ */
+ @Override
+ public String toString() {
+ return "[E]" + super.toString() + " (" + fromToFullFormat + ")";
+ }
+}
diff --git a/src/main/java/rover/task/Task.java b/src/main/java/rover/task/Task.java
new file mode 100644
index 0000000000..2a8a3d484e
--- /dev/null
+++ b/src/main/java/rover/task/Task.java
@@ -0,0 +1,81 @@
+package rover.task;
+
+import java.time.format.DateTimeParseException;
+
+import rover.exceptions.RoverException;
+
+/**
+ * Represents a task that can be added to the task list.
+ * A task has a description and a status that indicates whether it is done.
+ */
+public abstract sealed class Task permits Todo, Deadline, Event {
+ protected String description;
+ protected boolean isDone;
+
+ /**
+ * Constructor for a task.
+ *
+ * @param description The description of the task.
+ * @throws RoverException If the description is empty.
+ */
+ public Task(String description) throws RoverException {
+ this.description = description;
+ if (this.description.isEmpty()) {
+ throw new RoverException("The description of a task cannot be empty.");
+ }
+ this.isDone = false;
+ }
+
+ /**
+ * Marks the task as done.
+ */
+ public void setDone() {
+ this.isDone = true;
+ }
+
+ /**
+ * Marks the task as undone.
+ */
+ public void setUndone() {
+ this.isDone = false;
+ }
+
+ private String getStatusIcon() {
+ return (isDone ? "X" : " ");
+ }
+
+ /**
+ * Returns the description of the task for saving to the file.
+ *
+ * @return The description of the task.
+ */
+ public abstract String getTaskString();
+
+ /**
+ * Checks if the task is due before the given date and time.
+ *
+ * @param dateTime The date and time to compare with.
+ * @return True if the task is due before the given date and time, false otherwise.
+ * @throws DateTimeParseException If the date and time is in the wrong format.
+ */
+ public abstract boolean isBefore(String dateTime) throws DateTimeParseException;
+
+ /**
+ * Checks if the task is due after the given date and time.
+ *
+ * @param dateTime The date and time to compare with.
+ * @return True if the task is due after the given date and time, false otherwise.
+ * @throws DateTimeParseException If the date and time is in the wrong format.
+ */
+ public abstract boolean isAfter(String dateTime) throws DateTimeParseException;
+
+ /**
+ * Returns the string representation of the task.
+ *
+ * @return The string representation of the task.
+ */
+ @Override
+ public String toString() {
+ return "[" + this.getStatusIcon() + "] " + this.description;
+ }
+}
diff --git a/src/main/java/rover/task/TaskAction.java b/src/main/java/rover/task/TaskAction.java
new file mode 100644
index 0000000000..f5ab15b339
--- /dev/null
+++ b/src/main/java/rover/task/TaskAction.java
@@ -0,0 +1,10 @@
+package rover.task;
+
+/**
+ * Represents the actions that can be performed on a task given by its index.
+ */
+public enum TaskAction {
+ MARK_DONE,
+ MARK_UNDONE,
+ DELETE
+}
diff --git a/src/main/java/rover/task/TaskList.java b/src/main/java/rover/task/TaskList.java
new file mode 100644
index 0000000000..cf60ac800c
--- /dev/null
+++ b/src/main/java/rover/task/TaskList.java
@@ -0,0 +1,190 @@
+package rover.task;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiFunction;
+
+import rover.exceptions.RoverException;
+import rover.ui.Ui;
+
+/**
+ * Represents a list of tasks that can be added to, marked, unmarked, deleted, and displayed.
+ */
+public final class TaskList {
+
+ private static final String NEW_LINE = System.lineSeparator();
+ private static final String DELIMITER = " \\| ";
+ private final ArrayList tasks;
+ private int taskCount = 0;
+
+ /**
+ * Returns an empty task list.
+ */
+ public TaskList() {
+ this.tasks = new ArrayList<>();
+ }
+
+ /**
+ * Returns a task list with the tasks from the given array of task strings.
+ *
+ * @param ui The user interface to display messages.
+ * @param taskStrings The array of task strings to be converted to tasks.
+ * @throws RoverException If there is a possible corruption in the saved tasks.
+ * @throws DateTimeParseException If the date and time format is incorrect.
+ */
+ public TaskList(Ui ui, String ...taskStrings) throws RoverException, DateTimeParseException {
+ assert taskStrings != null : "Task strings should not be null.";
+ this.tasks = new ArrayList<>();
+ for (String taskString : taskStrings) {
+ String[] parts = taskString.split(DELIMITER);
+ if (parts.length != 3) {
+ throw new RoverException("Possible corruption in saved tasks.");
+ }
+ Task newTask = getTask(ui, parts);
+ tasks.add(newTask);
+ taskCount++;
+ }
+ }
+
+ /**
+ * Returns a task based on the given parts of the task string.
+ *
+ * @param parts The parts of the task string.
+ * @return The task based on the parts.
+ * @throws RoverException If there is a possible corruption in the saved tasks.
+ */
+ private Task getTask(Ui ui, String ...parts) throws RoverException {
+ assert parts != null : "Parts should not be null.";
+ Task newTask;
+ switch (parts[0]) {
+ case "T" -> newTask = new Todo(parts[2]);
+ case "D" -> newTask = new Deadline(parts[2], ui);
+ case "E" -> newTask = new Event(parts[2], ui);
+ default -> throw new RoverException("Possible corruption in saved tasks.");
+ }
+ if (parts[1].equals("1")) {
+ newTask.setDone();
+ } else if (!parts[1].equals("0")) {
+ throw new RoverException("Possible corruption in saved tasks.");
+ }
+ return newTask;
+ }
+
+ public ArrayList getTasks() {
+ return this.tasks;
+ }
+
+ public int getNumberOfTasks() {
+ return this.taskCount;
+ }
+
+ /**
+ * Displays all the tasks that match the given predicate.
+ *
+ * @param ui The user interface to display the tasks.
+ * @param predicate The predicate to filter the tasks.
+ * @param filterDescription The description of the filter.
+ */
+ public void showTasks(Ui ui, BiFunction predicate, String filterDescription) {
+ assert predicate != null : "Predicate should not be null.";
+ assert filterDescription != null : "Filter description should not be null.";
+ assert !filterDescription.isEmpty() : "Filter description should not be empty.";
+ AtomicBoolean wasExceptionThrown = new AtomicBoolean(false);
+ List filteredTasks = tasks.stream().filter(task -> predicate.apply(task, wasExceptionThrown)).toList();
+ if (wasExceptionThrown.get()) {
+ return;
+ }
+
+ String response = getStringOfFilteredTasks(filteredTasks, filterDescription);
+ ui.showMessage(response);
+ }
+
+ private String getStringOfFilteredTasks(List filteredTasks, String filterDescription) {
+ if (filteredTasks.isEmpty()) {
+ return "There are no tasks " + filterDescription + ".";
+ }
+ StringBuilder response = new StringBuilder("Here are the tasks " + filterDescription + ":" + NEW_LINE);
+ for (int i = 0; i < filteredTasks.size(); i++) {
+ response.append((i + 1)).append(". ").append(filteredTasks.get(i));
+ if (i != filteredTasks.size() - 1) {
+ response.append(NEW_LINE);
+ }
+ }
+ return response.toString();
+ }
+
+ /**
+ * Adds a new task to the task list.
+ *
+ * @param newTask The new task to be added.
+ * @param ui The user interface to display the added task.
+ * @throws RoverException If the task already exists in the list.
+ */
+ public void addTask(Task newTask, Ui ui) throws RoverException {
+ assert newTask != null : "Task should not be null.";
+ assert ui != null : "Ui should not be null.";
+ if (tasks.contains(newTask)) {
+ throw new RoverException("This task already exists in the list.");
+ }
+ tasks.add(newTask);
+ taskCount++;
+ String response = String.format("Got it%s. I've added this task:", ui.getUsername()) + NEW_LINE
+ + " " + newTask + NEW_LINE
+ + "Now you have " + taskCount + " task"
+ + (taskCount > 1 ? "s" : "") + " in the list.";
+ ui.showMessage(response);
+ }
+
+ /**
+ * Marks a task in the task list as done.
+ *
+ * @param index The index of the task to be marked as done.
+ * @param ui The user interface to display the tasks found.
+ */
+ public void markTask(int index, Ui ui) {
+ assert index >= 0 : "Index should be non-negative.";
+ assert index < taskCount : "Index should be less than the number of tasks.";
+ assert ui != null : "Ui should not be null.";
+ Task task = tasks.get(index);
+ task.setDone();
+ String response = String.format("Nice%s! I've marked this task as done:", ui.getUsername()) + NEW_LINE + task;
+ ui.showMessage(response);
+ }
+
+ /**
+ * Marks a task in the task list as undone.
+ *
+ * @param index The index of the task to be marked as undone.
+ * @param ui The user interface to display the tasks found.
+ */
+ public void unmarkTask(int index, Ui ui) {
+ assert index >= 0 : "Index should be non-negative.";
+ assert index < taskCount : "Index should be less than the number of tasks.";
+ assert ui != null : "Ui should not be null.";
+ Task task = tasks.get(index);
+ task.setUndone();
+ String response = String.format("Alright%s, I've marked this task as not done yet:", ui.getUsername())
+ + NEW_LINE + task;
+ ui.showMessage(response);
+ }
+
+ /**
+ * Deletes a task from the task list.
+ *
+ * @param index The index of the task to be deleted.
+ * @param ui The user interface to display the deleted task.
+ */
+ public void deleteTask(int index, Ui ui) {
+ assert index >= 0 : "Index should be non-negative.";
+ assert index < taskCount : "Index should be less than the number of tasks.";
+ assert ui != null : "Ui should not be null.";
+ Task task = tasks.get(index);
+ tasks.remove(index);
+ taskCount--;
+ String response = String.format("Noted%s. I've removed this task:", ui.getUsername()) + NEW_LINE + task
+ + NEW_LINE + "Now you have " + taskCount + " task"
+ + (taskCount > 1 ? "s" : "") + " in the list.";
+ ui.showMessage(response);
+ }
+}
diff --git a/src/main/java/rover/task/Todo.java b/src/main/java/rover/task/Todo.java
new file mode 100644
index 0000000000..7d46d74707
--- /dev/null
+++ b/src/main/java/rover/task/Todo.java
@@ -0,0 +1,72 @@
+package rover.task;
+
+import rover.exceptions.RoverException;
+
+/**
+ * Represents a todo task that can be added to the task list.
+ * A todo task has a description and a status that indicates whether it is done.
+ */
+public final class Todo extends Task {
+
+ /**
+ * Constructor for a todo task.
+ *
+ * @param description The description of the todo task.
+ * @throws RoverException If the description is empty.
+ */
+ public Todo(String description) throws RoverException {
+ super(description);
+ }
+
+ /**
+ * Checks if the task is due before the given date and time.
+ * Todo tasks do not have a date and time and will always return false.
+ *
+ * @param dateTime The date and time to compare with.
+ */
+ @Override
+ public boolean isBefore(String dateTime) {
+ return false;
+ }
+
+ /**
+ * Checks if the task is due after the given date and time.
+ * Todo tasks do not have a date and time and will always return false.
+ *
+ * @param dateTime The date and time to compare with.
+ */
+ @Override
+ public boolean isAfter(String dateTime) {
+ return false;
+ }
+
+ /**
+ * Compares this todo task with the specified object for equality.
+ *
+ * @param obj The object to compare with.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Todo other) {
+ return this.description.equals(other.description);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the description of the task for saving to the file.
+ */
+ @Override
+ public String getTaskString() {
+ return "T | " + (isDone ? "1" : "0") + " | " + description;
+ }
+
+ /**
+ * Returns the string representation of the task.
+ */
+ @Override
+ public String toString() {
+ return "[T]" + super.toString();
+ }
+
+}
diff --git a/src/main/java/rover/ui/DialogBox.java b/src/main/java/rover/ui/DialogBox.java
new file mode 100644
index 0000000000..e5954e9407
--- /dev/null
+++ b/src/main/java/rover/ui/DialogBox.java
@@ -0,0 +1,75 @@
+package rover.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.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+
+/**
+ * Represents a dialog box consisting of an ImageView to represent the speaker's face
+ * and a label containing text from the speaker.
+ */
+public class DialogBox extends HBox {
+ @FXML
+ private Label dialog;
+ @FXML
+ private ImageView displayPicture;
+
+ private DialogBox(String text, Image img) {
+ assert text != null : "Text should not be null.";
+ assert img != null : "Image should not be null.";
+ try {
+ FXMLLoader fxmlLoader = new FXMLLoader(Gui.class.getResource("/view/DialogBox.fxml"));
+ fxmlLoader.setController(this);
+ fxmlLoader.setRoot(this);
+ fxmlLoader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ dialog.setText(text);
+ displayPicture.setImage(img);
+ }
+
+ /**
+ * 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);
+ dialog.getStyleClass().add("reply-label");
+ }
+
+ /**
+ * Converts the dialog box to an error dialog box.
+ */
+ private void convertToErrorDialog() {
+ dialog.getStyleClass().add("error-label");
+ }
+
+ public static DialogBox getUserDialog(String text, Image img) {
+ return new DialogBox(text, img);
+ }
+
+ public static DialogBox getRoverDialog(String text, Image img) {
+ DialogBox db = new DialogBox(text, img);
+ db.flip();
+ return db;
+ }
+
+ public static DialogBox getErrorDialog(String text, Image img) {
+ DialogBox db = getRoverDialog(text, img);
+ db.convertToErrorDialog();
+ return db;
+ }
+}
diff --git a/src/main/java/rover/ui/Gui.java b/src/main/java/rover/ui/Gui.java
new file mode 100644
index 0000000000..89407ead11
--- /dev/null
+++ b/src/main/java/rover/ui/Gui.java
@@ -0,0 +1,282 @@
+package rover.ui;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+import rover.main.Rover;
+import rover.preferences.UserPreferences;
+
+/**
+ * Controller for the main GUI.
+ */
+public final class Gui extends AnchorPane implements Ui {
+
+ private static final Image DEFAULT_USER_IMAGE = new Image(Gui.class.getResourceAsStream("/images/User.png"));
+ private static final Image DEFAULT_ROVER_IMAGE = new Image(Gui.class.getResourceAsStream("/images/Rover.png"));
+
+ @FXML
+ private ScrollPane scrollPane;
+ @FXML
+ private VBox dialogContainer;
+ @FXML
+ private TextField userInput;
+
+ private Rover rover;
+ private Image userImage;
+ private Image roverImage;
+ private String username;
+ private UserPreferences userPreferences;
+
+ /**
+ * Initializes the GUI.
+ */
+ @FXML
+ public void initialize() {
+ scrollPane.vvalueProperty().bind(dialogContainer.heightProperty());
+ }
+
+ /**
+ * Gets the UserPreferences instance.
+ *
+ * @return The UserPreferences instance.
+ */
+ public UserPreferences getUserPreferences() {
+ return userPreferences;
+ }
+
+ /**
+ * Injects the UserPreferences instance.
+ *
+ * @param userPreferences The UserPreferences instance.
+ */
+ public void setUserPreferences(UserPreferences userPreferences) {
+ assert userPreferences != null : "User preferences should not be null.";
+ this.userPreferences = userPreferences;
+
+ JsonNode name = userPreferences.getJsonNode().get("name");
+ this.username = name.asText().isEmpty() ? "" : " " + name.asText();
+ setUserImage(Path.of(userPreferences.getJsonNode().get("userImage").asText()));
+ setRoverImage(Path.of(userPreferences.getJsonNode().get("roverImage").asText()));
+ }
+
+ /**
+ * Injects the Rover instance
+ */
+ public void setRover(Rover r) {
+ rover = r;
+ rover.setUi(this);
+ }
+
+ /**
+ * Gets the username of the user.
+ *
+ * @return The username of the user.
+ */
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Sets the username of the user.
+ *
+ * @param username The username of the user.
+ */
+ @Override
+ public boolean setUsername(String username) {
+ assert username != null : "Username should not be null.";
+ assert !username.isBlank() : "Username should not be blank.";
+ this.username = " " + username;
+ return userPreferences.setName(username);
+ }
+
+ /**
+ * Sets the image of the user.
+ *
+ * @param userImagePath The path to the image of the user.
+ */
+ public boolean setUserImage(Path userImagePath) {
+ if (userImagePath.toString().equals("default")) {
+ userImage = DEFAULT_USER_IMAGE;
+ return false;
+ } else if (!Files.exists(userImagePath)) {
+ displayError(String.format("File %s not found at the specified path. "
+ + "Using default user image.", userImagePath));
+ userImage = DEFAULT_USER_IMAGE;
+ return false;
+ }
+ userImage = new Image(userImagePath.toUri().toString());
+ return userPreferences.setUserImage(userImagePath.toString());
+ }
+
+ /**
+ * Sets the image of the rover.
+ *
+ * @param roverImagePath The path to the image of the rover.
+ */
+ public boolean setRoverImage(Path roverImagePath) {
+ if (roverImagePath.toString().equals("default")) {
+ roverImage = DEFAULT_ROVER_IMAGE;
+ return false;
+ } else if (!Files.exists(roverImagePath)) {
+ displayError(String.format("File %s not found at the specified path. "
+ + "Using default rover image.", roverImagePath));
+ roverImage = DEFAULT_ROVER_IMAGE;
+ return false;
+ }
+ roverImage = new Image(roverImagePath.toUri().toString());
+ return userPreferences.setRoverImage(roverImagePath.toString());
+ }
+
+ /**
+ * Shows the response from the user in the dialog container.
+ */
+ private void showUserResponse() {
+ assert dialogContainer != null : "Dialog container should not be null.";
+ dialogContainer.getChildren().addAll(
+ DialogBox.getUserDialog(userInput.getText(), userImage)
+ );
+ userInput.clear();
+ }
+
+ /**
+ * Shows the response from Rover in the dialog container.
+ * @param response The response from Rover.
+ */
+ private void showRoverResponse(String response) {
+ assert dialogContainer != null : "Dialog container should not be null.";
+ dialogContainer.getChildren().addAll(
+ DialogBox.getRoverDialog(response, roverImage)
+ );
+ }
+
+ /**
+ * Reads the user's input.
+ *
+ * @return The user's input.
+ */
+ @Override
+ public String readCommand() {
+ assert userInput != null : "User input should not be null.";
+ return userInput.getText();
+ }
+
+ /**
+ * Creates two dialog boxes, one echoing user input and the other containing Rover's reply and then appends them to
+ * the dialog container. Clears the user input after processing.
+ */
+ @FXML
+ private void handleUserInput() {
+ String input = readCommand();
+ showUserResponse();
+ assert rover != null : "Rover should not be null.";
+ boolean isExit = rover.handleResponse(input);
+ if (isExit) {
+ rover.endSession();
+ }
+ }
+
+ /**
+ * Displays the welcome message when the program starts.
+ */
+ @Override
+ public void showWelcome() {
+ String response = String.format("""
+ Hello%s! I'm Rover
+ I am your personal task manager.
+ What can I do for you?
+ """, username);
+ showRoverResponse(response);
+ }
+
+ /**
+ * Displays the goodbye message when the program ends.
+ * Part of the code was taken from a StackOverflow answer given by Jason C.
+ * source: ...
+ * Thanks to Jason C. for the original code.
+ * Thanks to @zuoshihua for pointing out the issue with the original code.
+ * Thanks to @ChinZJ for the fix to use new Timer(true) instead.
+ */
+ @Override
+ public void sayBye() {
+ String response = String.format("Bye%s. Hope to see you again soon!", username);
+ showRoverResponse(response);
+ new Timer(true).schedule(new TimerTask() {
+ public void run() {
+ Platform.exit();
+ }
+ }, 2000);
+ }
+
+ /**
+ * Displays the help message when the user types an invalid command.
+ */
+ @Override
+ public void showHelpMessage() {
+ String briefHelp = String.format("""
+ I'm sorry%s, but I don't know what that means.
+ The following commands are supported:
+ You can add a task by typing:
+ - todo (description)
+ - deadline (description) /by (deadline)
+ - event (description) /from (start) /to (end)
+ You can set your preferences by typing 'set (preference) (value)'.
+ To set your name, type 'set name (your name)'.
+ To set your user image, type 'set userImage (path to image)'.
+ To set Rover's image, type 'set roverImage (path to image)'.
+ List the existing tasks by typing 'list'.
+ Mark a task as done by typing 'mark (task number)'.
+ Mark a task as not done by typing 'unmark (task number)'.
+ Delete a task by typing 'delete (task number)'.
+ Find tasks with a certain keyword by typing 'find (keyword)'.
+ Show tasks before a certain date and/or time by typing 'show before (date) (time)'.
+ Show tasks after a certain date and/or time by typing 'show after (date) (time)'.
+ Exit the program by typing 'bye'.
+ """, username);
+ showRoverResponse(briefHelp);
+ }
+
+ /**
+ * Displays the message to the user.
+ *
+ * @param message The message to be displayed.
+ */
+ @Override
+ public void showMessage(String message) {
+ showRoverResponse(message);
+ }
+
+ /**
+ * Displays the message to the user without a line separator.
+ *
+ * @param message The message to be displayed.
+ */
+ @Override
+ public void showMessageWithoutLineSeparator(String message) {
+ showRoverResponse(message);
+ }
+
+ /**
+ * Displays the error message to the user.
+ *
+ * @param message The error message to be displayed.
+ */
+ @Override
+ public void displayError(String message) {
+ String response = "Oops! Error: " + message;
+ dialogContainer.getChildren().addAll(
+ DialogBox.getErrorDialog(response, roverImage)
+ );
+ }
+}
diff --git a/src/main/java/rover/ui/TextUi.java b/src/main/java/rover/ui/TextUi.java
new file mode 100644
index 0000000000..5640a81c99
--- /dev/null
+++ b/src/main/java/rover/ui/TextUi.java
@@ -0,0 +1,188 @@
+package rover.ui;
+
+import java.util.Scanner;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import rover.preferences.UserPreferences;
+
+/**
+ * Ui class deals with interactions with the user.
+ * It displays messages to the user and reads input from the user.
+ */
+public final class TextUi implements Ui {
+
+ private static final String divider = "--------------------------------------------";
+ private String username = "";
+ private final Scanner sc;
+ private UserPreferences userPreferences;
+
+ /**
+ * Constructor for Ui class.
+ * It initializes the scanner to read input from the user.
+ */
+ public TextUi() {
+ this.sc = new Scanner(System.in);
+ }
+
+ /**
+ * Reads the next line of input from the user.
+ *
+ * @return The next line of input from the user.
+ */
+ @Override
+ public String readCommand() {
+ String input = sc.nextLine();
+ assert input != null && !input.isEmpty() : "Input should not be null or empty.";
+ return input;
+ }
+
+ /**
+ * Gets the user preferences for the Ui.
+ *
+ * @return The user preferences object.
+ */
+ public UserPreferences getUserPreferences() {
+ return userPreferences;
+ }
+
+ /**
+ * Sets the user preferences for the Ui.
+ *
+ * @param userPreferences The user preferences object to set.
+ */
+ @Override
+ public void setUserPreferences(UserPreferences userPreferences) {
+ assert userPreferences != null : "User preferences should not be null.";
+ this.userPreferences = userPreferences;
+ JsonNode name = userPreferences.getJsonNode().get("name");
+ this.username = name.asText().isEmpty() ? "" : " " + name.asText();
+ }
+
+ /**
+ * Gets the username of the user.
+ *
+ * @return The username of the user.
+ */
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Sets the username of the user.
+ *
+ * @param username The username of the user.
+ */
+ @Override
+ public boolean setUsername(String username) {
+ assert username != null : "Username should not be null.";
+ assert !username.isBlank() : "Username should not be blank.";
+ this.username = " " + username;
+ return userPreferences.setName(username);
+ }
+
+ /**
+ * Displays a line to separate different messages.
+ */
+ private void showLine() {
+ System.out.println(divider);
+ }
+
+ /**
+ * Displays the welcome message when the program starts.
+ */
+ @Override
+ public void showWelcome() {
+ String logo = """
+ ___
+ | _`\\
+ | (_) ) _ _ _ __ _ __
+ | , / /'_`\\ ( ) ( ) /'__`\\( '__)
+ | |\\ \\ ( (_) )| \\_/ |( ___/| |
+ (_) (_)`\\___/'`\\___/'`\\____)(_)
+ """;
+
+ showLine();
+ System.out.println("Hello" + username + "! I'm Rover");
+ System.out.println(logo);
+ System.out.println("I am your personal task manager.");
+ System.out.println("What can I do for you?");
+ showLine();
+ }
+
+ /**
+ * Displays the goodbye message when the program ends.
+ */
+ @Override
+ public void sayBye() {
+ System.out.println("Bye" + username + ". Hope to see you again soon!");
+ showLine();
+ sc.close();
+ }
+
+ /**
+ * Displays the help message when the user types an invalid command.
+ */
+ @Override
+ public void showHelpMessage() {
+ showLine();
+ System.out.println("I'm sorry" + username + ", but I don't know what that means.");
+ String briefHelp = """
+ The following commands are supported:
+ You can add a task by typing:
+ - todo (description)
+ - deadline (description) /by (deadline)
+ - event (description) /from (start) /to (end)
+ You can set your preferences by typing 'set (preference) (value)'.
+ To set your name, type 'set name (your name)'.
+ To set your user image, type 'set userImage (path to image)'.
+ To set Rover's image, type 'set roverImage (path to image)'.
+ List the existing tasks by typing 'list'.
+ Mark a task as done by typing 'mark (task number)'.
+ Mark a task as not done by typing 'unmark (task number)'.
+ Delete a task by typing 'delete (task number)'.
+ Find tasks with a certain keyword by typing 'find (keyword)'.
+ Show tasks before a certain date and/or time by typing 'show before (date) (time)'.
+ Show tasks after a certain date and/or time by typing 'show after (date) (time)'.
+ Exit the program by typing 'bye'.
+ """;
+ System.out.print(briefHelp);
+ showLine();
+ }
+
+ /**
+ * Displays the message to the user.
+ *
+ * @param message The message to be displayed.
+ */
+ @Override
+ public void showMessage(String message) {
+ showLine();
+ System.out.println(message);
+ showLine();
+ }
+
+ /**
+ * Displays the message to the user without the 2nd line separator.
+ *
+ * @param message The message to be displayed.
+ */
+ @Override
+ public void showMessageWithoutLineSeparator(String message) {
+ showLine();
+ System.out.println(message);
+ }
+
+ /**
+ * Displays the error message to the user.
+ *
+ * @param message The error message to be displayed.
+ */
+ @Override
+ public void displayError(String message) {
+ showLine();
+ System.out.println("Oops! Error: " + message);
+ showLine();
+ }
+}
diff --git a/src/main/java/rover/ui/Ui.java b/src/main/java/rover/ui/Ui.java
new file mode 100644
index 0000000000..ba8da8ae4e
--- /dev/null
+++ b/src/main/java/rover/ui/Ui.java
@@ -0,0 +1,31 @@
+package rover.ui;
+
+import rover.preferences.UserPreferences;
+
+/**
+ * Ui interface deals with interactions with the user.
+ */
+public interface Ui {
+
+ UserPreferences getUserPreferences();
+
+ void setUserPreferences(UserPreferences userPreferences);
+
+ String getUsername();
+
+ boolean setUsername(String username);
+
+ String readCommand();
+
+ void showWelcome();
+
+ void sayBye();
+
+ void showHelpMessage();
+
+ void showMessage(String message);
+
+ void showMessageWithoutLineSeparator(String message);
+
+ void displayError(String message);
+}
diff --git a/src/main/resources/css/dialog-box.css b/src/main/resources/css/dialog-box.css
new file mode 100644
index 0000000000..0a3c63d2f7
--- /dev/null
+++ b/src/main/resources/css/dialog-box.css
@@ -0,0 +1,31 @@
+.label {
+ -fx-background-color: linear-gradient(to bottom right, #00ffbf, #00ddff);
+ -fx-border-color: #00ddff #00ddff #00ddff #00ddff;
+ -fx-border-width: 2px;
+ -fx-background-radius: 1em 1em 0 1em;
+ -fx-border-radius: 1em 1em 0 1em;
+}
+
+.reply-label {
+ -fx-background-color: linear-gradient(to bottom right, #ad8ef6, #b470f6);
+ -fx-border-color: #b470f6 #b470f6 #b470f6 #b470f6;
+ -fx-background-radius: 1em 1em 1em 0;
+ -fx-border-radius: 1em 1em 1em 0;
+}
+
+.error-label {
+ -fx-background-color: linear-gradient(to bottom right, #dc5959, #e18888);
+ -fx-border-color: #e18888 #e18888 #e18888 #e18888;
+}
+
+#displayPicture {
+ /* Shadow effect on image. */
+ -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 10, 0.5, 5, 5);
+
+ /* Change size of image. */
+ -fx-scale-x: 1;
+ -fx-scale-y: 1;
+
+ /* Rotate image clockwise by degrees. */
+ -fx-rotate: 0;
+}
diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css
new file mode 100644
index 0000000000..a06d9ee3bb
--- /dev/null
+++ b/src/main/resources/css/main.css
@@ -0,0 +1,46 @@
+.root {
+ main-color: rgb(237, 255, 242); /* Create a looked-up color called "main-color" within root. */
+ -fx-background-color: main-color;
+}
+
+.text-field {
+ -fx-background-color: #d9ffe2;
+ -fx-font: 20px "Arial";
+}
+
+.button {
+ -fx-background-color: mediumspringgreen;
+ -fx-font: italic bold 16px "Arial";
+}
+
+.button:hover {
+ -fx-background-color:cyan;
+ -fx-font-size: 18px;
+}
+
+.button:pressed {
+ -fx-background-color:orange;
+ -fx-font-size: 20px;
+}
+
+.scroll-pane,
+.scroll-pane .viewport {
+ -fx-background-color: transparent;
+}
+
+.scroll-bar {
+ -fx-font-size: 10px; /* Change width of scroll bar. */
+ -fx-background-color: main-color;
+}
+
+.scroll-bar .thumb {
+ -fx-background-color: #ff9cb4;
+ -fx-background-radius: 1em;
+}
+
+/* Hides the increment and decrement buttons. */
+.scroll-bar .increment-button,
+.scroll-bar .decrement-button {
+ -fx-pref-height: 0;
+ -fx-opacity: 0;
+}
diff --git a/src/main/resources/images/Rover.png b/src/main/resources/images/Rover.png
new file mode 100644
index 0000000000..c815ea58f0
Binary files /dev/null and b/src/main/resources/images/Rover.png differ
diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png
new file mode 100644
index 0000000000..b7349be178
Binary files /dev/null and b/src/main/resources/images/User.png differ
diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml
new file mode 100644
index 0000000000..a8fda3c0e4
--- /dev/null
+++ b/src/main/resources/view/DialogBox.fxml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
new file mode 100644
index 0000000000..7a0d3912e2
--- /dev/null
+++ b/src/main/resources/view/MainWindow.fxml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/rover/parser/ParserTest.java b/src/test/java/rover/parser/ParserTest.java
new file mode 100644
index 0000000000..1fb0cd0009
--- /dev/null
+++ b/src/test/java/rover/parser/ParserTest.java
@@ -0,0 +1,155 @@
+package rover.parser;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.time.format.DateTimeParseException;
+
+import org.junit.jupiter.api.Test;
+
+import rover.command.AddCommand;
+import rover.command.DeleteCommand;
+import rover.command.EmptyCommand;
+import rover.command.ExitCommand;
+import rover.command.FindCommand;
+import rover.command.InvalidCommand;
+import rover.command.ListCommand;
+import rover.command.MarkCommand;
+import rover.command.RetrySaveCommand;
+import rover.command.SetCommand;
+import rover.command.ShowAfterCommand;
+import rover.command.ShowBeforeCommand;
+import rover.command.UnmarkCommand;
+import rover.exceptions.RoverException;
+import rover.preferences.PreferenceOption;
+import rover.task.Deadline;
+import rover.task.Event;
+import rover.task.TaskAction;
+import rover.task.Todo;
+
+public class ParserTest {
+
+ @Test
+ public void checkParseCommand() {
+ Parser parser = new Parser();
+ assertEquals(new AddCommand("todo read book"), parser.parseCommand("todo read book"));
+ assertEquals(new AddCommand("deadline return book /by 2021-08-24 1800"),
+ parser.parseCommand("deadline return book /by 2021-08-24 1800"));
+ assertEquals(new AddCommand("event project meeting /at 2021-08-24 1800"),
+ parser.parseCommand("event project meeting /at 2021-08-24 1800"));
+ assertEquals(new DeleteCommand("delete 1"), parser.parseCommand("delete 1"));
+ assertEquals(new EmptyCommand(""), parser.parseCommand(""));
+ assertEquals(new ExitCommand("bye"), parser.parseCommand("bye"));
+ assertEquals(new FindCommand("find book"), parser.parseCommand("find book"));
+ assertEquals(new InvalidCommand("whatever"), parser.parseCommand("whatever"));
+ assertEquals(new ListCommand("list"), parser.parseCommand("list"));
+ assertEquals(new MarkCommand("mark 1"), parser.parseCommand("mark 1"));
+ assertEquals(new SetCommand("set name John"), parser.parseCommand("set name John"));
+ assertEquals(new ShowAfterCommand("show after 21/08/24"), parser.parseCommand("show after 21/08/24"));
+ assertEquals(new ShowBeforeCommand("show before 21/08/24"), parser.parseCommand("show before 21/08/24"));
+ assertEquals(new UnmarkCommand("unmark 1"), parser.parseCommand("unmark 1"));
+ }
+
+ @Test
+ public void checkIsPreviousCommandBye() {
+ Parser parser = new Parser();
+ assertEquals(new InvalidCommand("yes"), parser.parseCommand("yes"));
+ parser.parseCommand("bye");
+ assertEquals(new RetrySaveCommand("yes"), parser.parseCommand("yes"));
+ }
+
+ @Test
+ public void checkParsePreferenceOption() {
+ Parser parser = new Parser();
+ assertDoesNotThrow(() -> {
+ assertEquals(PreferenceOption.NAME, parser.parsePreferenceOption("name"));
+ assertEquals(PreferenceOption.USER_IMAGE, parser.parsePreferenceOption("userImage"));
+ assertEquals(PreferenceOption.ROVER_IMAGE, parser.parsePreferenceOption("roverImage"));
+ });
+ assertThrowsExactly(RoverException.class, () -> parser.parsePreferenceOption("backgroundImage"));
+ }
+
+ @Test
+ public void checkParseTaskDescription() {
+ Parser parser = new Parser();
+ assertDoesNotThrow(() -> {
+ assertEquals(new Todo("read book"), parser.parseTaskDescription("todo read book"));
+ assertEquals(new Deadline("return book /by 24/08/30 1800"),
+ parser.parseTaskDescription("deadline return book /by 24/08/30 1800"));
+ assertEquals(new Event("project meeting /from 24/08/30 1800 /to 1900"),
+ parser.parseTaskDescription("event project meeting /from 24/08/30 1800 /to 1900"));
+ });
+ }
+
+ @Test
+ public void checkParseTaskDescription2() {
+ Parser parser = new Parser();
+ try {
+ parser.parseTaskDescription("deadline return book by 24/08/21 1800");
+ } catch (RoverException e) {
+ assertEquals("A deadline task must be a task followed with '/by (deadline)'.", e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+
+ try {
+ parser.parseTaskDescription("deadline return book /by 240821 1800");
+ } catch (DateTimeParseException e) {
+ assertEquals("Unable to parse date: 240821", e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+
+ try {
+ parser.parseTaskDescription("event project meeting /from 24/08/21 1800 to 1900");
+ } catch (RoverException e) {
+ assertEquals("An event task must be a task followed with '/from (start) /to (end)'.",
+ e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+ }
+
+ @Test
+ public void checkParseTaskNumber() {
+ Parser parser = new Parser();
+ assertDoesNotThrow(() -> assertEquals(3, parser.parseTaskNumber("4", 4,
+ TaskAction.MARK_DONE)));
+
+ try {
+ parser.parseTaskNumber("", 2, TaskAction.DELETE);
+ } catch (RoverException e) {
+ assertEquals("Please specify the task number to be deleted.", e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+
+ try {
+ parser.parseTaskNumber("read book", 3, TaskAction.MARK_DONE);
+ } catch (RoverException e) {
+ assertEquals("Please specify a valid task number to be marked as done.", e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+
+ try {
+ parser.parseTaskNumber("-1", 4, TaskAction.MARK_DONE);
+ } catch (RoverException e) {
+ assertEquals("Please specify a valid task number to be marked as done.\n"
+ + "You only have 4 tasks in total.", e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+
+ try {
+ parser.parseTaskNumber("5", 4, TaskAction.MARK_UNDONE);
+ } catch (RoverException e) {
+ assertEquals("Please specify a valid task number to be marked as not done.\n"
+ + "You only have 4 tasks in total.", e.getMessage());
+ } catch (Exception e) {
+ fail("Unexpected exception thrown");
+ }
+ }
+}
diff --git a/src/test/java/rover/task/DeadlineTest.java b/src/test/java/rover/task/DeadlineTest.java
new file mode 100644
index 0000000000..d762a6cc7b
--- /dev/null
+++ b/src/test/java/rover/task/DeadlineTest.java
@@ -0,0 +1,94 @@
+package rover.task;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.format.DateTimeParseException;
+
+import org.junit.jupiter.api.Test;
+
+import rover.exceptions.RoverException;
+
+public class DeadlineTest {
+
+ @Test
+ public void checkIfExceptionThrown_emptyStringInitialisation() {
+ assertThrowsExactly(RoverException.class, () -> new Deadline(""));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_noByKeyword() {
+ assertThrowsExactly(RoverException.class, () -> new Deadline("read book by 2021-08-24"));
+ assertThrowsExactly(RoverException.class, () -> new Deadline("read book 2021-08-24"));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_noDate() {
+ assertThrowsExactly(RoverException.class, () -> new Deadline("read book /by "));
+ assertThrowsExactly(RoverException.class, () -> new Deadline("read book /by"));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_improperDate() {
+ assertThrowsExactly(DateTimeParseException.class, () -> new Deadline("read book /by 240921"));
+ }
+
+ @Test
+ public void checkIfExceptionNotThrown_properDate() {
+ assertDoesNotThrow(() -> {
+ new Deadline("read book /by 2030-08-24");
+ });
+ assertDoesNotThrow(() -> {
+ new Deadline("read book /by 2030-08-24 1800");
+ });
+ }
+
+ @Test
+ public void checkTaskString_beforeMarkingItDone() {
+ try {
+ Deadline deadline = new Deadline("do homework /by 2021-08-24");
+ assertEquals("D | 0 | do homework /by 2021-08-24", deadline.getTaskString());
+ assertEquals("[D][ ] do homework (by: Tuesday, 24 August, 2021 11:59 pm)", deadline.toString());
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkTaskString_afterMarkingItDone() {
+ try {
+ Deadline deadline = new Deadline("do homework /by 2021-08-24");
+ deadline.setDone();
+ assertEquals("D | 1 | do homework /by 2021-08-24", deadline.getTaskString());
+ assertEquals("[D][X] do homework (by: Tuesday, 24 August, 2021 11:59 pm)", deadline.toString());
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIfIsBefore() {
+ try {
+ Deadline deadline = new Deadline("do homework /by 2021-08-24");
+ assertFalse(deadline.isBefore("2021-08-24 18:00"));
+ assertFalse(deadline.isBefore("20-08-21"));
+ assertTrue(deadline.isBefore("26/08/24"));
+ } catch (DateTimeParseException | RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIfIsAfter() {
+ try {
+ Deadline deadline = new Deadline("do homework /by 2021-08-24");
+ assertTrue(deadline.isAfter("2021-08-24 18:00"));
+ assertTrue(deadline.isAfter("20-08-21"));
+ assertFalse(deadline.isAfter("26/08/24"));
+ } catch (DateTimeParseException | RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/rover/task/EventTest.java b/src/test/java/rover/task/EventTest.java
new file mode 100644
index 0000000000..0d67a79258
--- /dev/null
+++ b/src/test/java/rover/task/EventTest.java
@@ -0,0 +1,127 @@
+package rover.task;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.format.DateTimeParseException;
+
+import org.junit.jupiter.api.Test;
+
+import rover.exceptions.RoverException;
+
+public class EventTest {
+
+ @Test
+ public void checkIfExceptionThrown_emptyStringInitialisation() {
+ assertThrowsExactly(RoverException.class, () -> new Event(""));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_noFromKeyword() {
+ assertThrowsExactly(RoverException.class, () -> new Event("read book 2021-08-24 /to 2021-08-25"));
+ assertThrowsExactly(RoverException.class, () -> new Event("read book /to 2021-08-25"));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_noToKeyword() {
+ assertThrowsExactly(RoverException.class, () -> new Event("read book /from 2021-08-24 1800"));
+ assertThrowsExactly(RoverException.class, () -> new Event("read book /from 2021-08-24 1800 /to "));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_noDate() {
+ assertThrowsExactly(RoverException.class, () -> new Event("read book /from /to 2021-08-25 1800"));
+ assertThrowsExactly(RoverException.class, () -> new Event("read book /from 2021-08-24 1800 /to "));
+ }
+
+ @Test
+ public void checkIfExceptionThrown_improperDate() {
+ assertThrowsExactly(DateTimeParseException.class, () -> new Event("read book /from 240921 /to 250921"));
+ assertThrowsExactly(DateTimeParseException.class, () ->
+ new Event("read book /from 2030-08-24 1800 /to 250921"));
+ assertThrowsExactly(RoverException.class, () ->
+ new Event("read book /from 24/08/30 1900 /to 2030-08-24 1800"));
+ }
+
+ @Test
+ public void checkIfExceptionNotThrown_properDate() {
+ assertDoesNotThrow(() -> {
+ new Event("School Camp /from 2030-08-24 /to 2030-08-27");
+ });
+ assertDoesNotThrow(() -> {
+ new Event("School Camp /from 2030-08-24 1800 /to 2030-08-25 1800");
+ });
+ }
+
+ @Test
+ public void checkTaskString_beforeMarkingItDone() {
+ try {
+ Event event = new Event("School Camp /from 2021-08-24 1800 /to 2021-08-27 1800");
+ assertEquals("E | 0 | School Camp /from 2021-08-24 1800 /to 2021-08-27 1800", event.getTaskString());
+ assertEquals("[E][ ] School Camp (from Tuesday, 24 August, 2021 6:00 pm to Friday, "
+ + "27 August, 2021 6:00 pm)", event.toString());
+
+ Event event2 = new Event("School Camp /from 2021-08-24 /to 2021-08-27");
+ assertEquals("E | 0 | School Camp /from 2021-08-24 /to 2021-08-27", event2.getTaskString());
+ assertEquals("[E][ ] School Camp (from Tuesday, 24 August, 2021 12:00 am to Friday, "
+ + "27 August, 2021 11:59 pm)", event2.toString());
+ } catch (RoverException | DateTimeParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkTaskString_afterMarkingItDone() {
+ try {
+ Event event = new Event("School Camp /from 2021-08-24 1800 /to 2021-08-27 1800");
+ event.setDone();
+ assertEquals("E | 1 | School Camp /from 2021-08-24 1800 /to 2021-08-27 1800", event.getTaskString());
+ assertEquals("[E][X] School Camp (from Tuesday, 24 August, 2021 6:00 pm to Friday, "
+ + "27 August, 2021 6:00 pm)", event.toString());
+
+ Event event2 = new Event("School Camp /from 2021-08-24 /to 2021-08-27");
+ event2.setDone();
+ assertEquals("E | 1 | School Camp /from 2021-08-24 /to 2021-08-27", event2.getTaskString());
+ assertEquals("[E][X] School Camp (from Tuesday, 24 August, 2021 12:00 am to Friday, "
+ + "27 August, 2021 11:59 pm)", event2.toString());
+ } catch (RoverException | DateTimeParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIsBefore() {
+ try {
+ Event event = new Event("School Camp /from 2021-08-24 1800 /to 2021-08-27 1800");
+ assertFalse(event.isBefore("2021-08-24 1759"));
+ assertFalse(event.isBefore("2021-08-24"));
+ assertFalse(event.isBefore("24-08-21 1800"));
+ assertTrue(event.isBefore("24/08/2021 1801"));
+ assertTrue(event.isBefore("27/08/21 1759"));
+ assertTrue(event.isBefore("27-08-2021 1800"));
+ assertTrue(event.isBefore("2021-08-27 1801"));
+ assertTrue(event.isBefore("2021-08-28"));
+ } catch (RoverException | DateTimeParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIsAfter() {
+ try {
+ Event event = new Event("School Camp /from 2021-08-24 1800 /to 2021-08-27 1800");
+ assertTrue(event.isAfter("2021-08-24 1759"));
+ assertFalse(event.isAfter("2021-08-24"));
+ assertTrue(event.isAfter("24-08-21 1759"));
+ assertFalse(event.isAfter("24/08/2021 1800"));
+ assertFalse(event.isAfter("27/08/21 1759"));
+ assertFalse(event.isAfter("27-08-2021 1800"));
+ assertFalse(event.isAfter("2021-08-27 1759"));
+ assertFalse(event.isAfter("2021-08-28"));
+ } catch (RoverException | DateTimeParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/rover/task/TaskListTest.java b/src/test/java/rover/task/TaskListTest.java
new file mode 100644
index 0000000000..281c0a66bf
--- /dev/null
+++ b/src/test/java/rover/task/TaskListTest.java
@@ -0,0 +1,211 @@
+package rover.task;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.time.format.DateTimeParseException;
+
+import org.junit.jupiter.api.Test;
+
+import rover.exceptions.RoverException;
+import rover.ui.TextUi;
+import rover.ui.Ui;
+
+public class TaskListTest {
+
+ @Test
+ public void checkEmptyTaskList() {
+ assertEquals(0, new TaskList().getNumberOfTasks());
+ try {
+ assertEquals(0, new TaskList(null).getNumberOfTasks());
+ } catch (RoverException e) {
+ fail("Exception should not be thrown");
+ }
+ }
+
+ @Test
+ public void checkIfExceptionThrown_corruptedTaskString() {
+ try {
+ new TaskList(null, "T | 2 | read book");
+ } catch (RoverException e) {
+ assertEquals("Possible corruption in saved tasks.", e.getMessage());
+ }
+ try {
+ new TaskList(null, "X | 0 | read book");
+ } catch (RoverException e) {
+ assertEquals("Possible corruption in saved tasks.", e.getMessage());
+ }
+ try {
+ new TaskList(null, "T | 0 |read book");
+ } catch (RoverException e) {
+ assertEquals("Possible corruption in saved tasks.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIfExceptionThrown_corruptedTaskString2() {
+ try {
+ new TaskList(null, "D | 0 | read book 10/12/21");
+ } catch (RoverException e) {
+ assertEquals("A deadline task must be a task followed with '/by (deadline)'.", e.getMessage());
+ }
+ try {
+ new TaskList(null, "D | 1 | read book /by 10/12/21 /by 09/08/21");
+ } catch (RoverException e) {
+ assertEquals("A deadline task must be a task followed with '/by (deadline)'.", e.getMessage());
+ }
+ try {
+ new TaskList(null, "D | 0 | read book /by 10/12/");
+ } catch (RoverException e) {
+ fail("This exception should not be thrown");
+ } catch (DateTimeParseException e) {
+ assertEquals("Unable to parse date: 10/12/", e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIfExceptionThrown_corruptedTaskString3() {
+ Ui ui = new TextUi();
+ try {
+ new TaskList(ui, "E | 0 | read book 10/12/21 09/08/21");
+ } catch (RoverException e) {
+ assertEquals("An event task must be a task followed with '/from (start) /to (end)'.", e.getMessage());
+ }
+ try {
+ new TaskList(ui, "E | 1 | read book /from 10/12/21 /from 09/08/21");
+ } catch (RoverException e) {
+ assertEquals("An event task must be a task followed with '/from (start) /to (end)'.", e.getMessage());
+ }
+ try {
+ new TaskList(ui, "E | 0 | read book /from 10/12/21 09/08/21");
+ } catch (RoverException e) {
+ assertEquals("An event task must be a task followed with '/from (start) /to (end)'.", e.getMessage());
+ }
+ try {
+ new TaskList(ui, "E | 0 | read book /from 10/12/21 /to 09/08/");
+ } catch (RoverException e) {
+ fail("This exception should not be thrown");
+ } catch (DateTimeParseException e) {
+ assertEquals("Unable to parse date: 09/08/", e.getMessage());
+ }
+ try {
+ new TaskList(ui, "E | 0 | read book /from 10/12/21 /to 09/08/21");
+ } catch (RoverException e) {
+ assertEquals("The start date and time must be before the end date and time.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkTaskList_forProperStrings() {
+ String[] taskStrings = {
+ "T | 0 | read book",
+ "D | 1 | return book /by 2021-08-24 1800",
+ "E | 0 | project meeting /from 2021-08-25 1400 /to 2021-08-25 1600"
+ };
+ try {
+ Ui ui = new TextUi();
+ TaskList taskList = new TaskList(ui, taskStrings);
+ assertEquals(3, taskList.getNumberOfTasks());
+ assertEquals("T | 0 | read book", taskList.getTasks().get(0).getTaskString());
+ assertEquals("D | 1 | return book /by 2021-08-24 1800", taskList.getTasks().get(1).getTaskString());
+ assertEquals(
+ "E | 0 | project meeting /from 2021-08-25 1400 /to 2021-08-25 1600",
+ taskList.getTasks().get(2).getTaskString()
+ );
+ } catch (RoverException e) {
+ fail("Exception should not be thrown");
+ }
+ }
+
+ @Test
+ public void checkListingOfTasks() {
+ TaskList taskList = new TaskList();
+ Ui ui = new TextUi();
+ try {
+ taskList.addTask(new Todo("read book"), ui);
+ taskList.addTask(new Deadline("return book /by 2021-08-24 1800", ui), ui);
+ taskList.addTask(new Event("project meeting /from 2021-08-25 1400 /to 2021-08-25 1600", ui), ui);
+ } catch (RoverException | DateTimeParseException e) {
+ fail("Exception should not be thrown");
+ }
+
+ // Capture the output of the UI
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+
+ taskList.showTasks(ui, (task, ignore) -> true, "in your list");
+
+ String expectedOutput = """
+ --------------------------------------------
+ Here are the tasks in your list:
+ 1. [T][ ] read book
+ 2. [D][ ] return book (by: Tuesday, 24 August, 2021 6:00 pm)
+ 3. [E][ ] project meeting (from Wednesday, 25 August, 2021 2:00 pm to Wednesday, 25 August, 2021 4:00 pm)
+ --------------------------------------------
+ """.replace("\n", System.lineSeparator());
+ assertEquals(expectedOutput, outContent.toString());
+ }
+
+ @Test
+ public void checkAddingOfTasks() {
+ TaskList taskList = new TaskList();
+ Ui ui = new TextUi();
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+
+ try {
+ taskList.addTask(new Todo("read book"), ui);
+ } catch (RoverException | DateTimeParseException e) {
+ fail("Exception should not be thrown");
+ }
+
+ String expectedOutput = """
+ --------------------------------------------
+ Got it. I've added this task:
+ [T][ ] read book
+ Now you have 1 task in the list.
+ --------------------------------------------
+ """.replace("\n", System.lineSeparator());
+ assertEquals(expectedOutput, outContent.toString());
+ outContent.reset();
+
+ try {
+ taskList.addTask(new Deadline("return book /by 2030-08-24 1800", ui), ui);
+ } catch (RoverException | DateTimeParseException e) {
+ fail("Exception should not be thrown");
+ }
+
+ String expectedOutput2 = """
+ --------------------------------------------
+ Got it. I've added this task:
+ [D][ ] return book (by: Saturday, 24 August, 2030 6:00 pm)
+ Now you have 2 tasks in the list.
+ --------------------------------------------
+ """.replace("\n", System.lineSeparator());
+ assertEquals(expectedOutput2, outContent.toString());
+ outContent.reset();
+
+ try {
+ taskList.addTask(new Event("project meeting /from 2030-08-25 1400 /to 2030-08-25 1600"), ui);
+ } catch (RoverException | DateTimeParseException e) {
+ fail("Exception should not be thrown");
+ }
+
+ String expectedOutput3 = """
+ --------------------------------------------
+ Got it. I've added this task:
+ [E][ ] project meeting (from Sunday, 25 August, 2030 2:00 pm to Sunday, 25 August, 2030 4:00 pm)
+ Now you have 3 tasks in the list.
+ --------------------------------------------
+ """.replace("\n", System.lineSeparator());
+ assertEquals(expectedOutput3, outContent.toString());
+ outContent.reset();
+
+ try {
+ taskList.addTask(new Todo("read book"), ui);
+ } catch (RoverException | DateTimeParseException e) {
+ assertEquals("This task already exists in the list.", e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/rover/task/TaskTest.java b/src/test/java/rover/task/TaskTest.java
new file mode 100644
index 0000000000..2938b6edaf
--- /dev/null
+++ b/src/test/java/rover/task/TaskTest.java
@@ -0,0 +1,55 @@
+package rover.task;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import rover.exceptions.RoverException;
+
+public class TaskTest {
+
+
+ @Test
+ public void checkIfTaskIsAbstract() {
+ assertThrows(InstantiationException.class, () -> {
+ Class> clazz = Task.class;
+ clazz.getDeclaredConstructor(String.class).newInstance("test");
+ });
+ }
+
+ @Test
+ public void checkForTaskInEquality() {
+ try {
+ Task task1 = new Todo("read book");
+ Task task2 = new Todo(" read book");
+ Task task3 = new Todo("read book ");
+ assertNotEquals(task1, task2);
+ assertNotEquals(task1, task3);
+ assertNotEquals(task2, task3);
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkForTaskEquality() {
+ try {
+ Task task1 = new Todo("read book");
+ Task task2 = new Todo("read book");
+ Task task3 = new Deadline("read book /by 01/01/2021 1800");
+ Task task4 = new Deadline("read book /by 01/02/2021 1800");
+ Task task5 = new Event("read book /from 01/01/2021 1800 /to 01/01/2021 1900");
+ Task task6 = new Event("read book /from 01/01/2022 1800 /to 01/01/2022 1900");
+ assertEquals(task1, task2);
+ assertNotEquals(task1, task3);
+ assertNotEquals(task1, task5);
+ assertNotEquals(task3, task4);
+ assertNotEquals(task3, task5);
+ assertNotEquals(task5, task6);
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+}
diff --git a/src/test/java/rover/task/TodoTest.java b/src/test/java/rover/task/TodoTest.java
new file mode 100644
index 0000000000..87a32ca220
--- /dev/null
+++ b/src/test/java/rover/task/TodoTest.java
@@ -0,0 +1,62 @@
+package rover.task;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import org.junit.jupiter.api.Test;
+
+import rover.exceptions.RoverException;
+
+public class TodoTest {
+ @Test
+ public void checkIfExceptionThrown_emptyStringInitialisation() {
+ assertThrowsExactly(RoverException.class, () -> {
+ new Todo("");
+ });
+ }
+
+ @Test
+ public void checkTaskString_beforeMarkingItDone() {
+ try {
+ Todo todo = new Todo("read book");
+ assertEquals("T | 0 | read book", todo.getTaskString());
+ assertEquals("[T][ ] read book", todo.toString());
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkTaskString_afterMarkingItDone() {
+ try {
+ Todo todo = new Todo("read book");
+ todo.setDone();
+ assertEquals("T | 1 | read book", todo.getTaskString());
+ assertEquals("[T][X] read book", todo.toString());
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIfIsBeforeAlwaysReturnsFalse() {
+ try {
+ Todo todo = new Todo("read book");
+ assertFalse(todo.isBefore("2021-08-24T18:00"));
+ assertFalse(todo.isBefore("garbage"));;
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Test
+ public void checkIfIsAfterAlwaysReturnsFalse() {
+ try {
+ Todo todo = new Todo("read book");
+ assertFalse(todo.isAfter("2021-08-24T18:00"));
+ assertFalse(todo.isAfter("garbage"));;
+ } catch (RoverException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
deleted file mode 100644
index 657e74f6e7..0000000000
--- a/text-ui-test/EXPECTED.TXT
+++ /dev/null
@@ -1,7 +0,0 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
-
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat
deleted file mode 100644
index 0873744649..0000000000
--- a/text-ui-test/runtest.bat
+++ /dev/null
@@ -1,21 +0,0 @@
-@ECHO OFF
-
-REM create bin directory if it doesn't exist
-if not exist ..\bin mkdir ..\bin
-
-REM delete output from previous run
-if exist ACTUAL.TXT del ACTUAL.TXT
-
-REM compile the code into the bin folder
-javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\*.java
-IF ERRORLEVEL 1 (
- echo ********** BUILD FAILURE **********
- exit /b 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
-
-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
deleted file mode 100644
index c9ec870033..0000000000
--- a/text-ui-test/runtest.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-
-# create bin directory if it doesn't exist
-if [ ! -d "../bin" ]
-then
- mkdir ../bin
-fi
-
-# delete output from previous run
-if [ -e "./ACTUAL.TXT" ]
-then
- rm ACTUAL.TXT
-fi
-
-# compile the code into the bin folder, terminates if error occurred
-if ! javac -cp ../src/main/java -Xlint:none -d ../bin ../src/main/java/*.java
-then
- echo "********** BUILD FAILURE **********"
- exit 1
-fi
-
-# 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
-
-# convert to UNIX format
-cp EXPECTED.TXT EXPECTED-UNIX.TXT
-dos2unix ACTUAL.TXT EXPECTED-UNIX.TXT
-
-# compare the output to the expected output
-diff ACTUAL.TXT EXPECTED-UNIX.TXT
-if [ $? -eq 0 ]
-then
- echo "Test result: PASSED"
- exit 0
-else
- echo "Test result: FAILED"
- exit 1
-fi
\ No newline at end of file
diff --git a/text-ui-test/unix/EXPECTED_1.TXT b/text-ui-test/unix/EXPECTED_1.TXT
new file mode 100644
index 0000000000..51ece2838a
--- /dev/null
+++ b/text-ui-test/unix/EXPECTED_1.TXT
@@ -0,0 +1,89 @@
+--------------------------------------------
+Hello! I'm Rover
+___
+| _`\
+| (_) ) _ _ _ __ _ __
+| , / /'_`\ ( ) ( ) /'__`\( '__)
+| |\ \ ( (_) )| \_/ |( ___/| |
+(_) (_)`\___/'`\___/'`\____)(_)
+
+I am your personal task manager.
+What can I do for you?
+--------------------------------------------
+--------------------------------------------
+Oops! Error: Please enter a command.
+--------------------------------------------
+--------------------------------------------
+I'm sorry, but I don't know what that means.
+The following commands are supported:
+ You can add a task by typing:
+ - todo (description)
+ - deadline (description) /by (deadline)
+ - event (description) /from (start) /to (end)
+ List the existing tasks by typing 'list'.
+ Mark a task as done by typing 'mark (task number)'.
+ Mark a task as not done by typing 'unmark (task number)'.
+ Delete a task by typing 'delete (task number)'.
+ Show tasks before a certain date and/or time by typing 'show before (date) (time)'.
+ Show tasks after a certain date and/or time by typing 'show after (date) (time)'.
+ Exit the program by typing 'bye'.
+--------------------------------------------
+--------------------------------------------
+Oops! Error: The description of a task cannot be empty.
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [T][ ] buy milk
+Now you have 1 task in the list.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+--------------------------------------------
+--------------------------------------------
+I'm sorry, but I don't know what that means.
+The following commands are supported:
+ You can add a task by typing:
+ - todo (description)
+ - deadline (description) /by (deadline)
+ - event (description) /from (start) /to (end)
+ List the existing tasks by typing 'list'.
+ Mark a task as done by typing 'mark (task number)'.
+ Mark a task as not done by typing 'unmark (task number)'.
+ Delete a task by typing 'delete (task number)'.
+ Show tasks before a certain date and/or time by typing 'show before (date) (time)'.
+ Show tasks after a certain date and/or time by typing 'show after (date) (time)'.
+ Exit the program by typing 'bye'.
+--------------------------------------------
+--------------------------------------------
+Oops! Error: The date format should be 'dd/mm/yy'.
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+Now you have 2 tasks in the list.
+--------------------------------------------
+--------------------------------------------
+Oops! Error: The time format should be 'hh:mm'.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [T][ ] read book
+Now you have 3 tasks in the list.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Saving your tasks...
+Tasks saved successfully!
+Bye. Hope to see you again soon!
+--------------------------------------------
diff --git a/text-ui-test/unix/EXPECTED_2.TXT b/text-ui-test/unix/EXPECTED_2.TXT
new file mode 100644
index 0000000000..1bab2b30fb
--- /dev/null
+++ b/text-ui-test/unix/EXPECTED_2.TXT
@@ -0,0 +1,64 @@
+--------------------------------------------
+Hello! I'm Rover
+___
+| _`\
+| (_) ) _ _ _ __ _ __
+| , / /'_`\ ( ) ( ) /'__`\( '__)
+| |\ \ ( (_) )| \_/ |( ___/| |
+(_) (_)`\___/'`\___/'`\____)(_)
+
+I am your personal task manager.
+What can I do for you?
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Oops! Error: This task already exists in the list.
+--------------------------------------------
+--------------------------------------------
+Nice! I've marked this task as done:
+[T][X] buy milk
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][X] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Oops! Error: Please specify a valid task number to be marked as done.
+You only have 3 tasks in total.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][X] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Oops! Error: Please specify a valid task number to be deleted.
+You only have 3 tasks in total.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][X] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Here are the tasks before 02/02/25:
+1. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+--------------------------------------------
+--------------------------------------------
+Here are the tasks after 30/01/25:
+1. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+--------------------------------------------
+--------------------------------------------
+Saving your tasks...
+Tasks saved successfully!
+Bye. Hope to see you again soon!
+--------------------------------------------
diff --git a/text-ui-test/unix/input_1.txt b/text-ui-test/unix/input_1.txt
new file mode 100644
index 0000000000..bd5ca92298
--- /dev/null
+++ b/text-ui-test/unix/input_1.txt
@@ -0,0 +1,13 @@
+
+help
+todo
+todo buy milk
+list
+CS3230 Assignment 2 /by 010225 2359
+deadline CS3230 Assignment 2 /by 010225 2359
+deadline CS3230 Assignment 2 /by 01/02/25 2359
+event CS3230 Lecture /from 29/01/25 2pm /to 4pm
+list
+todo read book
+list
+bye
\ No newline at end of file
diff --git a/text-ui-test/unix/input_2.txt b/text-ui-test/unix/input_2.txt
new file mode 100644
index 0000000000..bafc03a179
--- /dev/null
+++ b/text-ui-test/unix/input_2.txt
@@ -0,0 +1,11 @@
+list
+todo read book
+mark 1
+list
+mark 4
+list
+delete 4
+list
+show before 02/02/25
+show after 30/01/25
+bye
\ No newline at end of file
diff --git a/text-ui-test/unix/runtest.sh b/text-ui-test/unix/runtest.sh
new file mode 100644
index 0000000000..d4e62bb643
--- /dev/null
+++ b/text-ui-test/unix/runtest.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+# create bin directory if it doesn't exist
+if [ ! -d "../../bin" ]
+then
+ mkdir ../../bin
+fi
+
+# delete output from previous run
+if [ -e "./ACTUAL_1.TXT" ]
+then
+ rm ACTUAL_1.TXT
+fi
+
+# delete data directory from previous run
+if [ -d "./data" ]
+then
+ rm -r ./data
+fi
+
+# compile the code into the bin folder, terminates if error occurred
+shopt -s globstar
+# Define excluded files (space-separated list)
+EXCLUDE_LIST="Launcher.java Main.java Gui.java DialogBox.java"
+FILES=$(find ../../src/main/java -type f -name "*.java" | grep -Ev "$(echo $EXCLUDE_LIST | sed 's/ /|/g')")
+if ! javac -cp ../../src/main/java -Xlint:none -d ../../bin $FILES; then
+ echo "********** BUILD FAILURE **********"
+ exit 1
+fi
+
+# run the program, feed commands from input_1.txt file and redirect the output to the ACTUAL_1.TXT
+java -cp ../../bin rover.main.Rover < input_1.txt > ACTUAL_1.TXT
+
+# compare the output to the expected output
+diff ACTUAL_1.TXT EXPECTED_1.TXT
+if [ $? -eq 0 ]
+then
+ echo "Input 1 result: PASSED"
+else
+ echo "Input 2 result: FAILED"
+ exit 1
+fi
+
+java -cp ../../bin rover.main.Rover < input_2.txt > ACTUAL_2.TXT
+
+# compare the output to the expected output
+diff ACTUAL_2.TXT EXPECTED_2.TXT
+if [ $? -eq 0 ]
+then
+ echo "Input 2 result: PASSED"
+ exit 0
+else
+ echo "Input 2 result: FAILED"
+ exit 1
+fi
diff --git a/text-ui-test/windows/EXPECTED_1.TXT b/text-ui-test/windows/EXPECTED_1.TXT
new file mode 100644
index 0000000000..55e1fcd40a
--- /dev/null
+++ b/text-ui-test/windows/EXPECTED_1.TXT
@@ -0,0 +1,93 @@
+--------------------------------------------
+Hello! I'm Rover
+___
+| _`\
+| (_) ) _ _ _ __ _ __
+| , / /'_`\ ( ) ( ) /'__`\( '__)
+| |\ \ ( (_) )| \_/ |( ___/| |
+(_) (_)`\___/'`\___/'`\____)(_)
+
+I am your personal task manager.
+What can I do for you?
+--------------------------------------------
+--------------------------------------------
+Oops! Error: Please enter a command.
+--------------------------------------------
+--------------------------------------------
+I'm sorry, but I don't know what that means.
+The following commands are supported:
+ You can add a task by typing:
+ - todo (description)
+ - deadline (description) /by (deadline)
+ - event (description) /from (start) /to (end)
+ List the existing tasks by typing 'list'.
+ Mark a task as done by typing 'mark (task number)'.
+ Mark a task as not done by typing 'unmark (task number)'.
+ Delete a task by typing 'delete (task number)'.
+ Show tasks before a certain date and/or time by typing 'show before (date) (time)'.
+ Show tasks after a certain date and/or time by typing 'show after (date) (time)'.
+ Exit the program by typing 'bye'.
+--------------------------------------------
+--------------------------------------------
+Oops! Error: The description of a task cannot be empty.
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [T][ ] buy milk
+Now you have 1 task in the list.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+--------------------------------------------
+--------------------------------------------
+I'm sorry, but I don't know what that means.
+The following commands are supported:
+ You can add a task by typing:
+ - todo (description)
+ - deadline (description) /by (deadline)
+ - event (description) /from (start) /to (end)
+ List the existing tasks by typing 'list'.
+ Mark a task as done by typing 'mark (task number)'.
+ Mark a task as not done by typing 'unmark (task number)'.
+ Delete a task by typing 'delete (task number)'.
+ Show tasks before a certain date and/or time by typing 'show before (date) (time)'.
+ Show tasks after a certain date and/or time by typing 'show after (date) (time)'.
+ Exit the program by typing 'bye'.
+--------------------------------------------
+--------------------------------------------
+Oops! Error: The date format should be 'dd/mm/yy'.
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+Now you have 2 tasks in the list.
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+Now you have 3 tasks in the list.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+--------------------------------------------
+--------------------------------------------
+Got it. I've added this task:
+ [T][ ] read book
+Now you have 4 tasks in the list.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+4. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Saving your tasks...
+Tasks saved successfully!
+Bye. Hope to see you again soon!
+--------------------------------------------
diff --git a/text-ui-test/windows/EXPECTED_2.TXT b/text-ui-test/windows/EXPECTED_2.TXT
new file mode 100644
index 0000000000..f9b6859c8d
--- /dev/null
+++ b/text-ui-test/windows/EXPECTED_2.TXT
@@ -0,0 +1,69 @@
+--------------------------------------------
+Hello! I'm Rover
+___
+| _`\
+| (_) ) _ _ _ __ _ __
+| , / /'_`\ ( ) ( ) /'__`\( '__)
+| |\ \ ( (_) )| \_/ |( ___/| |
+(_) (_)`\___/'`\___/'`\____)(_)
+
+I am your personal task manager.
+What can I do for you?
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][ ] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+4. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Oops! Error: This task already exists in the list.
+--------------------------------------------
+--------------------------------------------
+Nice! I've marked this task as done:
+[T][X] buy milk
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][X] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+4. [T][ ] read book
+--------------------------------------------
+--------------------------------------------
+Nice! I've marked this task as done:
+[T][X] read book
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][X] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+4. [T][X] read book
+--------------------------------------------
+--------------------------------------------
+Noted. I've removed this task:
+[T][X] read book
+Now you have 3 tasks in the list.
+--------------------------------------------
+--------------------------------------------
+Here are the tasks in your list:
+1. [T][X] buy milk
+2. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+3. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+--------------------------------------------
+--------------------------------------------
+Here are the tasks before 02/02/25:
+1. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+2. [E][ ] CS3230 Lecture (from Wednesday, 29 January, 2025 2:00 pm to Wednesday, 29 January, 2025 4:00 pm)
+--------------------------------------------
+--------------------------------------------
+Here are the tasks after 30/01/25:
+1. [D][ ] CS3230 Assignment 2 (by: Saturday, 01 February, 2025 11:59 pm)
+--------------------------------------------
+--------------------------------------------
+Saving your tasks...
+Tasks saved successfully!
+Bye. Hope to see you again soon!
+--------------------------------------------
diff --git a/text-ui-test/windows/input_1.txt b/text-ui-test/windows/input_1.txt
new file mode 100644
index 0000000000..bd5ca92298
--- /dev/null
+++ b/text-ui-test/windows/input_1.txt
@@ -0,0 +1,13 @@
+
+help
+todo
+todo buy milk
+list
+CS3230 Assignment 2 /by 010225 2359
+deadline CS3230 Assignment 2 /by 010225 2359
+deadline CS3230 Assignment 2 /by 01/02/25 2359
+event CS3230 Lecture /from 29/01/25 2pm /to 4pm
+list
+todo read book
+list
+bye
\ No newline at end of file
diff --git a/text-ui-test/windows/input_2.txt b/text-ui-test/windows/input_2.txt
new file mode 100644
index 0000000000..bafc03a179
--- /dev/null
+++ b/text-ui-test/windows/input_2.txt
@@ -0,0 +1,11 @@
+list
+todo read book
+mark 1
+list
+mark 4
+list
+delete 4
+list
+show before 02/02/25
+show after 30/01/25
+bye
\ No newline at end of file
diff --git a/text-ui-test/windows/runtest.bat b/text-ui-test/windows/runtest.bat
new file mode 100644
index 0000000000..ba77cd38ac
--- /dev/null
+++ b/text-ui-test/windows/runtest.bat
@@ -0,0 +1,46 @@
+@ECHO OFF
+
+REM delete output from previous run
+if exist ACTUAL_1.TXT del ACTUAL_1.TXT
+
+if exist data rmdir /s /q data
+
+set exclude_list="Launcher.java Main.java Gui.java DialogBox.java"
+REM compile the code into the bin folder
+for /R ..\..\src\main\java %%f in (*.java) do (
+ echo %exclude_list% | findstr /i /c:"%%~nxf" >nul || (
+ javac -cp ..\..\src\main\java -Xlint:none -d ..\..\bin "%%f"
+ IF ERRORLEVEL 1 (
+ echo ********** BUILD FAILURE **********
+ exit /b 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 rover.main.Rover < input_1.txt > ACTUAL_1.TXT
+
+REM compare the output to the expected output
+FC ACTUAL_1.TXT EXPECTED_1.TXT
+IF ERRORLEVEL 1 (
+ echo ********** Input 1 FAILURE **********
+ exit /b 1
+)
+
+echo ********** Input 1 SUCCESS **********
+
+REM delete output from previous run
+if exist ACTUAL_2.TXT del ACTUAL_2.TXT
+
+REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT
+java -classpath ..\..\bin rover.main.Rover < input_2.txt > ACTUAL_2.TXT
+
+REM compare the output to the expected output
+FC ACTUAL_2.TXT EXPECTED_2.TXT
+IF ERRORLEVEL 1 (
+ echo ********** Input 2 FAILURE **********
+ exit /b 1
+)
+
+echo ********** Input 2 SUCCESS **********
\ No newline at end of file