diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..acfb0fd44b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.checkstyle.configuration": "${workspaceFolder}/config/checkstyle/checkstyle.xml", + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..a72fc1a8bf --- /dev/null +++ b/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '7.1.2' +} + +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' +} + +checkstyle { + toolVersion = '10.2' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("opus.Launcher") +} + +shadowJar { + archiveBaseName = "opus" + archiveClassifier = null +} + +run{ + standardInput = System.in +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..52a992d152 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/tasks.txt b/data/tasks.txt new file mode 100644 index 0000000000..b046a1907e --- /dev/null +++ b/data/tasks.txt @@ -0,0 +1,4 @@ +D|0|return book|Sunday +E|1|project meeting|Mon 2pm|4pm +T|1|submit report +T|0|return book diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..b3299d4e85 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,100 @@ -# Duke User Guide +# Opus User Guide -// Update the title above to match the actual product name +This is the Userguide for Opus -// Product screenshot goes here +## Features Summary -// Product intro goes here +- **Adds Tasks:** Automatically adds tasks to a file. +- **Mark Tasks as Done:** You can mark tasks as completed. +- **Delete Tasks:** Allows you to remove tasks from the list. +- **List Tasks:** Displays all tasks in your list with their current status. -## Adding deadlines +## Task Types -// Describe the action and its outcome. +Opus supports three types of tasks: +- **Deadlines:** Tasks that need to be completed by a specific date. +- **Events:** Tasks that span a specific time period. +- **ToDos:** Simple tasks without deadlines or timeframes. -// Give examples of usage +## Using Opus -Example: `keyword (optional arguments)` -// A description of the expected outcome goes here +### Feature 1: Adding Tasks +To add a Task to the List, you have to specify what task you are adding. The task could be a Deadline, an Event, or a ToDo. + +#### Adding a Deadline + +###### Format of input : `deadline /by ` +###### Example Input: `deadline return book /by Sunday` + +###### Expected output: +``` +Got it. I've added this task: + [D][ ] return book (by: Sunday) +Now you have 1 task in the list. +``` + +#### Adding an Event + +###### Format of input : `event /from /to ` +###### Example input: `event project meeting /from Mon 2pm /to 4pm` + +###### Expected output: +``` +Got it. I've added this task: + [E][ ] project meeting (from: Mon 2pm to: 4pm) + Now you have 2 tasks in the list. +``` +#### Adding a ToDo + +###### Format of input : `todo ` +###### Example input: `todo submit report` + +###### Expected output: ``` -expected output +Got it. I've added this task: + [T][ ] submit report + Now you have 3 tasks in the list. ``` -## Feature ABC -// Feature details +### Feature 2: Marking Tasks as Done +Use the `mark` command to mark a task as completed. +###### Format of input : `mark ` +###### Example : `mark 1` +###### Expected output: +``` +Nice! I've marked this task as done: [D][X] return book (by: Sunday) +``` + +### Feature 3: Deleting Tasks + +Use the `delete` command to delete a Task. +###### Format of input : `delete ` + +###### Example : `delete 1` +###### Expected output: +``` +Noted. I've removed this task: +[D][X] return book (by: Sunday) +Now you have 2 tasks in the list. +``` + + +### Feature 4: Listing Tasks -## Feature XYZ +Use the `list` command to list all Tasks. +###### Format of input : `list` + +###### Example : `list` +###### Expected output: +``` +[E][ ] project meeting (from: Mon 2pm to: 4pm) +[T][ ] submit report +``` -// Feature details \ No newline at end of file +## Command to run to start the UI +Run the following command to start the UI +`./gradlew run` diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..2ea8b6b340 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/javafx.scene.control.Label b/javafx.scene.control.Label new file mode 100644 index 0000000000..e69de29bb2 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/opus/DialogBox.java b/src/main/java/opus/DialogBox.java new file mode 100644 index 0000000000..2bbdd4b3e3 --- /dev/null +++ b/src/main/java/opus/DialogBox.java @@ -0,0 +1,59 @@ +package opus; + +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; + + +/** + * 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 { + + private Label text; + private ImageView displayPicture; + + /** + * Constructs a dialog box with the specified text and image. + * + * @param s The text to be displayed. + * @param i The image to be displayed. + */ + + public DialogBox(String s, Image i) { + text = new Label(s); + displayPicture = new ImageView(i); + text.setWrapText(true); + displayPicture.setFitWidth(100.0); + displayPicture.setFitHeight(100.0); + this.setAlignment(Pos.TOP_RIGHT); + this.getChildren().addAll(text, displayPicture); + } + + /** + * Flips the dialog box such that the ImageView is on the right and text is on the left. + */ + + 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(String s, Image i) { + return new DialogBox(s, i); + } + + public static DialogBox getOpusDialog(String s, Image i) { + var db = new DialogBox(s, i); + db.flip(); + return db; + } +} diff --git a/src/main/java/opus/Launcher.java b/src/main/java/opus/Launcher.java new file mode 100644 index 0000000000..b36248a2ff --- /dev/null +++ b/src/main/java/opus/Launcher.java @@ -0,0 +1,11 @@ +package opus; +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/opus/Main.java b/src/main/java/opus/Main.java new file mode 100644 index 0000000000..8792738256 --- /dev/null +++ b/src/main/java/opus/Main.java @@ -0,0 +1,113 @@ +package opus; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.scene.layout.Region; +import javafx.stage.Stage; +import javafx.scene.image.Image; +import opus.commands.CommandResult; + + + +/** + * Represents the main class of the application. The main class is responsible for + * setting up the user interface and handling user input. + */ +public class Main extends Application { + + private static final String DEFAULT_FILE_PATH = "data/tasks.txt"; + private Image userImage = new Image(this.getClass().getResourceAsStream("/user.png")); + private Image opusImage = new Image(this.getClass().getResourceAsStream("/opus.png")); + private Opus opus = new Opus(DEFAULT_FILE_PATH); + + private ScrollPane scrollPane; + private VBox dialogContainer; + private TextField userInput; + private Button sendButton; + private Scene scene; + + public Main() { + this(DEFAULT_FILE_PATH); + } + + public Main(String defaultFilePath) { + } + + @Override + public void start(Stage stage) { + scrollPane = new ScrollPane(); + dialogContainer = new VBox(); + scrollPane.setContent(dialogContainer); + + userInput = new TextField(); + sendButton = new Button("Send"); + DialogBox dialogBox = new DialogBox("Hello!", userImage); + dialogContainer.getChildren().addAll(dialogBox); + + AnchorPane mainLayout = new AnchorPane(); + mainLayout.getChildren().addAll(scrollPane, userInput, sendButton); + stage.setTitle("Opus"); + 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); + + sendButton.setPrefWidth(55.0); + + AnchorPane.setTopAnchor(scrollPane, 1.0); + + AnchorPane.setBottomAnchor(sendButton, 1.0); + AnchorPane.setRightAnchor(sendButton, 1.0); + + AnchorPane.setLeftAnchor(userInput , 1.0); + AnchorPane.setBottomAnchor(userInput, 1.0); + + + scene = new Scene(mainLayout); + + stage.setScene(scene); + stage.show(); + + sendButton.setOnMouseClicked((event) -> { + handleUserInput(); + }); + userInput.setOnAction((event) -> { + handleUserInput(); + }); + + dialogContainer.heightProperty().addListener((observable) -> scrollPane.setVvalue(1.0)); + + } + + private void handleUserInput() { + String userText = userInput.getText(); + CommandResult commandResult = opus.getResponse(userText); + String opusText = commandResult.getFeedbackToUser(); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(userText, userImage), + DialogBox.getOpusDialog(opusText, opusImage) + ); + userInput.clear(); + + if (commandResult.isExit()) { + Platform.exit(); + } + } +} diff --git a/src/main/java/opus/Opus.java b/src/main/java/opus/Opus.java new file mode 100644 index 0000000000..8467110de7 --- /dev/null +++ b/src/main/java/opus/Opus.java @@ -0,0 +1,41 @@ +package opus; + +import opus.exceptions.OpusException; +import opus.commands.Command; +import opus.commands.CommandResult; + +/** + * Represents the main class of the program. + */ +public class Opus { + + private Storage storage; + private TaskList taskList; + + /** + * Creates a new instance of Opus. + * + * @param filePath The file path of the storage file. + */ + public Opus(String filePath) { + storage = new Storage(filePath); + taskList = new TaskList(storage.load()); + } + + /** + * Gets the response to the user input. + * + * @param input The user input. + * @return The response to the user. + */ + public CommandResult getResponse(String input) { + try { + Command command = Parser.parse(input); + String response = command.execute(taskList, storage); + boolean isExit = command.isExit(); + return new CommandResult(response, isExit); + } catch (OpusException e) { + return new CommandResult(e.getMessage(), false); + } + } +} diff --git a/src/main/java/opus/Parser.java b/src/main/java/opus/Parser.java new file mode 100644 index 0000000000..a0e0cbb49a --- /dev/null +++ b/src/main/java/opus/Parser.java @@ -0,0 +1,166 @@ +package opus; + +import opus.commands.AddCommand; +import opus.commands.Command; +import opus.commands.ByeCommand; +import opus.commands.ListCommand; +import opus.commands.MarkCommand; +import opus.commands.DeleteCommand; +import opus.commands.HelpCommand; +import opus.exceptions.OpusException; +import opus.exceptions.OpusEmptyDescriptionException; +import opus.exceptions.OpusMissingArgumentException; +import opus.exceptions.OpusInvalidDateFormatException; +import opus.exceptions.OpusUnknownCommandException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * The Parser class is responsible for parsing user commands and returning the corresponding Command objects. + * It handles validation and throws specific exceptions for invalid inputs. + */ +public class Parser { + + /** + * Parses the full command entered by the user and returns the corresponding Command object. + * + * @param fullCommand The complete command entered by the user. + * @return The Command object corresponding to the command. + * @throws OpusException If the command is invalid or cannot be parsed. + */ + public static Command parse(String fullCommand) throws OpusException { + String[] words = fullCommand.trim().split(" ", 2); + String action = words[0]; + String details = words.length > 1 ? words[1].trim() : ""; + + switch (action) { + case "bye": + return new ByeCommand(); + + case "list": + return new ListCommand(); + + case "mark": + return new MarkCommand(details); + + case "delete": + return new DeleteCommand(details); + + case "todo": + if (details.isEmpty()) { + throw new OpusEmptyDescriptionException("The description of a todo cannot be empty."); + } + return new AddCommand(action, details); + + case "deadline": + if (details.isEmpty()) { + throw new OpusMissingArgumentException("The description and date of a deadline cannot be empty."); + } + String[] deadlineParts = details.split(" /by ", 2); + if (deadlineParts.length < 2 || deadlineParts[0].trim().isEmpty() || deadlineParts[1].trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide details for the deadline."); + } + if (!isValidDate(deadlineParts[1].trim())) { + throw new OpusInvalidDateFormatException("Invalid date format for deadline. Please use yyyy-MM-dd."); + } + return new AddCommand(action, details); + + case "event": + if (details.isEmpty()) { + throw new OpusMissingArgumentException("The description and dates of an event cannot be empty."); + } + String[] eventParts = details.split(" /from ", 2); + if (eventParts.length < 2 || eventParts[0].trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide the description and /from date for the event."); + } + String[] timeParts = eventParts[1].split(" /to ", 2); + if (timeParts.length < 2 || timeParts[0].trim().isEmpty() || timeParts[1].trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide both the dates for the event."); + } + if (!isValidDate(timeParts[0].trim()) || !isValidDate(timeParts[1].trim())) { + throw new OpusInvalidDateFormatException("Invalid date format. Please use yyyy-MM-dd."); + } + return new AddCommand(action, details); + + case "help": + return new HelpCommand(); + + default: + throw new OpusUnknownCommandException("I'm sorry, but I don't know what that means."); + } + } + + /** + * Parses a string containing a task index and returns the index as an integer. + * + * @param details The string containing the task index. + * @return The task index as an integer. + * @throws OpusException If the index is missing or invalid. + */ + private static int parseIndex(String details) throws OpusException { + if (details.trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide a task number."); + } + try { + int index = Integer.parseInt(details.trim()) - 1; + if (index < 0) { + throw new OpusException("Task number must be a positive integer."); + } + return index; + } catch (NumberFormatException e) { + throw new OpusException("Please provide a valid task number."); + } + } + + /** + * Validates if the provided date string matches the expected format yyyy-MM-dd. + * + * @param date The date string to validate. + * @return True if the date is valid, false otherwise. + */ + private static boolean isValidDate(String date) { + try { + DateTimeFormatter inputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate.parse(date, inputFormat); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + /** + * Parses the details for a deadline task, extracting the task description and deadline. + * + * @param details The details part of the command for a deadline task. + * @return A string array containing the task description and the deadline. + * @throws OpusException If the details are invalid. + */ + public static String[] parseDeadlineDetails(String details) throws OpusException { + String[] deadlineParts = details.split(" /by ", 2); + if (deadlineParts.length < 2 || deadlineParts[0].trim().isEmpty() || deadlineParts[1].trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide both details for the deadline."); + } + return deadlineParts; + } + + /** + * Parses the details for an event task, extracting the task description, start time, and end time. + * + * @param details The details part of the command for an event task. + * @return A string array containing the task description, start time, and end time. + * @throws OpusException If the details are invalid. + */ + public static String[] parseEventDetails(String details) throws OpusException { + String[] eventParts = details.split(" /from ", 2); + if (eventParts.length < 2 || eventParts[0].trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide the description and /from date for the event."); + } + String[] timeParts = eventParts[1].split(" /to ", 2); + if (timeParts.length < 2 || timeParts[0].trim().isEmpty() || timeParts[1].trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide both the /from date and /to date for the event."); + } + return new String[] { eventParts[0].trim(), timeParts[0].trim(), timeParts[1].trim() }; + } +} diff --git a/src/main/java/opus/Storage.java b/src/main/java/opus/Storage.java new file mode 100644 index 0000000000..20efeb986d --- /dev/null +++ b/src/main/java/opus/Storage.java @@ -0,0 +1,97 @@ +package opus; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +import opus.tasks.Deadline; +import opus.tasks.Event; +import opus.tasks.Task; +import opus.tasks.ToDo; + +/** + * The Storage class handles the loading and saving of tasks from/to a file. + * It reads tasks from a file to initialize the task list and writes tasks to + * the file to save the current state of the task list. + */ +public class Storage { + private String filePath; + + /** + * Creates a Storage object with the specified file path. + * + * @param filePath The path to the file where tasks are stored. + */ + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * Loads tasks from the file and returns them as an ArrayList. + * If the file does not exist or there is an error, an empty list is returned. + * + * @return An ArrayList of tasks loaded from the file. + */ + public ArrayList load() { + ArrayList tasks = new ArrayList<>(); + File file = new File(filePath); + + try { + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + String[] parts = line.split("\\|"); + String taskType = parts[0]; + boolean isDone = parts[1].equals("1"); + switch (taskType) { + case "T": + tasks.add(new ToDo(parts[2])); + break; + case "D": + tasks.add(new Deadline(parts[2], parts[3])); + break; + case "E": + tasks.add(new Event(parts[2], parts[3], parts[4])); + break; + default: + break; + } + if (isDone) { + tasks.get(tasks.size() - 1).markAsDone(); + } + } + scanner.close(); + return tasks; + } catch (IOException e) { + return tasks; + } + } + + /** + * Saves the current list of tasks to the file. + * If the file or directories do not exist, they are created. + * + * @param tasks The list of tasks to save to the file. + */ + public void save(ArrayList tasks) { + File file = new File(filePath); + + File parentDir = file.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + + try { + FileWriter writer = new FileWriter(filePath); + for (Task task : tasks) { + writer.write(task.toSaveFormat() + "\n"); + } + writer.close(); + } catch (IOException e) { + // Log or handle the exception if saving fails + return; + } + } +} diff --git a/src/main/java/opus/TaskList.java b/src/main/java/opus/TaskList.java new file mode 100644 index 0000000000..185bbf39b7 --- /dev/null +++ b/src/main/java/opus/TaskList.java @@ -0,0 +1,100 @@ +package opus; + +import java.util.ArrayList; + +import opus.tasks.Task; + +/** + * Manages a list of tasks in the application. Supports adding, retrieving, + * removing, and listing tasks. + */ +public class TaskList { + private ArrayList tasks; + + /** + * Initializes an empty task list. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Initializes the task list with a pre-existing list of tasks. + * + * @param tasks The list of tasks to initialize with. + */ + public TaskList(ArrayList tasks) { + this.tasks = tasks; + } + + /** + * Adds a task to the task list. + * + * @param task The task to be added. + */ + public void addTask(Task task) { + tasks.add(task); + } + + /** + * Retrieves a task from the list by its index. + * + * @param index The index of the task. + * @return The task at the specified index. + */ + public Task getTask(int index) { + return tasks.get(index); + } + + /** + * Removes a task from the list by its index. + * + * @param index The index of the task to be removed. + * @return The removed task. + */ + public Task removeTask(int index) { + return tasks.remove(index); + } + + /** + * Returns the number of tasks in the task list. + * + * @return The size of the task list. + */ + public int getSize() { + return tasks.size(); + } + + /** + * Retrieves the list of tasks. + * + * @return The list of tasks. + */ + public ArrayList getTasks() { + return tasks; + } + + /** + * Prints all the tasks in the task list. + */ + public void listTasks() { + for (int i = 0; i < tasks.size(); i++) { + System.out.println((i + 1) + ". " + tasks.get(i)); + } + } + + /** + * Marks a task as done. + * + */ + public void findTasks(String keyword) { + int count = 0; + System.out.println("Here are the matching tasks in your list:"); + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).contains(keyword)) { + count++; + System.out.println(count + ". " + tasks.get(i)); + } + } + } +} diff --git a/src/main/java/opus/commands/AddCommand.java b/src/main/java/opus/commands/AddCommand.java new file mode 100644 index 0000000000..54be5ccaad --- /dev/null +++ b/src/main/java/opus/commands/AddCommand.java @@ -0,0 +1,60 @@ +package opus.commands; + +import opus.Storage; +import opus.TaskList; +import opus.exceptions.OpusException; +import opus.Parser; +import opus.tasks.Deadline; +import opus.tasks.Event; +import opus.tasks.Task; +import opus.tasks.ToDo; + +/** + * Represents a command to add a task to the task list. + */ +public class AddCommand implements Command { + private final String action; + private final String details; + + /** + * Constructs an AddCommand object. + * + * @param action The type of task to add. + * @param details The details of the task to add. + */ + public AddCommand(String action, String details) { + this.action = action; + this.details = details; + } + + @Override + public String execute(TaskList taskList, Storage storage) throws OpusException { + Task task; + switch (action) { + case "todo": + if (details.isEmpty()) { + throw new OpusException("The description of a todo cannot be empty."); + } + task = new ToDo(details); + break; + case "deadline": + String[] deadlineParts = Parser.parseDeadlineDetails(details); + task = new Deadline(deadlineParts[0], deadlineParts[1]); + break; + case "event": + String[] eventParts = Parser.parseEventDetails(details); + task = new Event(eventParts[0], eventParts[1], eventParts[2]); + break; + default: + throw new OpusException("Invalid task type."); + } + taskList.addTask(task); + return "Got it. I've added this task:\n" + task.toString() + + "\n" + "Now you have " + taskList.getSize() + " tasks in the list."; + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/opus/commands/ByeCommand.java b/src/main/java/opus/commands/ByeCommand.java new file mode 100644 index 0000000000..f691fb4827 --- /dev/null +++ b/src/main/java/opus/commands/ByeCommand.java @@ -0,0 +1,29 @@ +package opus.commands; + +import opus.exceptions.OpusException; +import opus.Storage; +import opus.TaskList; + +/** + * Represents a command to exit the program. + */ +public class ByeCommand implements Command { + /** + * Executes the command to exit the program. + * + * @param taskList The task list. + * @param storage The storage. + * @return The response to the user. + * @throws OpusException If an error occurs during execution. + */ + @Override + public String execute(TaskList taskList, Storage storage) throws OpusException { + storage.save(taskList.getTasks()); + return "Bye. Hope to see you again soon!"; + } + + @Override + public boolean isExit() { + return true; + } +} diff --git a/src/main/java/opus/commands/Command.java b/src/main/java/opus/commands/Command.java new file mode 100644 index 0000000000..34d754bd2d --- /dev/null +++ b/src/main/java/opus/commands/Command.java @@ -0,0 +1,13 @@ +package opus.commands; + +import opus.exceptions.OpusException; +import opus.Storage; +import opus.TaskList; + +/** + * Represents a command that can be executed by the user. + */ +public interface Command { + String execute(TaskList taskList, Storage storage) throws OpusException; + boolean isExit(); +} diff --git a/src/main/java/opus/commands/CommandResult.java b/src/main/java/opus/commands/CommandResult.java new file mode 100644 index 0000000000..fd9cbc2415 --- /dev/null +++ b/src/main/java/opus/commands/CommandResult.java @@ -0,0 +1,28 @@ +package opus.commands; + +/** + * Represents the result of a command execution. + */ +public class CommandResult { + private final String feedbackToUser; + private final boolean isExit; + + /** + * Creates a new command result. + * + * @param feedbackToUser The feedback to the user. + * @param isExit Whether the program should exit. + */ + public CommandResult(String feedbackToUser, boolean isExit) { + this.feedbackToUser = feedbackToUser; + this.isExit = isExit; + } + + public String getFeedbackToUser() { + return feedbackToUser; + } + + public boolean isExit() { + return isExit; + } +} diff --git a/src/main/java/opus/commands/DeleteCommand.java b/src/main/java/opus/commands/DeleteCommand.java new file mode 100644 index 0000000000..1a57334d3c --- /dev/null +++ b/src/main/java/opus/commands/DeleteCommand.java @@ -0,0 +1,53 @@ +package opus.commands; + +import opus.exceptions.OpusException; +import opus.exceptions.OpusMissingArgumentException; +import opus.exceptions.OpusInvalidArgumentException; +import opus.exceptions.OpusTaskNotFoundException; +import opus.Storage; +import opus.TaskList; +import opus.tasks.Task; + +/** + * Represents a command to delete a task from the task list. + */ +public class DeleteCommand implements Command { + private final int taskIndex; + + /** + * Creates a new DeleteCommand object. + * + * @param details The details of the command. + * @throws OpusException If the task number is invalid. + */ + public DeleteCommand(String details) throws OpusException { + if (details.trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide the task number to delete."); + } + try { + this.taskIndex = Integer.parseInt(details.trim()) - 1; // Convert to zero-based index + if (taskIndex < 0) { + throw new OpusInvalidArgumentException("Task number must be a positive integer."); + } + } catch (NumberFormatException e) { + throw new OpusInvalidArgumentException("Please provide a valid task number."); + } + } + + @Override + public String execute(TaskList taskList, Storage storage) throws OpusException { + if (taskIndex < 0 || taskIndex >= taskList.getSize()) { + throw new OpusTaskNotFoundException("Task number out of range."); + } + Task task = taskList.getTask(taskIndex); + taskList.removeTask(taskIndex); + storage.save(taskList.getTasks()); + return "Noted. I've removed this task:\n" + task.toString() + + "\nNow you have " + taskList.getSize() + " tasks in the list."; + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/opus/commands/HelpCommand.java b/src/main/java/opus/commands/HelpCommand.java new file mode 100644 index 0000000000..b84ba67325 --- /dev/null +++ b/src/main/java/opus/commands/HelpCommand.java @@ -0,0 +1,34 @@ +package opus.commands; + +import opus.Storage; +import opus.TaskList; + +/** + * Represents a command to show the help message. + */ +public class HelpCommand implements Command { + /** + * Executes the command. + * + * @param taskList The task list. + * @param storage The storage. + * @return The response to the user. + */ + @Override + public String execute(TaskList taskList, Storage storage) { + return "Here are the commands you can use:\n" + + "1. list - List all tasks\n" + + "2. mark - Mark a task as done\n" + + "3. delete - Delete a task\n" + + "4. deadline /by - Add a deadline\n" + + "5. event /from /to - Add an event\n" + + "6. todo - Add a Todo task\n" + + "7. bye - Exit the application\n" + + "8. help - Show this help message"; + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/opus/commands/ListCommand.java b/src/main/java/opus/commands/ListCommand.java new file mode 100644 index 0000000000..9a63ffd324 --- /dev/null +++ b/src/main/java/opus/commands/ListCommand.java @@ -0,0 +1,32 @@ +package opus.commands; + +import opus.Storage; +import opus.TaskList; +import opus.tasks.Task; + +/** + * Represents a command to list all tasks in the task list. + */ +public class ListCommand implements Command { + /** + * Executes the command to list all tasks in the task list. + * + * @param taskList The task list. + * @param storage The storage. + * @return The response to the user. + */ + @Override + public String execute(TaskList taskList, Storage storage) { + StringBuilder response = new StringBuilder(); + for (int i = 0; i < taskList.getSize(); i++) { + Task task = taskList.getTask(i); + response.append(i + 1).append(". ").append(task.toString()).append("\n"); + } + return response.toString(); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/opus/commands/MarkCommand.java b/src/main/java/opus/commands/MarkCommand.java new file mode 100644 index 0000000000..606309f9f7 --- /dev/null +++ b/src/main/java/opus/commands/MarkCommand.java @@ -0,0 +1,60 @@ +package opus.commands; + +import opus.exceptions.OpusException; +import opus.exceptions.OpusMissingArgumentException; +import opus.exceptions.OpusInvalidArgumentException; +import opus.exceptions.OpusTaskNotFoundException; +import opus.Storage; +import opus.TaskList; +import opus.tasks.Task; + +/** + * Represents a command to mark a task as done. + */ +public class MarkCommand implements Command { + private final int taskIndex; + + /** + * Creates a new MarkCommand object. + * + * @param details The details of the command. + * @throws OpusException If the task number is invalid. + */ + public MarkCommand(String details) throws OpusException { + if (details.trim().isEmpty()) { + throw new OpusMissingArgumentException("Please provide the task number to mark."); + } + try { + this.taskIndex = Integer.parseInt(details.trim()) - 1; // Convert to zero-based index + if (taskIndex < 0) { + throw new OpusInvalidArgumentException("Task number must be a positive integer."); + } + } catch (NumberFormatException e) { + throw new OpusInvalidArgumentException("Please provide a valid task number."); + } + } + + /** + * Marks the task at the specified index as done. + * + * @param taskList The task list containing the task to mark as done. + * @param storage The storage object to save the task list to. + * @return A message indicating that the task has been marked as done. + * @throws OpusException If the task number is out of range. + */ + @Override + public String execute(TaskList taskList, Storage storage) throws OpusException { + if (taskIndex < 0 || taskIndex >= taskList.getSize()) { + throw new OpusTaskNotFoundException("Task number out of range."); + } + Task task = taskList.getTask(taskIndex); + task.markAsDone(); + storage.save(taskList.getTasks()); + return "Nice! I've marked this task as done:\n" + task.toString(); + } + + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/opus/exceptions/OpusEmptyDescriptionException.java b/src/main/java/opus/exceptions/OpusEmptyDescriptionException.java new file mode 100644 index 0000000000..58ab75c0a6 --- /dev/null +++ b/src/main/java/opus/exceptions/OpusEmptyDescriptionException.java @@ -0,0 +1,10 @@ +package opus.exceptions; + +/** + * Represents an exception thrown when the description of a task is empty. + */ +public class OpusEmptyDescriptionException extends OpusException { + public OpusEmptyDescriptionException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/exceptions/OpusException.java b/src/main/java/opus/exceptions/OpusException.java new file mode 100644 index 0000000000..9bfa7bb5dc --- /dev/null +++ b/src/main/java/opus/exceptions/OpusException.java @@ -0,0 +1,16 @@ +package opus.exceptions; + +/** + * Base exception class for all Opus-related exceptions. + */ +public class OpusException extends Exception { + + /** + * Constructs a new OpusException with the specified detail message. + * + * @param message The detail message. + */ + public OpusException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/exceptions/OpusInvalidArgumentException.java b/src/main/java/opus/exceptions/OpusInvalidArgumentException.java new file mode 100644 index 0000000000..31641e3970 --- /dev/null +++ b/src/main/java/opus/exceptions/OpusInvalidArgumentException.java @@ -0,0 +1,16 @@ +package opus.exceptions; + +/** + * Thrown when an argument provided by the user is invalid. + */ +public class OpusInvalidArgumentException extends OpusException { + + /** + * Constructs a new OpusInvalidArgumentException with the specified detail message. + * + * @param message The detail message. + */ + public OpusInvalidArgumentException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/exceptions/OpusInvalidDateFormatException.java b/src/main/java/opus/exceptions/OpusInvalidDateFormatException.java new file mode 100644 index 0000000000..544be85737 --- /dev/null +++ b/src/main/java/opus/exceptions/OpusInvalidDateFormatException.java @@ -0,0 +1,16 @@ +package opus.exceptions; + +/** + * Thrown when a date format is invalid or cannot be parsed. + */ +public class OpusInvalidDateFormatException extends OpusException { + + /** + * Constructs a new OpusInvalidDateFormatException with the specified detail message. + * + * @param message The detail message. + */ + public OpusInvalidDateFormatException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/exceptions/OpusMissingArgumentException.java b/src/main/java/opus/exceptions/OpusMissingArgumentException.java new file mode 100644 index 0000000000..1ec4771a53 --- /dev/null +++ b/src/main/java/opus/exceptions/OpusMissingArgumentException.java @@ -0,0 +1,16 @@ +package opus.exceptions; + +/** + * Thrown when a required argument is missing in the user's command. + */ +public class OpusMissingArgumentException extends OpusException { + + /** + * Constructs a new OpusMissingArgumentException with the specified detail message. + * + * @param message The detail message. + */ + public OpusMissingArgumentException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/exceptions/OpusTaskNotFoundException.java b/src/main/java/opus/exceptions/OpusTaskNotFoundException.java new file mode 100644 index 0000000000..beaf86baa5 --- /dev/null +++ b/src/main/java/opus/exceptions/OpusTaskNotFoundException.java @@ -0,0 +1,16 @@ +package opus.exceptions; + +/** + * Thrown when a task is not found in the task list, typically due to an invalid index. + */ +public class OpusTaskNotFoundException extends OpusException { + + /** + * Constructs a new OpusTaskNotFoundException with the specified detail message. + * + * @param message The detail message. + */ + public OpusTaskNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/exceptions/OpusUnknownCommandException.java b/src/main/java/opus/exceptions/OpusUnknownCommandException.java new file mode 100644 index 0000000000..ccf5dd1908 --- /dev/null +++ b/src/main/java/opus/exceptions/OpusUnknownCommandException.java @@ -0,0 +1,16 @@ +package opus.exceptions; + +/** + * Represents an exception thrown when an unknown or unsupported command is entered by the user. + */ +public class OpusUnknownCommandException extends OpusException { + + /** + * Constructs an OpusUnknownCommandException with a specific error message. + * + * @param message The error message that describes the exception. + */ + public OpusUnknownCommandException(String message) { + super(message); + } +} diff --git a/src/main/java/opus/tasks/Deadline.java b/src/main/java/opus/tasks/Deadline.java new file mode 100644 index 0000000000..0f90b03563 --- /dev/null +++ b/src/main/java/opus/tasks/Deadline.java @@ -0,0 +1,76 @@ +package opus.tasks; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + + + +/** + * Represents a task with a deadline. + **/ +public class Deadline extends Task { + protected LocalDate byDateTime; + protected String byString; + + /** + * Constructs a Deadline task with the specified description and deadline date. + * If the date cannot be parsed, it is stored as a string. + * + * @param description The description of the task. + * @param by The deadline in the format [yyyy-MM-dd]. + */ + public Deadline(String description, String by) { + super(description); + try { + DateTimeFormatter inputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + this.byDateTime = LocalDate.parse(by, inputFormat); + } catch (DateTimeParseException e) { + this.byString = by; + } + } + private String getBy() { + if (byDateTime != null) { + DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("MMM dd yyyy"); + return byDateTime.format(outputFormat); + } else { + return byString; + } + } + + /** + * Returns the string representation of the deadline, including its description + * and whether it is done. + * + * @return The string representation of the deadline task. + */ + @Override + public String toString() { + return "[D]" + (isDone ? "[X] " : "[ ] ") + description + " (by: " + getBy() + ")"; + } + + /** + * Returns the save format for the deadline task to be written to storage. + * + * @return The formatted string for saving the deadline task. + */ + @Override + public String toSaveFormat() { + return "D|" + (isDone ? "1" : "0") + "|" + description + "|" + getBy(); + } + + /** + * Creates a Deadline object from a formatted string read from storage. + * + * @param fullLine The line read from the storage file. + * @return A new Deadline object based on the parsed string. + */ + public static Task fromFileFormat(String fullLine) { + String[] parts = fullLine.split("\\|"); + Deadline deadline = new Deadline(parts[2], parts[3]); + if (parts[1].equals("1")) { + deadline.markAsDone(); + } + return deadline; + } +} diff --git a/src/main/java/opus/tasks/Event.java b/src/main/java/opus/tasks/Event.java new file mode 100644 index 0000000000..ebb167f328 --- /dev/null +++ b/src/main/java/opus/tasks/Event.java @@ -0,0 +1,103 @@ +package opus.tasks; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + + +/** + * Represents an event task with a start date and an end date. + * The event dates are parsed from a string using {@code yyyy-MM-dd} format. + */ +public class Event extends Task { + private LocalDate fromDateTime; + private LocalDate toDateTime; + private String fromString; + private String toString; + + /** + * Constructs an Event task with the specified description, start date, and end date. + * If the dates cannot be parsed, they are stored as strings. + * + * @param description The description of the event. + * @param from The start date of the event in {@code yyyy-MM-dd} format. + * @param to The end date of the event in {@code yyyy-MM-dd} format. + */ + public Event(String description, String from, String to) { + super(description); + try { + DateTimeFormatter inputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + this.fromDateTime = LocalDate.parse(from, inputFormat); + this.toDateTime = LocalDate.parse(to, inputFormat); + } catch (DateTimeParseException e) { + this.fromString = from; + this.toString = to; + } + } + + /** + * Returns the formatted start date. If the date was successfully parsed, it is + * returned in {MMM dd yyyy} format. Otherwise, the original string is returned. + * + * @return The formatted start date or the original string if parsing failed. + */ + private String getFrom() { + if (fromDateTime != null) { + DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("MMM dd yyyy"); + return fromDateTime.format(outputFormat); + } else { + return fromString; + } + } + + /** + * Returns the formatted end date. If the date was successfully parsed, it is + * returned in {MMM dd yyyy} format. Otherwise, the original string is returned. + * + * @return The formatted end date or the original string if parsing failed. + */ + private String getTo() { + if (toDateTime != null) { + DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("MMM dd yyyy"); + return toDateTime.format(outputFormat); + } else { + return toString; + } + } + + /** + * Returns the save format for the event task to be written to storage. + * + * @return The formatted string for saving the event task. + */ + @Override + public String toSaveFormat() { + return "E|" + (isDone ? "1" : "0") + "|" + description + "|" + getFrom() + "|" + getTo(); + } + + /** + * Creates an Event object from a formatted string read from storage. + * + * @param fullLine The line read from the storage file. + * @return A new Event object based on the parsed string. + */ + public static Task fromFileFormat(String fullLine) { + String[] parts = fullLine.split("\\|"); + Event event = new Event(parts[2], parts[3], parts[4]); + if (parts[1].equals("1")) { + event.markAsDone(); + } + return event; + } + + /** + * Returns the string representation of the event, including its description, + * start date, end date, and whether it is done. + * + * @return The string representation of the event task. + */ + @Override + public String toString() { + return "[E]" + (isDone ? "[X] " : "[ ] ") + description + " (from: " + getFrom() + " to: " + getTo() + ")"; + } +} diff --git a/src/main/java/opus/tasks/Task.java b/src/main/java/opus/tasks/Task.java new file mode 100644 index 0000000000..833f8aab38 --- /dev/null +++ b/src/main/java/opus/tasks/Task.java @@ -0,0 +1,80 @@ +package opus.tasks; + +/** + * Represents a generic task in the application. Each task has a description and + * a status indicating whether it is done. + */ +public abstract class Task { + protected String description; + protected boolean isDone; + + /** + * Constructs a Task with the specified description. + * The task is initially marked as not done. + * + * @param description The description of the task. + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Marks the task as done. + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * Returns whether the task is done. + * + * @return {@code true} if the task is done, {@code false} otherwise. + */ + public boolean isDone() { + return isDone; + } + + /** + * Returns the string format for saving the task to a file. + * This method must be implemented by subclasses. + * + * @return The formatted string for saving the task. + */ + public abstract String toSaveFormat(); + + /** + * Creates a Task object from a formatted string read from storage. + * + * @param fullLine The line read from the storage file. + * @return A new Task object based on the parsed string, or {@code null} if the task type is unknown. + */ + public static Task fromFileFormat(String fullLine) { + String[] parts = fullLine.split("\\|"); + + if (parts[0].equals("T")) { + return ToDo.fromFileFormat(fullLine); + } + if (parts[0].equals("D")) { + return Deadline.fromFileFormat(fullLine); + } + if (parts[0].equals("E")) { + return Event.fromFileFormat(fullLine); + } + + switch (parts[0]) { + case "T": + return ToDo.fromFileFormat(fullLine); + case "D": + return Deadline.fromFileFormat(fullLine); + case "E": + return Event.fromFileFormat(fullLine); + default: + return null; + } + } + + public boolean contains(String keyword) { + return description.contains(keyword); + } +} diff --git a/src/main/java/opus/tasks/ToDo.java b/src/main/java/opus/tasks/ToDo.java new file mode 100644 index 0000000000..ef93cd9106 --- /dev/null +++ b/src/main/java/opus/tasks/ToDo.java @@ -0,0 +1,51 @@ +package opus.tasks; + +/** + * Represents a ToDo task, which is a basic task without any date or time associated with it. + */ +public class ToDo extends Task { + + /** + * Constructs a ToDo task with the specified description. + * + * @param description The description of the ToDo task. + */ + public ToDo(String description) { + super(description); + } + + /** + * Returns the string representation of the ToDo task in a format suitable for saving. + * + * @return The formatted string representing the ToDo task. + */ + @Override + public String toSaveFormat() { + return "T|" + (isDone ? "1" : "0") + "|" + description; + } + + /** + * Creates a ToDo object from a formatted string read from storage. + * + * @param fullLine The line read from the storage file. + * @return A new ToDo object based on the parsed string. + */ + public static Task fromFileFormat(String fullLine) { + String[] parts = fullLine.split("\\|"); + ToDo todo = new ToDo(parts[2]); + if (parts[1].equals("1")) { + todo.markAsDone(); + } + return todo; + } + + /** + * Returns the string representation of the ToDo task. + * + * @return The string representation of the ToDo task. + */ + @Override + public String toString() { + return "[T]" + (isDone ? "[X] " : "[ ] ") + description; + } +} diff --git a/src/main/resources/opus.png b/src/main/resources/opus.png new file mode 100644 index 0000000000..5c4479637b Binary files /dev/null and b/src/main/resources/opus.png differ diff --git a/src/main/resources/user.png b/src/main/resources/user.png new file mode 100644 index 0000000000..5ca18d5074 Binary files /dev/null and b/src/main/resources/user.png differ diff --git a/src/test/java/DeadlineTest.java b/src/test/java/DeadlineTest.java new file mode 100644 index 0000000000..4fbea1e56d --- /dev/null +++ b/src/test/java/DeadlineTest.java @@ -0,0 +1,14 @@ +import org.junit.jupiter.api.Test; // Special import + +import opus.tasks.Deadline; + +import static org.junit.jupiter.api.Assertions.assertEquals; // Static import + +public class DeadlineTest { + + @Test + public void testDeadline() { + Deadline deadline = new Deadline("submit report", "2023-11-30"); + assertEquals("[D][ ] submit report (by: Nov 30 2023)", deadline.toString()); + } +} diff --git a/src/test/java/TaskTest.java b/src/test/java/TaskTest.java new file mode 100644 index 0000000000..9e42cd485b --- /dev/null +++ b/src/test/java/TaskTest.java @@ -0,0 +1,17 @@ +import opus.tasks.Task; +import opus.tasks.ToDo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + + +public class TaskTest { + + + @Test + public void testMarkAsDone() { + Task task = new ToDo("test task"); + task.markAsDone(); + assertEquals("[T][X] test task", task.toString()); + } +} diff --git a/src/test/java/ToDoTest.java b/src/test/java/ToDoTest.java new file mode 100644 index 0000000000..305f815b35 --- /dev/null +++ b/src/test/java/ToDoTest.java @@ -0,0 +1,14 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +import opus.tasks.ToDo; + + +public class ToDoTest { + + @Test + public void testToDo() { + ToDo todo = new ToDo("buy groceries"); + assertEquals("[T][ ] buy groceries", todo.toString()); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 657e74f6e7..02eb60382f 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,7 +1,52 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - +Hello! I'm Opus +What can I do for you? +Got it. I've added this task: +[T][ ] borrow book +Now you have 1 tasks in the list. +Got it. I've added this task: +[D][ ] return book (by: Sunday) +Now you have 2 tasks in the list. +Got it. I've added this task: +[E][ ] project meeting (from: Mon 2pm to: 4pm) +Now you have 3 tasks in the list. +1. [T][ ] borrow book +2. [D][ ] return book (by: Sunday) +3. [E][ ] project meeting (from: Mon 2pm to: 4pm) +Nice! I've marked this task as done: +[T][X] borrow book +Nice! I've marked this task as done: +[D][X] return book (by: Sunday) +1. [T][X] borrow book +2. [D][X] return book (by: Sunday) +3. [E][ ] project meeting (from: Mon 2pm to: 4pm) +Got it. I've added this task: +[T][ ] write essay +Now you have 4 tasks in the list. +Got it. I've added this task: +[E][ ] team dinner (from: Fri 7pm to: 9pm) +Now you have 5 tasks in the list. +1. [T][X] borrow book +2. [D][X] return book (by: Sunday) +3. [E][ ] project meeting (from: Mon 2pm to: 4pm) +4. [T][ ] write essay +5. [E][ ] team dinner (from: Fri 7pm to: 9pm) +Got it. I've added this task: +[D][ ] submit report (by: Tuesday) +Now you have 6 tasks in the list. +Nice! I've marked this task as done: +[T][X] write essay +1. [T][X] borrow book +2. [D][X] return book (by: Sunday) +3. [E][ ] project meeting (from: Mon 2pm to: 4pm) +4. [T][X] write essay +5. [E][ ] team dinner (from: Fri 7pm to: 9pm) +6. [D][ ] submit report (by: Tuesday) +Got it. I've added this task: +[T][ ] plan vacation +Now you have 7 tasks in the list. +Got it. I've added this task: +[E][ ] conference (from: Wed 10am to: 12pm) +Now you have 8 tasks in the list. +Nice! I've marked this task as done: +[D][X] submit report (by: Tuesday) +Bye. Hope to see you again soon! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29bb2..4be8bd95e7 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1,17 @@ +todo borrow book +deadline return book /by Sunday +event project meeting /from Mon 2pm /to 4pm +list +mark 1 +mark 2 +list +todo write essay +event team dinner /from Fri 7pm /to 9pm +list +deadline submit report /by Tuesday +mark 4 +list +todo plan vacation +event conference /from Wed 10am /to 12pm +mark 6 +bye \ No newline at end of file diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh old mode 100644 new mode 100755 index c9ec870033..3680619f15 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -13,21 +13,17 @@ then 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 +if ! javac -cp ../src/main/java/opus/opus -Xlint:none -d ../bin ../src/main/java/opus/opus/*.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 +java -classpath ../bin Opus < input.txt > ACTUAL.TXT # compare the output to the expected output -diff ACTUAL.TXT EXPECTED-UNIX.TXT +diff ACTUAL.TXT EXPECTED.TXT if [ $? -eq 0 ] then echo "Test result: PASSED" @@ -35,4 +31,4 @@ then else echo "Test result: FAILED" exit 1 -fi \ No newline at end of file +fi