diff --git a/.gitignore b/.gitignore index f69985ef1f..e70b899aff 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,5 @@ src/main/resources/docs/ *.iml bin/ -/text-ui-test/ACTUAL.txt -text-ui-test/EXPECTED-UNIX.TXT +/text-textUi-test/ACTUAL.txt +text-textUi-test/EXPECTED-UNIX.TXT diff --git a/README.md b/README.md index 8715d4d915..6afcc71ac5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# duke.Duke project template This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. @@ -8,17 +8,10 @@ Prerequisites: JDK 11, update Intellij to the most recent version. 1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first) 1. Open the project into Intellij as follows: - 1. Click `Open`. - 1. Select the project directory, and click `OK`. - 1. If there are any further prompts, accept the defaults. + 1. Click `Open`. + 1. Select the project directory, and click `OK`. + 1. If there are any further prompts, accept the defaults. 1. Configure the project to use **JDK 11** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
In the same dialog, set the **Project language level** field to the `SDK default` option. -3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: - ``` - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - ``` +3. After that, locate the `src/main/java/duke/Launcher.java` file, right-click it, and choose `Run Launcher.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: + ![Duke](docs/Ui.png) \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..c97d0f76ba --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '5.1.0' + id 'checkstyle' + id 'org.openjfx.javafxplugin' version '0.0.10' +} + +repositories { + mavenCentral() +} + +dependencies { + + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.9.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.9.0' + + String javaFxVersion = '11' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +checkstyle { + toolVersion = '10.2' +} + +application { + mainClassName = "duke.Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +run { + standardInput = System.in +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..d618671b83 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..39efb6e4ac --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/data/duke.txt b/data/duke.txt new file mode 100644 index 0000000000..eacb83bfa9 --- /dev/null +++ b/data/duke.txt @@ -0,0 +1,3 @@ +T,1,return book +E,0,project meeting ,2022-10-11 +D,0,finish project,2022-09-15 diff --git a/docs/README.md b/docs/README.md index 8077118ebe..12447da694 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,90 @@ -# User Guide +# Commands -## Features +## Add a todo task: `todo DESCRIPTION` +Adds a task with the given `DESCRIPTION`. -### Feature-ABC +**Example of usage:** + ``` + todo return book + ``` -Description of the feature. +## Add a deadline task: `deadline DESCRIPTION /by yyyy-MM-dd` +Adds a task with the given `DESCRIPTION` and a specified deadline. -### Feature-XYZ +**Example of usage:** +``` +deadline read book /by 17-09-2022 +``` + +## Add an event: `event DESCRIPTION /at yyyy-MM-dd` +Adds an event with the given `DESCRIPTION` and a specified date. + +Date and time format is the same as the deadline command. + +**Example of usage:** +``` +event CS2103T lecture /at 17-09-2022 +``` -Description of the feature. +## List all tasks: `list` +Lists all tasks in the task list. -## Usage -### `Keyword` - Describe action +## Marking a task as done: `mark INDEX` — +Marks the task at the given `INDEX` as done. -Describe the action and its outcome. +You may want to use the `list` command to find the index of the task you want to mark as done. -Example of usage: +**Example of usage:** +``` +mark 1 + ``` + +## Marking a task as not done: `unmark INDEX` — +Marks the task at the given `INDEX` as not done. + +**Example of usage:** + ``` + unmark 1 + ``` + +## Delete a task - `delete INDEX` — +Deletes the task at the given `INDEX`. -`keyword (optional arguments)` +**Example of usage:** + ``` +delete 1 + ``` -Expected outcome: +## Find tasks by keyword: `find KEYWORD` — +Finds all tasks whose description contains the given `KEYWORD`. +The search is case-insensitive. -Description of the outcome. +**Example of usage:** ``` -expected output +find book ``` + +## Exit the program: `exit` +Exits the program with a goodbye message. + +# Advanced +## Data Storage +All the data is saved in the file `./data/duke.txt`. You can modify task list by directly editing +this file. Each line of the file describes one task. The format is the following: + ``` +,,, + ``` +- `TASK Type` is `T` for todo task, `D` for deadline, and `E` for event tasks. +- `COMPLETION STATUS` is 0 or 1 depending on whether the task is completed or not. +- `DESCRIPTION` is the description of the task. +- Currently, `ARGS...` only take in a date for deadline and events. The date must be in the + format `yyyy-MM-dd`. Specifically, day and month number should not have leading zeros. + +Example data file: + ``` +T,1,return book +E,0,project meeting ,2022-10-11 +D,0,finish project,2022-09-15 + ``` \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..8634d3e748 Binary files /dev/null and b/docs/Ui.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..f3d88b1c2f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..b7c8c5dbf5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/duke/Duke.java b/src/main/java/duke/Duke.java new file mode 100644 index 0000000000..878f91715e --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,66 @@ +package duke; + +import java.io.File; +import java.util.Scanner; + +import duke.commands.Command; +import duke.exceptions.InvalidCommandException; +import duke.parser.Parser; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.TextUi; + +/** + * Runs the CLI Application. + */ +class Duke { + private static final String DATA_PATH = new File("").getAbsolutePath() + "/data/duke.txt"; + private final TaskList taskList; + private final Storage storage; + private final TextUi textUi; + + public Duke() { + Storage storage = new Storage(DATA_PATH); + this.storage = storage; + this.taskList = new TaskList(storage.load()); + this.textUi = new TextUi(); + } + + + /** + * Initialises the application and begins interacting with the user. + */ + public void run() { + textUi.showWelcomeMessage(); + + boolean isExit = false; + + Scanner scanner = new Scanner(System.in); + Duke duke = new Duke(); + + while (!isExit && scanner.hasNextLine()) { + try { + String fullCommand = scanner.nextLine(); + + Command c = Parser.parse(fullCommand); + Response response = c.execute(taskList, storage); + System.out.println(response.getMessage()); + isExit = c.isExit(); + } catch (InvalidCommandException e) { + System.out.println(e.getMessage()); + } + } + + scanner.close(); + } + + /** + * Note: You are strongly encouraged to customize the chatbot name, + * command/display formats, and even the personality of the chatbot + * to make your chatbot unique. + */ + public static void main(String[] args) { + Duke duke = new Duke(); + duke.run(); + } +} diff --git a/src/main/java/duke/DukeApp.java b/src/main/java/duke/DukeApp.java new file mode 100644 index 0000000000..4af22edc16 --- /dev/null +++ b/src/main/java/duke/DukeApp.java @@ -0,0 +1,145 @@ +package duke; + +import java.io.File; + +import duke.commands.Command; +import duke.exceptions.InvalidCommandException; +import duke.parser.Parser; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.DialogBox; +import duke.ui.TextUi; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +/** + * Runs the GUI application. + */ +public class DukeApp extends Application { + private static final String DATA_PATH = new File("").getAbsolutePath() + "/data/duke.txt"; + private ScrollPane scrollPane; + private VBox dialogContainer; + private TextField userInput; + private Storage storage; + private TaskList taskList; + private TextUi textUi; + + @Override + public void start(Stage stage) { + + textUi = new TextUi(); + storage = new Storage(DATA_PATH); + taskList = new TaskList(storage.load()); + + scrollPane = new ScrollPane(); + dialogContainer = new VBox(); + scrollPane.setContent(dialogContainer); + + userInput = new TextField(); + Button submitButton = new Button("Done"); + + AnchorPane mainLayout = new AnchorPane(); + mainLayout.getChildren().addAll(scrollPane, userInput, submitButton); + + Scene scene = new Scene(mainLayout); + + stage.setTitle("Duke"); + stage.setResizable(false); + stage.setMinHeight(600.0); + stage.setMinWidth(400.0); + + mainLayout.setPrefSize(400.0, 600.0); + + scrollPane.setPrefSize(385, 535); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS); + + scrollPane.setVvalue(1.0); + scrollPane.setFitToWidth(true); + + dialogContainer.setPrefHeight(Region.USE_COMPUTED_SIZE); + + userInput.setPrefWidth(325.0); + + submitButton.setPrefWidth(55.0); + + AnchorPane.setTopAnchor(scrollPane, 1.0); + + AnchorPane.setBottomAnchor(submitButton, 1.0); + AnchorPane.setRightAnchor(submitButton, 1.0); + + AnchorPane.setLeftAnchor(userInput , 1.0); + AnchorPane.setBottomAnchor(userInput, 1.0); + + submitButton.setOnMouseClicked((event) -> { + handleUserInput(); + }); + + userInput.setOnAction((event) -> { + handleUserInput(); + }); + + //Scroll down to the end every time dialogContainer's height changes. + dialogContainer.heightProperty().addListener((observable) -> scrollPane.setVvalue(1.0)); + + stage.setScene(scene); + stage.show(); + + welcomeUser(); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + private void handleUserInput() { + boolean isExit = false; + + Label userText = new Label(userInput.getText()); + + final String userCommand = userInput.getText(); + + Label dukeResult; + + try { + + final Command c = Parser.parse(userCommand); + isExit = c.isExit(); + + Response response = c.execute(taskList, storage); + + dukeResult = new Label(response.getMessage()); + + } catch (InvalidCommandException e) { + textUi.showMessage(e.getMessage()); + dukeResult = new Label(e.getMessage()); + } + + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(userText), + DialogBox.getDukeDialog(dukeResult) + ); + userInput.clear(); + + if (isExit) { + System.exit(0); + } + } + + /** + * Displays the welcome message to the user. + */ + private void welcomeUser() { + dialogContainer.getChildren().addAll( + DialogBox.getDukeDialog(new Label(TextUi.WELCOME_MESSAGE)) + ); + } +} diff --git a/src/main/java/duke/Launcher.java b/src/main/java/duke/Launcher.java new file mode 100644 index 0000000000..220d23814b --- /dev/null +++ b/src/main/java/duke/Launcher.java @@ -0,0 +1,12 @@ +package duke; + +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(DukeApp.class, args); + } +} diff --git a/src/main/java/duke/Response.java b/src/main/java/duke/Response.java new file mode 100644 index 0000000000..0c1070c5fc --- /dev/null +++ b/src/main/java/duke/Response.java @@ -0,0 +1,13 @@ +package duke; + +public class Response { + private final String message; + + public Response(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/duke/commands/Command.java b/src/main/java/duke/commands/Command.java new file mode 100644 index 0000000000..02b4d2c47c --- /dev/null +++ b/src/main/java/duke/commands/Command.java @@ -0,0 +1,45 @@ +package duke.commands; + +import duke.Response; +import duke.exceptions.InvalidCommandException; +import duke.storage.Storage; +import duke.task.TaskList; + +/** + * An executable command. + */ +public abstract class Command { + /** + * Executes the command. + * + * @param taskList the user's task list + * @param storage storage handler of user data + * @throws InvalidCommandException + */ + public abstract Response execute(TaskList taskList, Storage storage) throws InvalidCommandException; + + /** + * {@return true if and only if this command is the exit command} + */ + public boolean isExit() { + return this instanceof ExitCommand; + } +} + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/duke/commands/CreateDeadlineCommand.java b/src/main/java/duke/commands/CreateDeadlineCommand.java new file mode 100644 index 0000000000..6a8e3d2dcd --- /dev/null +++ b/src/main/java/duke/commands/CreateDeadlineCommand.java @@ -0,0 +1,16 @@ +package duke.commands; + +import duke.task.Deadline; + +/** + * Creates and stores a deadline. + */ +public class CreateDeadlineCommand extends CreateTaskCommand { + /** + * Constructor for CreateDeadlineCommand. + * @param deadline the deadline to be stored + */ + public CreateDeadlineCommand(Deadline deadline) { + super(deadline); + } +} diff --git a/src/main/java/duke/commands/CreateEventCommand.java b/src/main/java/duke/commands/CreateEventCommand.java new file mode 100644 index 0000000000..c4df7c7d65 --- /dev/null +++ b/src/main/java/duke/commands/CreateEventCommand.java @@ -0,0 +1,16 @@ +package duke.commands; + +import duke.task.Event; + +/** + * Creates and stores an event. + */ +public class CreateEventCommand extends CreateTaskCommand { + /** + * Constructor for CreateEventCommand. + * @param event the event to be stored + */ + public CreateEventCommand(Event event) { + super(event); + } +} diff --git a/src/main/java/duke/commands/CreateTaskCommand.java b/src/main/java/duke/commands/CreateTaskCommand.java new file mode 100644 index 0000000000..97ec0c543c --- /dev/null +++ b/src/main/java/duke/commands/CreateTaskCommand.java @@ -0,0 +1,32 @@ +package duke.commands; + +import duke.Response; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +/** + * Creates and stores a task. + */ +class CreateTaskCommand extends Command { + private final Task task; + + /** + * Constructor for CreateTaskCommand. + * @param task the task to be stored + */ + CreateTaskCommand(Task task) { + this.task = task; + } + + @Override + public Response execute(TaskList taskList, Storage storage) { + taskList.store(task); + + String message = String.format("Got it. I've added this task:\n\t%s\n", task); + + storage.save(taskList); + + return new Response(message); + } +} diff --git a/src/main/java/duke/commands/CreateTodoCommand.java b/src/main/java/duke/commands/CreateTodoCommand.java new file mode 100644 index 0000000000..7f22d260cd --- /dev/null +++ b/src/main/java/duke/commands/CreateTodoCommand.java @@ -0,0 +1,16 @@ +package duke.commands; + +import duke.task.Todo; + +/** + * Creates and stores a todo. + */ +public class CreateTodoCommand extends CreateTaskCommand { + /** + * Constructor for CreateTodoCommand. + * @param todo the todo to be stored + */ + public CreateTodoCommand(Todo todo) { + super(todo); + } +} diff --git a/src/main/java/duke/commands/DeleteTaskCommand.java b/src/main/java/duke/commands/DeleteTaskCommand.java new file mode 100644 index 0000000000..70a206d339 --- /dev/null +++ b/src/main/java/duke/commands/DeleteTaskCommand.java @@ -0,0 +1,30 @@ +package duke.commands; + +import duke.Response; +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +/** + * Deletes a task from the task list. + */ +public class DeleteTaskCommand extends Command { + private final int index; + + /** + * Constructor for DeleteTaskCommand. + * @param index the index specifying the task to be deleted. + */ + public DeleteTaskCommand(int index) { + this.index = index; + } + + @Override + public Response execute(TaskList taskList, Storage storage) throws NoSuchTaskException { + Task task = taskList.delete(index); + String message = String.format("Noted. I've removed this task:\n\t%s\n", task); + storage.save(taskList); + return new Response(message); + } +} diff --git a/src/main/java/duke/commands/ExitCommand.java b/src/main/java/duke/commands/ExitCommand.java new file mode 100644 index 0000000000..38a4135c63 --- /dev/null +++ b/src/main/java/duke/commands/ExitCommand.java @@ -0,0 +1,16 @@ +package duke.commands; + +import duke.Response; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.TextUi; + +/** + * A special command that exits the application. + */ +public class ExitCommand extends Command { + @Override + public Response execute(TaskList taskList, Storage storage) { + return new Response(TextUi.GOODBYE_MESSAGE); + } +} diff --git a/src/main/java/duke/commands/FindCommand.java b/src/main/java/duke/commands/FindCommand.java new file mode 100644 index 0000000000..845d144b2e --- /dev/null +++ b/src/main/java/duke/commands/FindCommand.java @@ -0,0 +1,32 @@ +package duke.commands; + +import java.util.List; +import java.util.stream.Collectors; + +import duke.Response; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +public class FindCommand extends Command { + private String keyword; + + public FindCommand(String keyword) { + this.keyword = keyword; + } + + @Override + public Response execute(TaskList taskList, Storage storage) { + List tasks = taskList.getAll().stream() + .filter(task -> task.getTitle().contains(this.keyword)) + .collect(Collectors.toList()); + + if (tasks.isEmpty()) { + String message = String.format("No tasks found for the keyword: [%s]", this.keyword); + return new Response(message); + } else { + TaskList filteredTaskLists = new TaskList(tasks); + return new ListTasksCommand().execute(filteredTaskLists, storage); + } + } +} diff --git a/src/main/java/duke/commands/ListTasksCommand.java b/src/main/java/duke/commands/ListTasksCommand.java new file mode 100644 index 0000000000..72bebd79c1 --- /dev/null +++ b/src/main/java/duke/commands/ListTasksCommand.java @@ -0,0 +1,23 @@ +package duke.commands; + +import duke.Response; +import java.util.List; + +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +/** + * Displays the user's current tasks in a numbered list format. + */ +public class ListTasksCommand extends Command { + @Override + public Response execute(TaskList taskList, Storage storage) { + List tasks = taskList.getAll(); + StringBuilder message = new StringBuilder("Here are the tasks in your list:\n"); + for (int i = 0; i < taskList.getNumTasks(); i++) { + message.append(String.format("%d. %s\n", i + 1, tasks.get(i))); + } + return new Response(message.toString()); + } +} diff --git a/src/main/java/duke/commands/MarkCommand.java b/src/main/java/duke/commands/MarkCommand.java new file mode 100644 index 0000000000..7ed38727d9 --- /dev/null +++ b/src/main/java/duke/commands/MarkCommand.java @@ -0,0 +1,31 @@ +package duke.commands; + +import duke.Response; +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +/** + * Marks a task as completed. + */ +public class MarkCommand extends Command { + private final int index; + + /** + * Constructor for MarkCommand. + * @param index the index specifying the task to be deleted. + */ + public MarkCommand(int index) { + this.index = index; + } + + @Override + public Response execute(TaskList taskList, Storage storage) throws NoSuchTaskException { + Task task = taskList.get(index); + String message = String.format("OK, I've marked this task as completed:\n\t%s", task); + task.markAsCompleted(); + storage.save(taskList); + return new Response(message); + } +} diff --git a/src/main/java/duke/commands/TagTaskCommand.java b/src/main/java/duke/commands/TagTaskCommand.java new file mode 100644 index 0000000000..bf06415227 --- /dev/null +++ b/src/main/java/duke/commands/TagTaskCommand.java @@ -0,0 +1,33 @@ +package duke.commands; + +import duke.Response; +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +public class TagTaskCommand extends Command { + private final int index; + private final String tag; + + /** + * Constructor for MarkCommand. + * @param index the index specifying the task to be deleted. + */ + public TagTaskCommand(int index, String tag) { + this.index = index; + this.tag = tag; + } + + @Override + public Response execute(TaskList taskList, Storage storage) throws NoSuchTaskException { + Task task = taskList.get(index); + String message = String.format("OK, I've added the tag '#%s' to this task:\n\t%s", + tag, + task); + + task.addTag(tag); + storage.save(taskList); + return new Response(message); + } +} diff --git a/src/main/java/duke/commands/UnmarkCommand.java b/src/main/java/duke/commands/UnmarkCommand.java new file mode 100644 index 0000000000..692247314e --- /dev/null +++ b/src/main/java/duke/commands/UnmarkCommand.java @@ -0,0 +1,31 @@ +package duke.commands; + +import duke.Response; +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; + +/** + * Marks a task as incomplete. + */ +public class UnmarkCommand extends Command { + private final int index; + + /** + * Constructor for UnmarkCommand. + * @param index the index specifying the task to be deleted. + */ + public UnmarkCommand(int index) { + this.index = index; + } + + @Override + public Response execute(TaskList taskList, Storage storage) throws NoSuchTaskException { + Task task = taskList.get(index); + String message = String.format("OK, I've marked this task as not done yet:\n\t%s", task); + task.markAsIncomplete(); + storage.save(taskList); + return new Response(message); + } +} diff --git a/src/main/java/duke/enums/Action.java b/src/main/java/duke/enums/Action.java new file mode 100644 index 0000000000..78cfad3692 --- /dev/null +++ b/src/main/java/duke/enums/Action.java @@ -0,0 +1,35 @@ +package duke.enums; + +import duke.exceptions.InvalidCommandException; + +/** + * A helper enumeration that specifies various keywords and their corresponding actions that are + * available to the user. + */ +public enum Action { + Todo("todo"), + Deadline("deadline"), + Event("event"), + Mark("mark"), + Unmark("unmark"), + Delete("delete"), + Find("find"), + Tag("tag"); + + public final String label; + + Action(String label) { + this.label = label; + } + + public static Action parseCommand(String command) throws InvalidCommandException { + for (Action action : values()) { + if (command.startsWith(action.label)) { + return action; + } + } + + throw new InvalidCommandException( + "Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete"); + } +} diff --git a/src/main/java/duke/exceptions/EmptyTitleException.java b/src/main/java/duke/exceptions/EmptyTitleException.java new file mode 100644 index 0000000000..b27602e348 --- /dev/null +++ b/src/main/java/duke/exceptions/EmptyTitleException.java @@ -0,0 +1,8 @@ +package duke.exceptions; + +public class EmptyTitleException extends InvalidCommandException { + public EmptyTitleException() { + super("Cannot create a task with an empty title!"); + } +} + diff --git a/src/main/java/duke/exceptions/InvalidCommandException.java b/src/main/java/duke/exceptions/InvalidCommandException.java new file mode 100644 index 0000000000..e608204bde --- /dev/null +++ b/src/main/java/duke/exceptions/InvalidCommandException.java @@ -0,0 +1,9 @@ +package duke.exceptions; + +public class InvalidCommandException extends Exception { + public InvalidCommandException(String message) { + super(message); + } +} + + diff --git a/src/main/java/duke/exceptions/InvalidDeadlineException.java b/src/main/java/duke/exceptions/InvalidDeadlineException.java new file mode 100644 index 0000000000..1c4e145004 --- /dev/null +++ b/src/main/java/duke/exceptions/InvalidDeadlineException.java @@ -0,0 +1,10 @@ +package duke.exceptions; + +public class InvalidDeadlineException extends InvalidCommandException { + public InvalidDeadlineException() { + super("Could not parse deadline. To create a deadline, " + + "please use the format in this example: " + + "deadline return book /by yyyy-MM-dd"); + } +} + diff --git a/src/main/java/duke/exceptions/InvalidEventException.java b/src/main/java/duke/exceptions/InvalidEventException.java new file mode 100644 index 0000000000..9f23988f0f --- /dev/null +++ b/src/main/java/duke/exceptions/InvalidEventException.java @@ -0,0 +1,9 @@ +package duke.exceptions; + +public class InvalidEventException extends InvalidCommandException { + public InvalidEventException() { + super("Could not parse event. To create an event, " + + "please use the format in this example: " + + "event project meeting /at Mon 2-4pm"); + } +} diff --git a/src/main/java/duke/exceptions/InvalidTaskIndexException.java b/src/main/java/duke/exceptions/InvalidTaskIndexException.java new file mode 100644 index 0000000000..34ddd29662 --- /dev/null +++ b/src/main/java/duke/exceptions/InvalidTaskIndexException.java @@ -0,0 +1,7 @@ +package duke.exceptions; + +public class InvalidTaskIndexException extends InvalidCommandException { + public InvalidTaskIndexException() { + super("Command should be followed by a number. For example: mark 2, unmark 3, delete 4."); + } +} diff --git a/src/main/java/duke/exceptions/NoSuchTaskException.java b/src/main/java/duke/exceptions/NoSuchTaskException.java new file mode 100644 index 0000000000..b707529ed9 --- /dev/null +++ b/src/main/java/duke/exceptions/NoSuchTaskException.java @@ -0,0 +1,15 @@ +package duke.exceptions; + +public class NoSuchTaskException extends InvalidCommandException { + public NoSuchTaskException(int numTasks, int index) { + super(String.format("No task found. You only have %d tasks but you referenced a task at index %d", + numTasks, + index)); + } + + public NoSuchTaskException(int numTasks, String index) { + super(String.format("No task found. You only have %d tasks but you referenced a task at index %s", + numTasks, + index)); + } +} diff --git a/src/main/java/duke/exceptions/ParsingTaskException.java b/src/main/java/duke/exceptions/ParsingTaskException.java new file mode 100644 index 0000000000..c273db97bd --- /dev/null +++ b/src/main/java/duke/exceptions/ParsingTaskException.java @@ -0,0 +1,7 @@ +package duke.exceptions; + +public class ParsingTaskException extends Exception { + public ParsingTaskException(String addtionalMessage) { + super("An error occurred parsing task data!\n" + addtionalMessage); + } +} diff --git a/src/main/java/duke/parser/Parser.java b/src/main/java/duke/parser/Parser.java new file mode 100644 index 0000000000..472525ea5a --- /dev/null +++ b/src/main/java/duke/parser/Parser.java @@ -0,0 +1,89 @@ +package duke.parser; + +import duke.commands.*; +import duke.enums.Action; +import duke.exceptions.*; +import duke.task.Deadline; +import duke.task.Event; +import duke.task.Todo; + +/** + * The interface responsible for parsing user input. + */ +public class Parser { + /** + * Parses a raw string into a command, if possible. + * @param fullCommand the string to parse + * @return the appropriate Command corresponding to the input + * @throws InvalidCommandException if the input string is not a valid command. + */ + public static Command parse(String fullCommand) throws InvalidCommandException { + if (fullCommand.trim().equals("bye")) { + return new ExitCommand(); + } else if (fullCommand.trim().equals("list")) { + return new ListTasksCommand(); + } else { + Action action = Action.parseCommand(fullCommand); + + String contents = fullCommand.substring(action.label.length()).trim(); + + try { + switch (action) { + case Mark: + return new MarkCommand(Integer.parseInt(contents) - 1); + case Unmark: + return new UnmarkCommand(Integer.parseInt(contents) - 1); + case Delete: + return new DeleteTaskCommand(Integer.parseInt(contents) - 1); + case Find: + return new FindCommand(contents); + case Tag: + String[] components = contents.split(" "); + if (components.length != 2 || !components[1].trim().startsWith("#")) { + throw new InvalidCommandException("Invalid tag command. Tag tasks with the following syntax: 'tag 1 #fun'."); + } else { + int index = Integer.parseInt(components[0]) - 1; + String tag = components[1].replaceAll("#", ""); + return new TagTaskCommand(index, tag); + } + case Todo: + if (contents.isBlank()) { + throw new EmptyTitleException(); + } else { + return new CreateTodoCommand(new Todo(contents)); + } + case Deadline: + String[] deadlineComponents = contents.split(" /by "); + + if (deadlineComponents.length != 2) { + throw new InvalidDeadlineException(); + } else if (deadlineComponents[0].isBlank()) { + throw new EmptyTitleException(); + } else { + return new CreateDeadlineCommand( + new Deadline(deadlineComponents[0].trim(), + deadlineComponents[1].trim()) + ); + } + case Event: + String[] eventComponents = contents.split(" /at "); + if (eventComponents.length != 2) { + throw new InvalidEventException(); + } else if (eventComponents[0].isBlank()) { + throw new EmptyTitleException(); + } else { + return new CreateEventCommand( + new Event(eventComponents[0].trim(), + eventComponents[1].trim()) + ); + } + default: + break; + } + } catch (NumberFormatException e) { + throw new InvalidTaskIndexException(); + } + } + throw new InvalidCommandException("Unknown command found"); + } +} diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java new file mode 100644 index 0000000000..c2192755d8 --- /dev/null +++ b/src/main/java/duke/storage/Storage.java @@ -0,0 +1,112 @@ +package duke.storage; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import duke.exceptions.ParsingTaskException; +import duke.task.*; + +/** + * The interface responsible for handling the storing and loading of user data from disk. + */ +public class Storage { + private String dataPath; + + /** + * Constructor for Storage. + * @param dataPath the file path to a .txt file to store and load data from. + */ + public Storage(String dataPath) { + this.dataPath = dataPath; + } + + private static Task parse(String data) throws ParsingTaskException { + String[] components = data.split(","); + + if (components.length == 0) { + throw new ParsingTaskException("Data was empty or not formatted properly."); + } + + String type = components[0]; + + switch (type) { + case "T": + return Todo.parse(data); + case "D": + return Deadline.parse(data); + case "E": + return Event.parse(data); + default: + throw new ParsingTaskException(String.format("duke.Task.Task was of unknown type: %s", type)); + } + } + + /** + * Loads the user's task list from disk. + * @return the user's task list. If the file does not exist, an empty one is created. + */ + public List load() { + List tasks = new ArrayList<>(); + + try { + File file = new File(this.dataPath); + + file.getParentFile().mkdirs(); + file.createNewFile(); + + Scanner scanner = new Scanner(file); + + while (scanner.hasNextLine()) { + String data = scanner.nextLine(); + try { + tasks.add(Storage.parse(data)); + } catch (ParsingTaskException e) { + System.out.println(e.getMessage()); + } + } + + scanner.close(); + + return tasks; + } catch (Exception e) { + System.out.println("An error occurred.\n" + e); + } finally { + return tasks; + } + } + + public void save(TaskList tasks) { + try { + // Create new file + String content = ""; + + for (Task t : tasks.getAll()) { + content += t.toSaveString() + "\n"; + } + + File file = new File(this.dataPath); + + // If the file doesn't exist, then create it + file.getParentFile().mkdirs(); + file.createNewFile(); + + + FileWriter fw = new FileWriter(file.getAbsoluteFile()); + BufferedWriter bw = new BufferedWriter(fw); + + // Write in file + bw.write(content); + + // Close connection + bw.close(); + + System.out.println("Saved tasks list successfully!"); + } catch (Exception e) { + System.out.println(e.getMessage() + this.dataPath); + } + } +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 0000000000..8d2b6f6710 --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,76 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import duke.exceptions.InvalidDeadlineException; +import duke.exceptions.ParsingTaskException; + +/** + * Deadlines are tasks that need to be done before a specific date/time e.g., submit report by 11/10/2019 5pm + */ +public class Deadline extends Task { + protected LocalDate by; + + public Deadline(String description, LocalDate by) { + super(description); + this.by = by; + } + + public Deadline(String description, String by) throws InvalidDeadlineException { + super(description); + try { + this.by = LocalDate.parse(by.trim()); + } catch (DateTimeParseException e) { + throw new InvalidDeadlineException(); + } + } + + public Deadline(String description, String by, boolean completed) throws InvalidDeadlineException { + super(description, completed); + try { + this.by = LocalDate.parse(by.trim()); + } catch (DateTimeParseException e) { + throw new InvalidDeadlineException(); + } + } + + public String getFormattedDate() { + return this.by.format(DateTimeFormatter.ofLocalizedDate(DATE_FORMAT)); + + } + + @Override + public String toString() { + return "[D]" + super.toString() + + " (by: " + this.getFormattedDate() + + ")"; + } + + @Override + public String toSaveString() { + return "D," + super.toSaveString() + String.format(",%s", this.by); + } + + public static Deadline parse(String data) throws ParsingTaskException { + String[] components = data.split(","); + if (components.length != 4) { + throw new ParsingTaskException(String.format("Todos require 4 components, but found %d.", + components.length)); + } + try { + boolean completed = Integer.parseInt(components[1]) == 1; + + String description = components[2]; + String by = components[3]; + + return new Deadline(description, by, completed); + } catch (NumberFormatException e) { + throw new ParsingTaskException(String.format("Expected a number at component 1, but found %s", + components[1])); + } catch (InvalidDeadlineException e) { + throw new ParsingTaskException(e.getMessage()); + } + } +} diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 0000000000..5dbab99099 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,104 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import duke.exceptions.InvalidEventException; +import duke.exceptions.ParsingTaskException; + + +/** + * Events are tasks that start at a specific time and ends at a specific time. + * For example, team project meeting on 2/10/2019 2-4pm + */ +public class Event extends Task { + + /** + * The time of the event. + */ + protected LocalDate at; + + /** + * Constructor for an Event. + * @param description the event's description + * @param at the date of the event + */ + public Event(String description, LocalDate at) { + super(description); + this.at = at; + } + + /** + * Constructor for an Event. + * @param description the event's description + * @param at a String representing the event's date + * @throws InvalidEventException if the time string cannot be parsed correctly + */ + public Event(String description, String at) throws InvalidEventException { + super(description); + try { + this.at = LocalDate.parse(at.trim()); + } catch (DateTimeParseException e) { + throw new InvalidEventException(); + } + } + + /** + * Constructor for an Event. + * @param description the event's description + * @param at a String representing the event's date + * @param completed the completion state of the event + * @throws InvalidEventException if the time string cannot be parsed correctly + */ + public Event(String description, String at, boolean completed) + throws InvalidEventException { + super(description, completed); + try { + this.at = LocalDate.parse(at.trim()); + } catch (DateTimeParseException e) { + throw new InvalidEventException(); + } + } + + /** + * Gets the event's date, in a reader-friendly format + * @return the formatted date string + */ + public String getFormattedDate() { + return this.at.format(DateTimeFormatter.ofLocalizedDate(Task.DATE_FORMAT)); + } + + @Override + public String toString() { + return "[E]" + super.toString() + + " (at: " + this.getFormattedDate() + + ")"; + } + + @Override + public String toSaveString() { + return "E," + super.toSaveString() + String.format(",%s", this.at); + } + + public static Event parse(String data) throws ParsingTaskException { + String[] components = data.split(","); + if (components.length != 4) { + throw new ParsingTaskException(String.format("Events require 4 components, but only found %d.", + components.length)); + } + try { + boolean completed = Integer.parseInt(components[1]) == 1; + + String description = components[2]; + String at = components[3]; + + return new Event(description, at, completed); + } catch (NumberFormatException e) { + throw new ParsingTaskException(String.format("Expected a number at component 1, but found %s", + components[1])); + } catch (InvalidEventException e) { + throw new ParsingTaskException(e.getMessage()); + } + } +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 0000000000..735b21556d --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,69 @@ +package duke.task; + +import java.io.File; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.List; + +/** + * An object representing a task in a checklist. This is the application's primary class. + */ +public class Task { + public static final FormatStyle DATE_FORMAT = FormatStyle.MEDIUM; + private static final String DATA_PATH = new File("").getAbsolutePath() + "/data/duke.txt"; + private String title; + private boolean completed; + + private List tags = new ArrayList<>(); + + Task(String title) { + assert !title.isBlank() : "Task title cannot be empty"; + + this.title = title; + this.completed = false; + } + + Task(String title, boolean completed) { + assert !title.isBlank() : "Task title cannot be empty"; + + this.title = title; + this.completed = completed; + } + + public String getTitle() { + return this.title; + } + + public boolean isCompleted() { + return this.completed; + } + + public void markAsCompleted() { + this.completed = true; + } + + public void markAsIncomplete() { + this.completed = false; + } + + public List getTags() { + return tags; + } + + public final void addTag(String tag) { + this.tags.add(tag); + } + + private String getStatusIcon() { + return (this.completed ? "X" : " "); // mark done task with X + } + + @Override + public String toString() { + return String.format("[%s] %s", this.getStatusIcon(), this.title); + } + + public String toSaveString() { + return String.format("%d,%s", this.completed ? 1 : 0, this.title); + } +} diff --git a/src/main/java/duke/task/TaskList.java b/src/main/java/duke/task/TaskList.java new file mode 100644 index 0000000000..9a1ad4666a --- /dev/null +++ b/src/main/java/duke/task/TaskList.java @@ -0,0 +1,45 @@ +package duke.task; + +import java.util.List; + +import duke.exceptions.NoSuchTaskException; + +public class TaskList { + private List tasks; + + public TaskList(List tasks) { + assert tasks != null : "Task list cannot be null"; + + this.tasks = tasks; + } + + public List getAll() { + return this.tasks; + } + + public int getNumTasks() { + return this.tasks.size(); + } + + private String getNumTasksAsString() { + return String.format("Now you have %d tasks in the list.", this.getNumTasks()); + } + + public void store(Task task) { + this.tasks.add(task); + } + + public Task get(int index) throws NoSuchTaskException { + try { + return this.tasks.get(index); + } catch (IndexOutOfBoundsException e) { + throw new NoSuchTaskException(this.getNumTasks(), index + 1); + } + } + + public Task delete(int index) throws NoSuchTaskException { + Task task = this.get(index); + this.tasks.remove(index); + return task; + } +} diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java new file mode 100644 index 0000000000..445b499d51 --- /dev/null +++ b/src/main/java/duke/task/Todo.java @@ -0,0 +1,42 @@ +package duke.task; +import duke.exceptions.ParsingTaskException; + +/** + * Todos are tasks without any date/time attached to it e.g., visit new theme park + */ +public class Todo extends Task { + public Todo(String title) { + super(title); + } + + public Todo(String title, boolean completed) { + super(title, completed); + } + + @Override + public String toString() { + return "[T]" + super.toString(); + } + + @Override + public String toSaveString() { + return "T," + super.toSaveString(); + } + + public static Todo parse(String data) throws ParsingTaskException { + String[] components = data.split(","); + if (components.length != 3) { + throw new ParsingTaskException(String.format("Todos require 3 components, but found %d.", + components.length)); + } + try { + boolean completed = Integer.parseInt(components[1]) == 1; + String title = components[2]; + + return new Todo(title, completed); + } catch (NumberFormatException e) { + throw new ParsingTaskException(String.format("Expected a number at component 1, but found %s", + components[1])); + } + } +} diff --git a/src/main/java/duke/ui/DialogBox.java b/src/main/java/duke/ui/DialogBox.java new file mode 100644 index 0000000000..d9b0761d80 --- /dev/null +++ b/src/main/java/duke/ui/DialogBox.java @@ -0,0 +1,46 @@ +package duke.ui; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; + +/** + * The UI component that is responsible for displaying outputs to the user. + */ +public class DialogBox extends HBox { + + /** + * Constructs a component to display output, containing an image and text. + * + * @param label label containing text + */ + public DialogBox(Label label) { + label.setWrapText(true); + + this.setAlignment(Pos.TOP_RIGHT); + this.getChildren().addAll(label); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + this.setAlignment(Pos.TOP_LEFT); + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + FXCollections.reverse(tmp); + this.getChildren().setAll(tmp); + } + + public static DialogBox getUserDialog(Label label) { + return new DialogBox(label); + } + + public static DialogBox getDukeDialog(Label label) { + var db = new DialogBox(label); + db.flip(); + return db; + } +} diff --git a/src/main/java/duke/ui/TextUi.java b/src/main/java/duke/ui/TextUi.java new file mode 100644 index 0000000000..dfbf7e4065 --- /dev/null +++ b/src/main/java/duke/ui/TextUi.java @@ -0,0 +1,49 @@ +package duke.ui; + +import java.io.PrintStream; +import java.util.Scanner; + +/** + * The interface responsible for handling the app's interaction with the user. + */ +public class TextUi { + public static final String WELCOME_MESSAGE = "Hello! I'm Duke\n" + "What can I do for you?"; + public static final String GOODBYE_MESSAGE = "Bye. Hope to see you again soon!"; + private static final String PREFIX = "duke >> "; + + private static final PrintStream out = System.out; + private final Scanner in = new Scanner(System.in); + + /** + * Gets the command inputted by the user and returns it. + */ + public String getUserCommand() { + out.print("Enter command: "); + String inputLine = in.nextLine(); + out.println(PREFIX + inputLine); + + return inputLine; + } + + /** + * Welcomes the user. + */ + public void showWelcomeMessage() { + System.out.println(WELCOME_MESSAGE); + } + + /** + * Bids the user goodbye. This should only be shown on the ExitCommand. + */ + public void showGoodByeMessage() { + System.out.println(GOODBYE_MESSAGE); + } + + /** + * Displays a message to the user. + * @param message the message to be displayed + */ + public void showMessage(String message) { + System.out.println(message); + } +} diff --git a/src/main/java/duke/ui/Ui.java b/src/main/java/duke/ui/Ui.java new file mode 100644 index 0000000000..00088cd6f9 --- /dev/null +++ b/src/main/java/duke/ui/Ui.java @@ -0,0 +1,31 @@ +package duke.ui; + +/** + * The interface responsible for handling the app's interaction with the user. + */ +public class Ui { + private static final String WELCOME_MESSAGE = "Hello! I'm Duke\n" + "What can I do for you?"; + private static final String GOODBYE_MESSAGE = "Bye. Hope to see you again soon!"; + + /** + * Welcomes the user. + */ + public void showWelcomeMessage() { + System.out.println(WELCOME_MESSAGE); + } + + /** + * Bids the user goodbye. This should only be shown on the ExitCommand. + */ + public void showGoodByeMessage() { + System.out.println(GOODBYE_MESSAGE); + } + + /** + * Displays a message to the user. + * @param message the message to be displayed + */ + public void showMessage(String message) { + System.out.println(message); + } +} diff --git a/src/test/java/duke/DukeTest.java b/src/test/java/duke/DukeTest.java new file mode 100644 index 0000000000..eb8bfa747e --- /dev/null +++ b/src/test/java/duke/DukeTest.java @@ -0,0 +1,15 @@ +package duke; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DukeTest { + @Test + public void dummyTest(){ + assertEquals(2, 2); + } + + @Test + public void anotherDummyTest(){ + assertEquals(4, 4); + } +} diff --git a/src/test/java/duke/commands/CreateDeadlineCommandTest.java b/src/test/java/duke/commands/CreateDeadlineCommandTest.java new file mode 100644 index 0000000000..cf725b8bb0 --- /dev/null +++ b/src/test/java/duke/commands/CreateDeadlineCommandTest.java @@ -0,0 +1,37 @@ +package duke.commands; + +import duke.storage.Storage; +import duke.task.Deadline; +import duke.task.Task; +import duke.task.TaskList; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class CreateDeadlineCommandTest { + + @Test + public void execution_addsSingleItem() { + TaskList taskList = new TaskList(new ArrayList()); + Deadline deadline = new Deadline("Test", LocalDate.now()); + CreateDeadlineCommand command = new CreateDeadlineCommand(deadline); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.getNumTasks(), 1); + } + + @Test + public void execution_addsCreatedDeadline() { + TaskList taskList = new TaskList(new ArrayList()); + Deadline deadline = new Deadline("Test", LocalDate.now()); + CreateDeadlineCommand command = new CreateDeadlineCommand(deadline); + + command.execute(taskList, new Storage("")); + + assertDoesNotThrow(() -> assertEquals(taskList.get(0), deadline)); + } +} diff --git a/src/test/java/duke/commands/CreateEventCommandTest.java b/src/test/java/duke/commands/CreateEventCommandTest.java new file mode 100644 index 0000000000..07395f1cd0 --- /dev/null +++ b/src/test/java/duke/commands/CreateEventCommandTest.java @@ -0,0 +1,37 @@ +package duke.commands; + +import duke.storage.Storage; +import duke.task.Event; +import duke.task.Task; +import duke.task.TaskList; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class CreateEventCommandTest { + + @Test + public void execution_addsSingleItem() { + TaskList taskList = new TaskList(new ArrayList()); + Event event = new Event("Test", LocalDate.now()); + CreateEventCommand command = new CreateEventCommand(event); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.getNumTasks(), 1); + } + + @Test + public void execution_addsCreatedEvent() { + TaskList taskList = new TaskList(new ArrayList()); + Event event = new Event("Test", LocalDate.now()); + CreateEventCommand command = new CreateEventCommand(event); + + command.execute(taskList, new Storage("")); + + assertDoesNotThrow(() -> assertEquals(taskList.get(0), event)); + } +} diff --git a/src/test/java/duke/commands/CreateTodoCommandTest.java b/src/test/java/duke/commands/CreateTodoCommandTest.java new file mode 100644 index 0000000000..894da1fb62 --- /dev/null +++ b/src/test/java/duke/commands/CreateTodoCommandTest.java @@ -0,0 +1,36 @@ +package duke.commands; + +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; +import duke.task.Todo; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class CreateTodoCommandTest { + + @Test + public void execution_addsSingleItem() { + TaskList taskList = new TaskList(new ArrayList()); + Todo todo = new Todo("Test"); + CreateTodoCommand command = new CreateTodoCommand(todo); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.getNumTasks(), 1); + } + + @Test + public void execution_addsCreatedEvent() { + TaskList taskList = new TaskList(new ArrayList()); + Todo todo = new Todo("Test"); + CreateTodoCommand command = new CreateTodoCommand(todo); + + command.execute(taskList, new Storage("")); + + assertDoesNotThrow(() -> assertEquals(taskList.get(0), todo)); + } +} diff --git a/src/test/java/duke/commands/DeleteTaskCommandTest.java b/src/test/java/duke/commands/DeleteTaskCommandTest.java new file mode 100644 index 0000000000..f19e7a7435 --- /dev/null +++ b/src/test/java/duke/commands/DeleteTaskCommandTest.java @@ -0,0 +1,46 @@ +package duke.commands; + +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.task.Todo; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class DeleteTaskCommandTest { + private static TaskList getTaskList() { + return new TaskList(new ArrayList<>() {{ + add(new Todo("Test")); + }}); + } + + @Test + public void execution_invalidIndex_exceptionThrown() { + + DeleteTaskCommand command = new DeleteTaskCommand(1); + + assertThrows(NoSuchTaskException.class, + () -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_exceptionNotThrown() { + DeleteTaskCommand command = new DeleteTaskCommand(0); + + assertDoesNotThrow(() -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_singleTaskRemoved() throws NoSuchTaskException { + + DeleteTaskCommand command = new DeleteTaskCommand(0); + TaskList taskList = getTaskList(); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.getNumTasks(), 0); + } +} diff --git a/src/test/java/duke/commands/ExitCommandTest.java b/src/test/java/duke/commands/ExitCommandTest.java new file mode 100644 index 0000000000..f23c1b2a68 --- /dev/null +++ b/src/test/java/duke/commands/ExitCommandTest.java @@ -0,0 +1,14 @@ +package duke.commands; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExitCommandTest { + + @Test + public void isExit_returnsTrue() { + ExitCommand command = new ExitCommand(); + assertEquals(command.isExit(), true); + } +} diff --git a/src/test/java/duke/commands/MarkCommandTest.java b/src/test/java/duke/commands/MarkCommandTest.java new file mode 100644 index 0000000000..75ad0e606f --- /dev/null +++ b/src/test/java/duke/commands/MarkCommandTest.java @@ -0,0 +1,48 @@ +package duke.commands; + +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.task.Todo; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class MarkCommandTest { + private static TaskList getTaskList() { + return new TaskList(new ArrayList<>() {{ + add(new Todo("Test", false)); + }}); + } + + @Test + public void execution_invalidIndex_exceptionThrown() { + + MarkCommand command = new MarkCommand(1); + + assertThrows(NoSuchTaskException.class, + () -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_exceptionNotThrown() { + MarkCommand command = new MarkCommand(0); + + assertDoesNotThrow(() -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_singleTaskRemoved() throws NoSuchTaskException { + + MarkCommand command = new MarkCommand(0); + TaskList taskList = getTaskList(); + + assertEquals(taskList.get(0).isCompleted(), false); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.get(0).isCompleted(), true); + } +} diff --git a/src/test/java/duke/commands/TagTaskCommandTest.java b/src/test/java/duke/commands/TagTaskCommandTest.java new file mode 100644 index 0000000000..3b331a1192 --- /dev/null +++ b/src/test/java/duke/commands/TagTaskCommandTest.java @@ -0,0 +1,48 @@ +package duke.commands; + +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.task.Todo; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class TagTaskCommandTest { + private static TaskList getTaskList() { + return new TaskList(new ArrayList<>() {{ + add(new Todo("Test", false)); + }}); + } + + @Test + public void execution_invalidIndex_exceptionThrown() { + + TagTaskCommand command = new TagTaskCommand(1, "IGNORE_ME"); + + assertThrows(NoSuchTaskException.class, + () -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_exceptionNotThrown() { + TagTaskCommand command = new TagTaskCommand(0, "IGNORE_ME"); + + assertDoesNotThrow(() -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_singleTaskRemoved() throws NoSuchTaskException { + String tag = "fun"; + TagTaskCommand command = new TagTaskCommand(0, tag); + TaskList taskList = getTaskList(); + + assertEquals(taskList.get(0).getTags().contains(tag), false); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.get(0).getTags().contains(tag), true); + } +} diff --git a/src/test/java/duke/commands/UnmarkCommandTest.java b/src/test/java/duke/commands/UnmarkCommandTest.java new file mode 100644 index 0000000000..cf044375b4 --- /dev/null +++ b/src/test/java/duke/commands/UnmarkCommandTest.java @@ -0,0 +1,48 @@ +package duke.commands; + +import duke.exceptions.NoSuchTaskException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.task.Todo; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class UnmarkCommandTest { + private static TaskList getTaskList() { + return new TaskList(new ArrayList<>() {{ + add(new Todo("Test", true)); + }}); + } + + @Test + public void execution_invalidIndex_exceptionThrown() { + + UnmarkCommand command = new UnmarkCommand(1); + + assertThrows(NoSuchTaskException.class, + () -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_exceptionNotThrown() { + UnmarkCommand command = new UnmarkCommand(0); + + assertDoesNotThrow(() -> command.execute(getTaskList(), new Storage(""))); + } + + @Test + public void execution_validIndex_singleTaskRemoved() throws NoSuchTaskException { + + UnmarkCommand command = new UnmarkCommand(0); + TaskList taskList = getTaskList(); + + assertEquals(taskList.get(0).isCompleted(), true); + + command.execute(taskList, new Storage("")); + + assertEquals(taskList.get(0).isCompleted(), false); + } +} diff --git a/src/test/java/duke/task/DeadlineTest.java b/src/test/java/duke/task/DeadlineTest.java new file mode 100644 index 0000000000..635b0183cb --- /dev/null +++ b/src/test/java/duke/task/DeadlineTest.java @@ -0,0 +1,41 @@ +package duke.task; +import duke.exceptions.InvalidDeadlineException; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +public class DeadlineTest { + + @Test + public void constructor_invalidDate_exceptionThrown() { + assertThrows(InvalidDeadlineException.class, + () -> new Deadline("Test task", "09-17-2022", false)); + + assertThrows(InvalidDeadlineException.class, + () -> new Deadline("Test task", "blah blah", false)); + } + + @Test + public void constructor_validDate_exceptionNotThrown(){ + assertDoesNotThrow(() -> new Deadline("Test task", "2022-09-17", false)); + assertDoesNotThrow(() -> new Deadline("Test task", " 2022-09-17 ", false)); + } + + @Test + public void constructor_validDate_deadlineIsCorrect(){ + assertDoesNotThrow(() -> { + Deadline deadline = new Deadline("Test task", "2022-09-17", false); + assertEquals(deadline.by, LocalDate.of(2022, 9, 17)); + }); + } + + @Test + public void constructor_validDateWithWhitespaces_deadlineIsCorrect(){ + assertDoesNotThrow(() -> { + Deadline deadline = new Deadline("Test task", " 2022-09-17 \n ", false); + assertEquals(deadline.by, LocalDate.of(2022, 9, 17)); + }); + } +} diff --git a/src/test/java/duke/task/EventTest.java b/src/test/java/duke/task/EventTest.java new file mode 100644 index 0000000000..9c4fbe36a9 --- /dev/null +++ b/src/test/java/duke/task/EventTest.java @@ -0,0 +1,41 @@ +package duke.task; +import duke.exceptions.InvalidEventException; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +public class EventTest { + + @Test + public void constructor_invalidDate_exceptionThrown() { + assertThrows(InvalidEventException.class, + () -> new Event("Test task", "09-17-2022", false)); + + assertThrows(InvalidEventException.class, + () -> new Event("Test task", "blah blah", false)); + } + + @Test + public void constructor_validDate_exceptionNotThrown(){ + assertDoesNotThrow(() -> new Event("Test task", "2022-09-17", false)); + assertDoesNotThrow(() -> new Event("Test task", " 2022-09-17 ", false)); + } + + @Test + public void constructor_validDate_deadlineIsCorrect(){ + assertDoesNotThrow(() -> { + Event event = new Event("Test task", "2022-09-17", false); + assertEquals(event.at, LocalDate.of(2022, 9, 17)); + }); + } + + @Test + public void constructor_validDateWithWhitespaces_deadlineIsCorrect(){ + assertDoesNotThrow(() -> { + Event event = new Event("Test task", " 2022-09-17 \n ", false); + assertEquals(event.at, LocalDate.of(2022, 9, 17)); + }); + } +} diff --git a/src/test/java/duke/task/TaskListTest.java b/src/test/java/duke/task/TaskListTest.java new file mode 100644 index 0000000000..d2690e5ef7 --- /dev/null +++ b/src/test/java/duke/task/TaskListTest.java @@ -0,0 +1,75 @@ +package duke.task; + +import duke.exceptions.NoSuchTaskException; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class TaskListTest { + private static TaskList getTaskListWithSingleItem() { + return new TaskList(new ArrayList<>() {{ + add(new Todo("Test", false)); + }}); + } + + @Test + public void store_insertsSingleItem() { + TaskList taskList = getTaskListWithSingleItem(); + int prevNumTasks = taskList.getNumTasks(); + + Task task = new Task("Test task"); + taskList.store(task); + + assertEquals(taskList.getNumTasks(), prevNumTasks + 1); + } + + @Test + public void store_insertsCorrectItem() throws NoSuchTaskException { + TaskList taskList = getTaskListWithSingleItem(); + int prevNumTasks = taskList.getNumTasks(); + + Task task = new Task("Test task"); + taskList.store(task); + + assertEquals(taskList.get(prevNumTasks), task); + } + + @Test + public void delete_validIndex_exceptionNotThrown() { + TaskList taskList = getTaskListWithSingleItem(); + + assertDoesNotThrow(() -> taskList.delete(0)); + } + + @Test + public void delete_invalidIndex_exceptionThrown() { + TaskList taskList = getTaskListWithSingleItem(); + + assertThrows(NoSuchTaskException.class, () -> taskList.delete(1)); + } + + @Test + public void delete_validIndex_removesSingleItem() throws NoSuchTaskException { + TaskList taskList = getTaskListWithSingleItem(); + int prevNumTasks = taskList.getNumTasks(); + + taskList.delete(0); + + assertEquals(taskList.getNumTasks(), prevNumTasks - 1); + } + + @Test + public void delete_removesCorrectItem() throws NoSuchTaskException { + TaskList taskList = getTaskListWithSingleItem(); + int prevNumTasks = taskList.getNumTasks(); + + Task task = new Task("Another task"); + taskList.store(task); + + taskList.delete(1); + + assertEquals(taskList.getAll().stream().filter(e -> e == task).toArray().length, 0); + } +} diff --git a/src/test/java/duke/task/TaskTest.java b/src/test/java/duke/task/TaskTest.java new file mode 100644 index 0000000000..e36a87005e --- /dev/null +++ b/src/test/java/duke/task/TaskTest.java @@ -0,0 +1,35 @@ +package duke.task; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TaskTest { + + @Test + public void markAsCompleted_incompleteTask_isCompletedReturnsTrue(){ + Task task = new Task("Test task", false); + task.markAsCompleted(); + assertEquals(task.isCompleted(), true); + } + + @Test + public void markAsCompleted_completeTask_isCompletedReturnsTrue(){ + Task task = new Task("Test task", true); + task.markAsCompleted(); + assertEquals(task.isCompleted(), true); + } + + @Test + public void markAsIncomplete_incompleteTask_isCompletedReturnsFalse(){ + Task task = new Task("Test task", false); + task.markAsIncomplete(); + assertEquals(task.isCompleted(), false); + } + + @Test + public void markAsInomplete_completeTask_isCompletedReturnsFalse(){ + Task task = new Task("Test task", true); + task.markAsIncomplete(); + assertEquals(task.isCompleted(), false); + } +} diff --git a/text-ui-test/ACTUAL.TXT b/text-ui-test/ACTUAL.TXT new file mode 100644 index 0000000000..6e34fd5dbe --- /dev/null +++ b/text-ui-test/ACTUAL.TXT @@ -0,0 +1,74 @@ +Hello! I'm duke.Duke +What can I do for you? +Got it. I've added this task: + [T][ ] read book + +Got it. I've added this task: + [T][ ] borrow book + +Got it. I've added this task: + [T][ ] return book + +Invalid command given. +Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete +Here are the tasks in your list: +1. [T][ ] read book +2. [T][ ] borrow book +3. [T][ ] return book +Got it. I've added this task: + [D][ ] return book (by: Sunday) + +Got it. I've added this task: + [E][ ] project meeting (at: Mon 2-4pm) + +Here are the tasks in your list: +1. [T][ ] read book +2. [T][ ] borrow book +3. [T][ ] return book +4. [D][ ] return book (by: Sunday) +5. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +Cannot create a task with an empty title! +Invalid command given. +Could not parse deadline. To create a deadline, please use the format in this example: deadline return book /by Sunday +Invalid command given. +Could not parse event. To create an event, please use the format in this example: event project meeting /at Mon 2-4pm +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Noted. I've removed this task: + [T][ ] read book + +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][ ] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +No task found. You only have 4 tasks but you referenced a task at index 100 +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Nice! I've marked this task as done: + [T][X] return book +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][X] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +No task found. You only have 4 tasks but you referenced a task at index 100 +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +OK, I've marked this task as not done yet: + [T][ ] return book +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][ ] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete +Bye. Hope to see you again soon! diff --git a/text-ui-test/EXPECTED-UNIX.TXT b/text-ui-test/EXPECTED-UNIX.TXT new file mode 100644 index 0000000000..6e34fd5dbe --- /dev/null +++ b/text-ui-test/EXPECTED-UNIX.TXT @@ -0,0 +1,74 @@ +Hello! I'm duke.Duke +What can I do for you? +Got it. I've added this task: + [T][ ] read book + +Got it. I've added this task: + [T][ ] borrow book + +Got it. I've added this task: + [T][ ] return book + +Invalid command given. +Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete +Here are the tasks in your list: +1. [T][ ] read book +2. [T][ ] borrow book +3. [T][ ] return book +Got it. I've added this task: + [D][ ] return book (by: Sunday) + +Got it. I've added this task: + [E][ ] project meeting (at: Mon 2-4pm) + +Here are the tasks in your list: +1. [T][ ] read book +2. [T][ ] borrow book +3. [T][ ] return book +4. [D][ ] return book (by: Sunday) +5. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +Cannot create a task with an empty title! +Invalid command given. +Could not parse deadline. To create a deadline, please use the format in this example: deadline return book /by Sunday +Invalid command given. +Could not parse event. To create an event, please use the format in this example: event project meeting /at Mon 2-4pm +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Noted. I've removed this task: + [T][ ] read book + +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][ ] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +No task found. You only have 4 tasks but you referenced a task at index 100 +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Nice! I've marked this task as done: + [T][X] return book +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][X] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +No task found. You only have 4 tasks but you referenced a task at index 100 +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +OK, I've marked this task as not done yet: + [T][ ] return book +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][ ] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete +Bye. Hope to see you again soon! diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 657e74f6e7..6e34fd5dbe 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,7 +1,74 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| +Hello! I'm duke.Duke +What can I do for you? +Got it. I've added this task: + [T][ ] read book +Got it. I've added this task: + [T][ ] borrow book + +Got it. I've added this task: + [T][ ] return book + +Invalid command given. +Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete +Here are the tasks in your list: +1. [T][ ] read book +2. [T][ ] borrow book +3. [T][ ] return book +Got it. I've added this task: + [D][ ] return book (by: Sunday) + +Got it. I've added this task: + [E][ ] project meeting (at: Mon 2-4pm) + +Here are the tasks in your list: +1. [T][ ] read book +2. [T][ ] borrow book +3. [T][ ] return book +4. [D][ ] return book (by: Sunday) +5. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +Cannot create a task with an empty title! +Invalid command given. +Could not parse deadline. To create a deadline, please use the format in this example: deadline return book /by Sunday +Invalid command given. +Could not parse event. To create an event, please use the format in this example: event project meeting /at Mon 2-4pm +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Noted. I've removed this task: + [T][ ] read book + +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][ ] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +No task found. You only have 4 tasks but you referenced a task at index 100 +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +Nice! I've marked this task as done: + [T][X] return book +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][X] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +No task found. You only have 4 tasks but you referenced a task at index 100 +Invalid command given. +Command should be followed by a number. For example: mark 2, unmark 3, delete 4. +OK, I've marked this task as not done yet: + [T][ ] return book +Here are the tasks in your list: +1. [T][ ] borrow book +2. [T][ ] return book +3. [D][ ] return book (by: Sunday) +4. [E][ ] project meeting (at: Mon 2-4pm) +Invalid command given. +Could not determine the command. Valid commands include: todo, deadline, event, mark, unmark, delete +Bye. Hope to see you again soon! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29bb2..7a00305ede 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1,26 @@ +todo read book +todo borrow book +todo return book +list blah blah +list +deadline return book /by Sunday +event project meeting /at Mon 2-4pm +list +todo +deadline /bys Sunday +event /atMon 2-4pm +delete +delete one +delete 1 +list +mark 100 +mark +mark one +mark 2 +list +unmark 100 +unmark one +unmark 2 +list +byeee +bye diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 0873744649..62752b8814 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -15,7 +15,7 @@ IF ERRORLEVEL 1 ( REM no error here, errorlevel == 0 REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT -java -classpath ..\bin Duke < input.txt > ACTUAL.TXT +java -classpath ..\bin duke.Duke < input.txt > ACTUAL.TXT REM compare the output to the expected output FC ACTUAL.TXT EXPECTED.TXT