diff --git a/.gitignore b/.gitignore index f69985ef..fb44f48a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.txt text-ui-test/EXPECTED-UNIX.TXT +/data/ +/src/main/META-INF/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..b2f5d5bc --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.0' + + String javaFxVersion = '11' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClassName = "Duke" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.29' +} + +run { + standardInput = System.in + enableAssertions = true +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f3d88b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..f371643e --- /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-7.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..2fe81a7d --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..62bd9b9c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/DialogBox.java b/src/main/java/DialogBox.java new file mode 100644 index 00000000..7883cdb1 --- /dev/null +++ b/src/main/java/DialogBox.java @@ -0,0 +1,71 @@ +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.shape.Circle; + +public class DialogBox extends HBox { + + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/views/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + + displayPicture.setImage(img); + Circle clip = new Circle(25, 25, 25); + displayPicture.setClip(clip); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + /** + * Returns the user dialog box for display. + * + * @param text User input message text. + * @param img User profile display picture. + */ + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + /** + * Returns the duke dialog box for display. + * + * @param text Duke response message text. + * @param img Duke profile display picture. + */ + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} \ No newline at end of file diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java index 5d313334..7b86abae 100644 --- a/src/main/java/Duke.java +++ b/src/main/java/Duke.java @@ -1,10 +1,75 @@ +import commands.Command; +import parser.Parser; +import storage.Storage; +import ui.Ui; +import tasks.*; +import exceptions.*; + +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Main class of the Duke program. + * The Duke program is a ChatBot that helps user to keep track of to-dos, deadlines and events. + */ public class Duke { + + private final Storage storage; + private TaskList taskList; + private final Ui ui; + + /** + * Constructor of Duke. + * + * @param filePath Location of the file which stores the tasks. + */ + public Duke(String filePath) { + ui = new Ui(); + storage = new Storage(filePath); + try { + taskList = new TaskList(storage.loadTasks()); + } catch (FileNotFoundException e) { + ui.showLoadingError(); + try { + storage.init(); + } catch (IOException error) { + ui.showError(error.getMessage()); + } + taskList = new TaskList(); + } + } + + /** Method to start running the Duke program. */ + public void runCmd() { + ui.showWelcome(); + boolean isExit = false; + while (!isExit) { + try { + String fullCommand = ui.readCommand(); + ui.showLine(); + Command c = Parser.parse(fullCommand); + c.execute(taskList, ui, storage); + isExit = c.isExit(); + } catch (DukeException | IOException e) { + ui.showError(e.getMessage()); + } finally { + ui.showLine(); + } + } + } + + /** Returns Duke response message for display on GUI. */ + public String reply(String input) { + try { + String fullCommand = input; + Command c = Parser.parse(fullCommand); + return c.execute(taskList, ui, storage); + } catch (DukeException | IOException e) { + return e.getMessage(); + } + } + public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); + new Duke("data/tasks.txt").runCmd(); } } diff --git a/src/main/java/Launcher.java b/src/main/java/Launcher.java new file mode 100644 index 00000000..0e288bc0 --- /dev/null +++ b/src/main/java/Launcher.java @@ -0,0 +1,7 @@ +import javafx.application.Application; + +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 00000000..272dbbde --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,30 @@ +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private final Duke duke = new Duke("data/tasks.txt"); + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/views/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + scene.getStylesheets().add(Main.class.getResource("/css/stylesheet.css").toExternalForm()); + stage.setScene(scene); + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/MainWindow.java b/src/main/java/MainWindow.java new file mode 100644 index 00000000..6f32ab00 --- /dev/null +++ b/src/main/java/MainWindow.java @@ -0,0 +1,49 @@ +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +/** + * Controller for MainWindow. Provides the layout for the other controls. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Duke duke; + + private final Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private final Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + public void setDuke(Duke d) { + duke = d; + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = duke.reply(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/commands/AddCommand.java b/src/main/java/commands/AddCommand.java new file mode 100644 index 00000000..1ddf2d62 --- /dev/null +++ b/src/main/java/commands/AddCommand.java @@ -0,0 +1,39 @@ +package commands; + +import storage.Storage; +import tasks.*; +import tasks.TaskList; +import ui.Ui; +import ui.Messages; + +import java.io.IOException; + +/** + * AddCommand is used to add new task into the TaskList. + * Extends the Command class. + */ +public class AddCommand extends Command{ + Task task; + + public AddCommand(Task task){ + this.task = task; + } + + public String execute(TaskList taskList, Ui ui, Storage storage) throws IOException { + boolean isAdded = taskList.addTask(task); + + if (isAdded){ + storage.saveTasks(taskList.getTasks()); + ui.showAdded(); + ui.printTask(task.toString()); + ui.printTaskCount(taskList.getListSize()); + + return Messages.ADDED_MSG + Messages.getTaskMsg(task.toString()) + + Messages.getTaskCountMsg(taskList.getListSize()); + + } else { + ui.showError("Task is not added. There is a schedule clash!"); + return "Task is not added. There is a schedule clash!"; + } + } +} diff --git a/src/main/java/commands/Command.java b/src/main/java/commands/Command.java new file mode 100644 index 00000000..577e16cd --- /dev/null +++ b/src/main/java/commands/Command.java @@ -0,0 +1,45 @@ +package commands; + +import exceptions.DukeException; +import storage.Storage; +import tasks.TaskList; +import ui.Ui; + +import java.io.IOException; + +/** + * This Command class is an abstract class to be extended by other command classes. + */ +public abstract class Command { + + protected boolean isExit; + + public Command(){} + + /** + * Executes a command and returns the success message. + * Needs to be implemented in other command classes (abstract). + * + * @param taskList TaskList object where all available tasks are stored. + * @param ui Ui object to print statements for user interaction. + * @param storage Storage object to save the tasks into specific file. + * + * @throws DukeException If command execution failed. + * @throws IOException If file writing/ saving failed. + */ + public abstract String execute(TaskList taskList, Ui ui, Storage storage) throws DukeException, IOException; + + /** + * Sets program exit flag. + * + * @param exit Exit flag (true or false). + */ + public void setExit(boolean exit){ + isExit = exit; + } + + /** Returns the exit flag */ + public boolean isExit(){ + return isExit; + } +} diff --git a/src/main/java/commands/CommandEnum.java b/src/main/java/commands/CommandEnum.java new file mode 100644 index 00000000..108c29b0 --- /dev/null +++ b/src/main/java/commands/CommandEnum.java @@ -0,0 +1,5 @@ +package commands; + +public enum CommandEnum { + LIST, TODO, DEADLINE, EVENT, DONE, DELETE, FIND, VIEW, HELP, BYE +} diff --git a/src/main/java/commands/CompleteCommand.java b/src/main/java/commands/CompleteCommand.java new file mode 100644 index 00000000..ca5c6c5e --- /dev/null +++ b/src/main/java/commands/CompleteCommand.java @@ -0,0 +1,30 @@ +package commands; + +import exceptions.DukeException; +import storage.Storage; +import tasks.TaskList; +import ui.Ui; +import ui.Messages; +import java.io.IOException; + +/** + * CompleteCommand is used to set the status of a task to "done". + * Extends the Command class. + */ +public class CompleteCommand extends Command{ + + int taskId; + + public CompleteCommand(int taskId){ + this.taskId = taskId; + } + + public String execute(TaskList taskList, Ui ui, Storage storage) throws DukeException, IOException { + String completedTask = taskList.setDone(taskId); + storage.saveTasks(taskList.getTasks()); + ui.showCompleted(); + ui.printTask(completedTask); + + return Messages.COMPLETED_MSG + Messages.getTaskMsg(completedTask); + } +} diff --git a/src/main/java/commands/DeleteCommand.java b/src/main/java/commands/DeleteCommand.java new file mode 100644 index 00000000..f01fd8b7 --- /dev/null +++ b/src/main/java/commands/DeleteCommand.java @@ -0,0 +1,31 @@ +package commands; + +import exceptions.DukeException; +import storage.Storage; +import tasks.TaskList; +import ui.Messages; +import ui.Ui; +import java.io.IOException; + +/** + * DeleteCommand is used to delete a task from the TaskList. + * Extends the Command class. + */ +public class DeleteCommand extends Command{ + + int taskId; + + public DeleteCommand(int taskId){ + this.taskId = taskId; + } + + public String execute(TaskList taskList, Ui ui, Storage storage) throws DukeException, IOException { + String deletedTask = taskList.deleteTask(taskId); + storage.saveTasks(taskList.getTasks()); + ui.showDeleted(); + ui.printTask(deletedTask); + ui.printTaskCount(taskList.getListSize()); + + return Messages.DELETED_MSG + Messages.getTaskMsg(deletedTask); + } +} diff --git a/src/main/java/commands/ExitCommand.java b/src/main/java/commands/ExitCommand.java new file mode 100644 index 00000000..cf7925f6 --- /dev/null +++ b/src/main/java/commands/ExitCommand.java @@ -0,0 +1,19 @@ +package commands; + +import storage.Storage; +import tasks.TaskList; +import ui.Messages; +import ui.Ui; + +/** + * ExitCommand is used to terminate and exit the Duke program. + * Extends the Command class. + */ +public class ExitCommand extends Command{ + + public String execute(TaskList taskList, Ui ui, Storage storage) { + ui.showExit(); + super.setExit(true); + return Messages.EXIT_MSG; + } +} diff --git a/src/main/java/commands/FindCommand.java b/src/main/java/commands/FindCommand.java new file mode 100644 index 00000000..d4066978 --- /dev/null +++ b/src/main/java/commands/FindCommand.java @@ -0,0 +1,29 @@ +package commands; + +import storage.Storage; +import tasks.Task; +import tasks.TaskList; +import ui.Messages; +import ui.Ui; + +import java.util.ArrayList; + +/** + * FindCommand is used to search for tasks which description contains the input keyword. + * Extends the Command class. + */ +public class FindCommand extends Command{ + + String keyword; + + public FindCommand(String keyword){ + this.keyword = keyword; + } + + public String execute(TaskList taskList, Ui ui, Storage storage) { + ArrayList results = taskList.searchTask(keyword); + ui.showSearchCompleted(); + ui.printAllTasks(results); + return Messages.SEARCH_COMPLETED_MSG + Messages.getAllTasksMsg(results); + } +} diff --git a/src/main/java/commands/HelpCommand.java b/src/main/java/commands/HelpCommand.java new file mode 100644 index 00000000..842e07d4 --- /dev/null +++ b/src/main/java/commands/HelpCommand.java @@ -0,0 +1,18 @@ +package commands; + +import storage.Storage; +import tasks.TaskList; +import ui.Messages; +import ui.Ui; + +/** + * HelpCommand is used to list all command types and their formats. + * Extends the Command class. + */ +public class HelpCommand extends Command{ + + public String execute(TaskList taskList, Ui ui, Storage storage) { + ui.showHelp(); + return Messages.HELP_MSG; + } +} diff --git a/src/main/java/commands/InvalidCommand.java b/src/main/java/commands/InvalidCommand.java new file mode 100644 index 00000000..ffc35854 --- /dev/null +++ b/src/main/java/commands/InvalidCommand.java @@ -0,0 +1,23 @@ +package commands; + +import exceptions.DukeException; +import storage.Storage; +import tasks.TaskList; +import ui.Ui; + +/** + * InvalidCommand is used to capture invalid input command that cannot be executed. + * Extends the Command class. + */ +public class InvalidCommand extends Command{ + + private final String errorMsg; + + public InvalidCommand(String errorMsg){ + this.errorMsg = errorMsg; + } + + public String execute(TaskList taskList, Ui ui, Storage storage) throws DukeException { + throw new DukeException(errorMsg); + } +} diff --git a/src/main/java/commands/ListCommand.java b/src/main/java/commands/ListCommand.java new file mode 100644 index 00000000..0883d1e7 --- /dev/null +++ b/src/main/java/commands/ListCommand.java @@ -0,0 +1,19 @@ +package commands; + +import storage.Storage; +import tasks.TaskList; +import ui.Messages; +import ui.Ui; + +/** + * ListCommand is used to list (print out) all the tasks in the TaskList. + * Extends the Command class. + */ +public class ListCommand extends Command{ + + public String execute(TaskList taskList, Ui ui, Storage storage) { + ui.showListed(); + ui.printAllTasks(taskList.getTasks()); + return Messages.LISTED_MSG + Messages.getAllTasksMsg(taskList.getTasks()); + } +} diff --git a/src/main/java/commands/ViewCommand.java b/src/main/java/commands/ViewCommand.java new file mode 100644 index 00000000..4e91783a --- /dev/null +++ b/src/main/java/commands/ViewCommand.java @@ -0,0 +1,27 @@ +package commands; + +import storage.Storage; +import tasks.TaskList; +import ui.Messages; +import ui.Ui; + +import java.time.LocalDate; + +/** + * ViewCommand is used to list all the tasks scheduled on the input date. + * Extends the Command class. + */ +public class ViewCommand extends Command{ + + LocalDate date; + + public ViewCommand(LocalDate date){ + this.date = date; + } + + public String execute(TaskList taskList, Ui ui, Storage storage) { + ui.showScheduleFor(date); + ui.printAllTasks(taskList.getTaskSchedule(date)); + return Messages.getScheduledMsg(date) + Messages.getAllTasksMsg(taskList.getTaskSchedule(date)); + } +} \ No newline at end of file diff --git a/src/main/java/exceptions/DukeException.java b/src/main/java/exceptions/DukeException.java new file mode 100644 index 00000000..53e6b3bc --- /dev/null +++ b/src/main/java/exceptions/DukeException.java @@ -0,0 +1,20 @@ +package exceptions; + +/** + * The class DukeException is a form of Throwable that indicates exceptions caught by the Duke program. + */ +public class DukeException extends Exception{ + private final String message; + + /** + * Constructor of DukeException. + * + * @param message Duke error message to be printed. + */ + public DukeException(String message) { this.message = message; } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/parser/Parser.java b/src/main/java/parser/Parser.java new file mode 100644 index 00000000..fb3b5cbe --- /dev/null +++ b/src/main/java/parser/Parser.java @@ -0,0 +1,178 @@ +package parser; + +import commands.*; +import tasks.DeadlineTask; +import tasks.EventTask; +import tasks.Task; +import tasks.ToDoTask; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * A Parser object deals with making sense of the user command. + */ +public class Parser { + /** + * Returns a Command object converted from the input command string. + * If the input command string is invalid, an InvalidCommand object will be returned. + * + * @param fullCommand Input command string. + */ + public static Command parse(String fullCommand) { + assert fullCommand != null : "Command be null"; + + // split the fullCommand and get the command keyword + String[] args = fullCommand.split(" ", 2); + String keyword = args[0].toUpperCase(); + + CommandEnum commandEnum = null; + try { + commandEnum = CommandEnum.valueOf(keyword); + } catch (IllegalArgumentException e) { + //Use ui to display error later + return new InvalidCommand("Command <" + keyword.toLowerCase() + "> not found."); + } + + switch(commandEnum){ + case LIST: + return doList(); + case HELP: + return doHelp(); + case TODO: + return doAddTodo(args); + case DEADLINE: + return doAddDeadline(args); + case EVENT: + return doAddEvent(args); + case DONE: + return doComplete(args); + case DELETE: + return doDelete(args); + case FIND: + return doFind(args); + case VIEW: + return doView(args); + case BYE: + return doExit(); + default: + return new InvalidCommand("Error executing command"); + } + } + + private static Command doList(){ + return new ListCommand(); + } + + private static Command doHelp(){ + return new HelpCommand(); + } + + private static Command doAddTodo (String[] args){ + String description = args[1]; + boolean descriptionMissing = description.isBlank() || description.isEmpty(); + try { + if (descriptionMissing) { + return new InvalidCommand("Task cannot be added. " + + "\nDescription is missing."); + } + Task task = new ToDoTask(description); + return new AddCommand(task); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Task cannot be added. " + + "\nDescription is missing."); + } + } + + private static Command doAddDeadline(String[] args){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"); + try { + String[] deadlineInfo = args[1].split(" /by "); + String description = deadlineInfo[0]; + boolean descriptionMissing = description.isBlank() || description.isEmpty(); + if (descriptionMissing) { + return new InvalidCommand("Task cannot be added. " + + "\nDescription is missing."); + } + LocalDateTime deadline = LocalDateTime.parse(deadlineInfo[1], formatter); + Task task = new DeadlineTask(description, deadline); + return new AddCommand(task); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Task cannot be added. " + + "\nDeadline or description is missing."); + } catch (DateTimeParseException e) { + return new InvalidCommand("Task cannot be added. " + + "\nPlease enter deadline in the format of 'yyyy-MM-dd HHmm'"); + } + } + + private static Command doAddEvent(String[] args){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"); + try { + String[] eventInfo = args[1].split(" /at "); + String description = eventInfo[0]; + boolean descriptionMissing = eventInfo[0].isBlank() || eventInfo[0].isEmpty(); + if (descriptionMissing) { + return new InvalidCommand("Task cannot be added. " + + "\nDescription is missing."); + } + + String[] times = eventInfo[1].split(" -to "); + LocalDateTime start = LocalDateTime.parse(times[0], formatter); + LocalDateTime end = LocalDateTime.parse(times[1], formatter); + Task task = new EventTask(description, start, end); + return new AddCommand(task); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Task cannot be added. \n" + + "Event time or description is missing."); + } catch (DateTimeParseException e) { + return new InvalidCommand("Task cannot be added. \n" + + "Please enter event datetime in the format of 'yyyy-MM-dd HHmm -to yyyy-MM-dd HHmm'"); + } + } + + private static Command doComplete(String[] args){ + try { + int taskId = Integer.parseInt(args[1]); + return new CompleteCommand(taskId); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Task id is missing."); + } + } + + private static Command doDelete(String[] args){ + try { + int taskId = Integer.parseInt(args[1]); + return new DeleteCommand(taskId); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Task id is missing."); + } + } + + private static Command doFind(String[] args){ + try { + String keyword = args[1]; + return new FindCommand(keyword); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Search keyword is missing."); + } + } + + private static Command doView(String[] args) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + LocalDate date = LocalDate.parse(args[1], formatter); + return new ViewCommand(date); + } catch (IndexOutOfBoundsException e) { + return new InvalidCommand("Date is missing."); + } catch (DateTimeParseException e) { + return new InvalidCommand("Please enter the date in the format of 'yyyy-MM-dd'"); + } + } + + private static Command doExit(){ + return new ExitCommand(); + } +} diff --git a/src/main/java/storage/Storage.java b/src/main/java/storage/Storage.java new file mode 100644 index 00000000..9eeb0dc2 --- /dev/null +++ b/src/main/java/storage/Storage.java @@ -0,0 +1,172 @@ +package storage; + +import exceptions.DukeException; +import tasks.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.FormatStyle; +import java.util.*; + +/** + * A Storage object deals with loading tasks from the file and saving tasks in the file. + */ +public class Storage { + private final String filePath; + + /** + * Constructor of Storage. + * + * @param filePath Filepath of which the file to store tasks is located. + */ + public Storage(String filePath){ + this.filePath = filePath; + } + + /** + * Reads tasks stored in specific file and load them into a TaskList. + * Returns an ArrayList of Task objects. + * The filepath has to be specified when constructing the Storage object. + * + * @throws FileNotFoundException If file specified is not found. + */ + public ArrayList loadTasks() throws FileNotFoundException { + File taskFile = new File(filePath); + boolean fileExists = taskFile.exists(); + + if (fileExists) { + Scanner s = new Scanner(taskFile); + ArrayList tasks = new ArrayList<>(); + + while (s.hasNext()) { + String taskStr = s.nextLine(); + try { + Task newTask = convertToTask(taskStr); + tasks.add(newTask); + } catch (Exception e){ + System.out.println(e.getMessage()); + } + } + return tasks; + } else { + throw new FileNotFoundException(); + } + } + + /** + * Creates directory and file at specific filepath for storing tasks. + * The filepath has to be specified when constructing the Storage object. + * + * @throws IOException If the creation of file or directory is unsuccessful. + */ + public void init() throws IOException { + File tasks = new File(filePath); + + String directory = tasks.getParent(); + File folders = new File(directory); + if (!folders.exists()) { + folders.mkdirs(); + } + tasks.createNewFile(); + } + + /** + * Returns a Task object converted from the input string. + * + * @param taskStr Task in string format. + */ + private Task convertToTask(String taskStr) throws DukeException { + assert taskStr != null: "taskStr must be valid"; + + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT); + String[] args = taskStr.split(" \\| "); + + String keyword = args[0]; + String status = args[1]; + String description = args[2]; + + Task task = null; + switch (keyword) { + case "T": { + task = new ToDoTask(description); + break; + } + case "D": + assert args[3] != null: "deadline value cannot be null"; + try { + LocalDateTime deadline = LocalDateTime.parse(args[3], formatter); + task = new DeadlineTask(description, deadline); + } catch (DateTimeParseException e){ + throw new DukeException("One or more Task(s) cannot be loaded due to invalid format."); + } + break; + + case "E": + assert args[3] != null: "start datetime value cannot be null"; + assert args[4] != null: "start datetime value cannot be null"; + try { + LocalDateTime start = LocalDateTime.parse(args[3], formatter); + LocalDateTime end = LocalDateTime.parse(args[4], formatter); + task = new EventTask(description, start, end); + } catch (DateTimeParseException e) { + throw new DukeException("One or more Task(s) cannot be loaded due to invalid format."); + } + break; + } + if (status.equals("1")) { task.markAsDone(); } + return task; + } + + /** + * Saves the list of tasks into specific file. + * The filepath has to be specified when constructing the Storage object. + * + * @param tasks ArrayList of Task objects to be saved. + * @throws IOException If the file writing/ saving operation is unsuccessful. + */ + public void saveTasks(ArrayList tasks) throws IOException { + FileWriter fw = new FileWriter(filePath); + + try { + for (Task task : tasks) { + String taskType = task.getClass().getSimpleName(); + + String taskStr = null; + String description = task.getDescription(); + String status = task.isDone() ? "1" : "0"; + + switch (taskType) { + case "ToDoTask": + taskStr = "T | " + status + " | " + description + "\n"; + break; + + case "DeadlineTask": + DeadlineTask deadlineTask = (DeadlineTask)task; + taskStr = "D | " + status + " | " + description + " | " + deadlineTask.getBy() + "\n"; + break; + + case "EventTask": + EventTask eventTask = (EventTask)task; + taskStr = "E | " + status + " | " + description + " | " + eventTask.getStart() + + " | " + eventTask.getEnd() + "\n"; + break; + } + fw.write(taskStr); + } + fw.close(); + } catch (IOException e){ + System.out.println(e.getMessage()); + } + } + + /** + * Returns the filepath of current Storage object. + */ + public String getFilePath() { + return filePath; + } +} diff --git a/src/main/java/tasks/DeadlineTask.java b/src/main/java/tasks/DeadlineTask.java new file mode 100644 index 00000000..39456d26 --- /dev/null +++ b/src/main/java/tasks/DeadlineTask.java @@ -0,0 +1,37 @@ +package tasks; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +/** + * A DeadLineTask object stores a task with deadline. + * * Extends the Task class. + */ +public class DeadlineTask extends Task { + + protected LocalDateTime deadline; + + /** + * Constructor of DeadLineTask. + * + * @param description Brief description of the task. + * @param deadline Deadline of the task. + */ + public DeadlineTask(String description, LocalDateTime deadline) { + super(description); + this.deadline = deadline; + } + + /** + * Returns the deadline of the DeadLineTask object. + */ + public String getBy() { + return deadline.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)); + } + + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + getBy() + ")"; + } +} diff --git a/src/main/java/tasks/EventTask.java b/src/main/java/tasks/EventTask.java new file mode 100644 index 00000000..93dde2f6 --- /dev/null +++ b/src/main/java/tasks/EventTask.java @@ -0,0 +1,54 @@ +package tasks; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +/** + * A EventTask object stores a task (event) with date time. + * Extends the Task class. + */ +public class EventTask extends Task { + + protected LocalDateTime start; + protected LocalDateTime end; + + /** + * Constructor of EventTask. + * + * @param description Brief description of the task. + * @param start Start DateTime of the task (event). + * @param end End DateTime of the task (event). + */ + public EventTask(String description, LocalDateTime start, LocalDateTime end) { + super(description); + this.start = start; + this.end = end; + } + + /** + * Returns the event start date time of the EventTask object. + */ + public String getStart() { + return start.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)); + } + + /** + * Returns the event end date time of the EventTask object. + */ + public String getEnd() { + return end.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)); + } + + /** + * Returns the event date time (period) of the EventTask object. + */ + public String getAt() { + return getStart() + " - " + getEnd(); + } + + @Override + public String toString() { + return "[E]" + super.toString() + " (at: " + getAt() + ")"; + } +} diff --git a/src/main/java/tasks/Scheduler.java b/src/main/java/tasks/Scheduler.java new file mode 100644 index 00000000..dea90f23 --- /dev/null +++ b/src/main/java/tasks/Scheduler.java @@ -0,0 +1,252 @@ +package tasks; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; + +/** + * A Scheduler object stores the start and end datetime of events. + * It will check for clashes before an event is added to the task list. + * Return true if the new event can be added and false otherwise. + */ +public class Scheduler { + + private final ArrayList deadlines; + private final ArrayList events; + + public Scheduler(){ + deadlines = new ArrayList<>(); + events = new ArrayList<>(); + } + + /** + * Returns after adding new deadline. + * The core scheduling function. + * + * @param deadlineTask New deadline. + */ + public void scheduleDeadline(DeadlineTask deadlineTask) { + LocalDateTime deadline = deadlineTask.deadline; + boolean scheduleIsEmpty = deadlines.size() == 0; + if (scheduleIsEmpty) { + deadlines.add(deadlineTask); + return; + } + int index = searchForNewDeadlineSlot(deadline); + deadlines.add(index, deadlineTask); + return; + } + + /** + * Returns true if the event is scheduled, false if the event cannot be added. + * The core scheduling function. + * + * @param event New event. + */ + public boolean scheduleEvent(EventTask event) { + LocalDateTime start = event.start; + LocalDateTime end = event.end; + + // start time is later than end time + if (start.isAfter(end)) { + return false; + } + + // no event in the schedule + boolean scheduleIsEmpty = events.size() == 0; + if (scheduleIsEmpty) { + events.add(event); + return true; + } + + boolean startLate = start.isAfter(events.get(events.size()-1).start) || + start.isEqual(events.get(events.size()-1).start); + boolean startAfterLastEvent = start.isAfter(events.get(events.size()-1).end) || + start.isEqual(events.get(events.size()-1).end) ; + boolean startEarly = start.isBefore(events.get(0).start) || + start.isEqual(events.get(0).start); + boolean endBeforeFirstEvent = end.isBefore(events.get(0).start) || + end.isEqual(events.get(0).start); + + if (startLate) { + if (startAfterLastEvent) { + events.add(event); + return true; + } + } else if (startEarly) { + if (endBeforeFirstEvent){ + events.add(0,event); + return true; + } + } else { //start somewhere in the middle + int index = searchForNewEventSlot(start); + if (index == -1) { //clashed + return false; + } + boolean startNotClash = start.isAfter(events.get(index - 1).end) + || start.isEqual(events.get(index - 1).end) ; + boolean endNotClash = end.isBefore(events.get(index).start) || + end.isEqual(events.get(index).start); + if (startNotClash && endNotClash) { + events.add(index, event); + return true; + } + } + return false; + } + + /** + * Returns -1 if there is time clashes, else returns the index of the slot. + * The helper function to binary search for slot based on event start time. + * + * @param start Start DateTime of the new event. + */ + private int searchForNewEventSlot(LocalDateTime start){ + int left = 0; + int right = events.size() - 1; + + while (left < right){ + int mid = (left + right) / 2; + if ((events.get(mid).start).equals(start)) { + return -1; + } else if ((events.get(mid).start).isAfter(start)) { + right = mid; + } else { + left = mid + 1; + } + } + return right; + } + + /** + * Returns index+1 if there is same deadline timing, else returns the index of the desired slot. + * The helper function to binary search for slot based on event start time. + * + * @param deadline Start DateTime of the new event. + */ + private int searchForNewDeadlineSlot(LocalDateTime deadline){ + int left = 0; + int right = deadlines.size() - 1; + + while (left < right){ + int mid = (left + right) / 2; + if ((deadlines.get(mid).deadline).equals(deadline)) { + return mid+1; + } else if ((deadlines.get(mid).deadline).isAfter(deadline)) { + right = mid; + } else { + left = mid + 1; + } + } + return right; + } + + /** + * Loads schedule of existing events. + * + * @param tasks List of tasks loaded from existing tasks file. + */ + public void loadSchedule(ArrayList tasks){ + for (Task task: tasks) { + boolean isDeadline = task.getClass().equals(DeadlineTask.class); + boolean isEvent = task.getClass().equals(EventTask.class); + + if (isEvent) { + EventTask event = (EventTask) task; + scheduleEvent(event); + } + if (isDeadline) { + DeadlineTask deadline = (DeadlineTask) task; + scheduleDeadline(deadline); + } + } + } + + /** + * Returns -1 if the event is not found, else return the index of the event. + * Another helper function to binary search for slot based on event start time. + * + * @param event Event to look up for. + */ + private int retrieveEventSlot(EventTask event){ + int left = 0; + int right = events.size() - 1; + + while (left <= right){ + int mid = (left + right) / 2; + if (events.get(mid).equals(event)) { + return mid; + } else if ((events.get(mid).start).isAfter(event.start)) { + right = mid - 1; + } else { + left = mid + 1; + } + } + return -1; + } + + /** + * Returns -1 if the deadline is not found, else return the index of the deadline. + * Another helper function to binary search for slot based on event start time. + * + * @param deadline Deadline to look up for. + */ + private int retrieveDeadlineSlot(DeadlineTask deadline){ + int left = 0; + int right = deadlines.size() - 1; + + while (left <= right){ + int mid = (left + right) / 2; + if (deadlines.get(mid).equals(deadline)) { + return mid; + } else if ((deadlines.get(mid).deadline).isAfter(deadline.deadline)) { + right = mid - 1; + } else { + left = mid + 1; + } + } + return -1; + } + + /** + * Removes a particular event from the schedule (clear the slot). + * + * @param event Event to be removed. + */ + public void freeUpEventSlot(EventTask event){ + int index = retrieveEventSlot(event); + assert index != -1: "The event must be in the schedule"; + events.remove(index); + } + + /** + * Removes a particular deadline from the schedule (clear the slot). + * + * @param deadline Deadline to be removed. + */ + public void freeUpDeadlineSlot(DeadlineTask deadline){ + int index = retrieveDeadlineSlot(deadline); + assert index != -1: "The deadline must be in the schedule"; + deadlines.remove(index); + } + + /** + * Returns the list of events and deadlines scheduled on the input date. + * + * @param date Date of schedule. + */ + public ArrayList getSchedule(LocalDate date) { + ArrayList tasks = new ArrayList<>(); + for (int i = 0; i < deadlines.size(); i++) { + if((deadlines.get(i).deadline).toLocalDate().equals(date)){ + tasks.add(deadlines.get(i)); + } + } + for (int i = 0; i < events.size(); i++) { + if((events.get(i).start).toLocalDate().equals(date)){ + tasks.add(events.get(i)); + } + } + return tasks; + } +} diff --git a/src/main/java/tasks/Task.java b/src/main/java/tasks/Task.java new file mode 100644 index 00000000..6a3c5ad3 --- /dev/null +++ b/src/main/java/tasks/Task.java @@ -0,0 +1,37 @@ +package tasks; + +/** + * A Task is a high-level Task class to be extended. + */ +public class Task { + + protected String description; + protected boolean isDone; + + /** + * Constructor of Task. + * + * @param description Brief description of the task. + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** Returns the description of the task. */ + public String getDescription() { return description; } + + /** Returns the status flag of the task. */ + public String getStatusIcon() { return (isDone ? "X" : " "); } + + /** Returns the status of the task. */ + public boolean isDone() { return isDone; } + + /** Set the status of the task to "done". */ + public void markAsDone() { isDone = true; } + + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + getDescription(); + } +} diff --git a/src/main/java/tasks/TaskList.java b/src/main/java/tasks/TaskList.java new file mode 100644 index 00000000..07cf8fb2 --- /dev/null +++ b/src/main/java/tasks/TaskList.java @@ -0,0 +1,140 @@ +package tasks; + +import exceptions.*; + +import java.time.LocalDate; +import java.util.ArrayList; + +/** + * A TaskList object stores all available tasks (to-dos, deadlines, events). + */ +public class TaskList { + private final ArrayList taskList; + private final Scheduler scheduler; + + public TaskList() { + taskList = new ArrayList<>(); + scheduler = new Scheduler(); + } + + /** + * Custom constructor of TaskList. + * To be used when loading tasks from an existing record. + * + * @param tasks ArrayList of Task objects to be loaded. + */ + public TaskList(ArrayList tasks) { + taskList = tasks; + scheduler = new Scheduler(); + scheduler.loadSchedule(tasks); + } + + /** Returns the list size (task count) of the TaskList. */ + public int getListSize() { return taskList.size(); } + + /** + * Gets task by id and sets the status of the task to "done". + * Returns the completed task in String format. + * + * @param taskId ID of the completed task. + * @throws DukeException If task with the specific ID is not found. + */ + public String setDone(int taskId) throws DukeException { + int taskIndex = taskId - 1; + boolean isOutOfRange = taskId > taskList.size() || taskId < 1; + + if (isOutOfRange) { + throw new DukeException("Task with id " + taskId + " is not found."); + } + Task doneTask = taskList.get(taskIndex); + doneTask.markAsDone(); + return doneTask.toString(); + } + + /** + * Gets task by id and deletes the task. + * Returns the deleted task in String format. + * + * @param taskId ID of the task to be deleted. + * @throws DukeException If task with the specific ID is not found. + */ + public String deleteTask(int taskId) throws DukeException { + boolean isOutOfRange = taskId > taskList.size() || taskId < 1; + + if (isOutOfRange) { + throw new DukeException("Task with id " + taskId + " is not found."); + } + Task deletedTask = taskList.get(taskId - 1); + taskList.remove(taskId - 1); + + boolean isEvent = deletedTask.getClass().equals(EventTask.class); + boolean isDeadline= deletedTask.getClass().equals(DeadlineTask.class); + + if (isEvent) { + scheduler.freeUpEventSlot((EventTask) deletedTask); + } else if (isDeadline) { + scheduler.freeUpDeadlineSlot((DeadlineTask) deletedTask); + } + return deletedTask.toString(); + } + + /** + * Adds a new task into the TaskList. + * + * @param task Task to be added. + */ + public boolean addTask(Task task) { + boolean isDeadline = task.getClass().equals(DeadlineTask.class); + boolean isEvent = task.getClass().equals(EventTask.class); + boolean canBeAdded = true; + + if (isDeadline) { + DeadlineTask deadline= (DeadlineTask) task; + scheduler.scheduleDeadline(deadline); + } + + if (isEvent) { + EventTask event = (EventTask) task; + canBeAdded = scheduler.scheduleEvent(event); + } + if (canBeAdded) { + taskList.add(task); + } + return canBeAdded; + } + + /** + * Searches whether a task with input keyword exists in the TaskList. + * + * @param keyword Search keyword. + */ + public ArrayList searchTask(String keyword) { + assert keyword != null: "The search keyword cannot be null at this point"; + ArrayList results = new ArrayList<>(); + + for (Task task: taskList) { + String description = task.description.toLowerCase(); + if (description.contains(keyword.toLowerCase())) { + results.add(task); + } + } + return results; + } + + /** + * Returns all tasks in the TaskList. + */ + public ArrayList getTasks() { + return taskList; + } + + /** + * Returns all tasks in the TaskList scheduled on the input date. + * + * @param date User input date. + */ + public ArrayList getTaskSchedule(LocalDate date) { + ArrayList schedule = scheduler.getSchedule(date); + return schedule; + } +} diff --git a/src/main/java/tasks/ToDoTask.java b/src/main/java/tasks/ToDoTask.java new file mode 100644 index 00000000..431dfb7e --- /dev/null +++ b/src/main/java/tasks/ToDoTask.java @@ -0,0 +1,22 @@ +package tasks; + +/** + * A ToDoTask object stores a task without any specific date and time. + * Extends the Task class. + */ +public class ToDoTask extends Task { + + /** + * Constructor of ToDoTask. + * + * @param description Brief description of the task. + */ + public ToDoTask(String description) { + super(description); + } + + @Override + public String toString() { + return "[T]" + super.toString(); + } +} diff --git a/src/main/java/ui/Messages.java b/src/main/java/ui/Messages.java new file mode 100644 index 00000000..cad822e2 --- /dev/null +++ b/src/main/java/ui/Messages.java @@ -0,0 +1,64 @@ +package ui; + +import tasks.Task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; + +/** + * Class for declaring messages to display on Duke GUI. + */ +public final class Messages { + + public static final String WELCOME_MSG = "Hello, I'm Duke!\n" + "What can I do for you?\n" + + "Ps: Type 'help' for more information.\n"; + + public static final String HELP_MSG = "View all existing tasks: list\n" + + "Add a new to-do: todo {taskDescription}\n" + + "Add a new deadline: deadline {taskDescription} /by {yyyy-MM-dd HHmm}\n" + + "Add a new event: event {taskDescription} /at {yyyy-MM-dd HHmm} -to {yyyy-MM-dd HHmm}\n" + + "Mark a task as completed: done {taskId}\n" + + "Delete a task: delete {taskId}\n" + + "Fine task by keyword: find {keyword}\n" + + "Check schedule of a date: view {yyyy-MM-dd}\n" + + "Exit the program: bye\n"; + + public static final String LOADING_ERROR = "No record found. Creating new file...\n"; + + public static final String ADDED_MSG = "Got it! I've added this task:\n"; + + public static final String COMPLETED_MSG = "Amazing! The task is marked as done now:\n"; + + public static final String DELETED_MSG = "Noted. I've removed this task:\n"; + + public static final String LISTED_MSG = "Here are the task(s) in your list:\n"; + + public static final String SEARCH_COMPLETED_MSG = "Here are the matching task(s) in your list:\n"; + + public static final String EXIT_MSG = "Bye. Hope to see you again soon!\n"; + + public static String getScheduledMsg(LocalDate date){ + return "Here are the task(s) scheduled on " + + date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) + ":\n"; + } + + public static String getTaskCountMsg(int listSize) { return "Now you have " + listSize + " task(s) in the list\n"; } + + public static String getTaskMsg(String task) { return task + "\n"; } + + public static String getAllTasksMsg(ArrayList taskList) { + if (taskList.size() == 0){ + return "There's no task now :D\n"; + } else { + String taskStr = ""; + int index = 1; + for (Task task : taskList) { + taskStr += index + ". " + task + "\n"; + index++; + } + return taskStr; + } + } +} diff --git a/src/main/java/ui/Ui.java b/src/main/java/ui/Ui.java new file mode 100644 index 00000000..ce1fb3cb --- /dev/null +++ b/src/main/java/ui/Ui.java @@ -0,0 +1,123 @@ +package ui; + +import tasks.Task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * An Ui object deals with interactions with the user. + * Prints response in the Terminal based on user's inputs. + */ +public class Ui { + private final Scanner sc = new Scanner(System.in); + + /** Prints Duke logo and Hello message. */ + public void showWelcome() { + String logo = " ____ _ \n" + + "| _ \\ _ _| | _____ \n" + + "| | | | | | | |/ / _ \\\n" + + "| |_| | |_| | < __/\n" + + "|____/ \\__,_|_|\\_\\___|\n"; + System.out.println(logo); + System.out.println("____________________________________________________________________________________"); + System.out.println("Hello, I'm Duke!" ); + System.out.println("What can I do for you?"); + System.out.println("Ps: Type 'help' for more information."); + System.out.println("____________________________________________________________________________________"); + } + + /** Prints separator line. */ + public void showLine() { + System.out.println("____________________________________________________________________________________"); + } + + /** Prints all command instructions. */ + public void showHelp(){ + System.out.println("View all existing tasks: list"); + System.out.println("Add a new to-do: todo {description}"); + System.out.println("Add a new deadline: deadline {description} /by {yyyy-MM-dd HHmm}"); + System.out.println("Add a new event: event {description} /at {yyyy-MM-dd HHmm} -to {yyyy-MM-dd HHmm}"); + System.out.println("Mark a task as completed: done {taskId}"); + System.out.println("Delete a task: delete {taskId}"); + System.out.println("Fine task by keyword: find {keyword}"); + System.out.println("Check schedule of a date: view {yyyy-MM-dd}"); + System.out.println("Exit the program: bye"); + } + + /** Reads user's input command. */ + public String readCommand() { + return sc.nextLine(); + } + + /** Prints loading error message (file not found). */ + public void showLoadingError() { + System.out.println("No record found. Creating new file..."); + } + + /** + * Prints Duke error message. + * + * @param message Error message to be printed. + */ + public void showError(String message){ + System.out.println(message); + } + + /** Prints task added successful message. */ + public void showAdded() { System.out.println("Got it! I've added this task:"); } + + /** Prints task completed successful message. */ + public void showCompleted() { System.out.println("Amazing! The task is marked as done now:"); } + + /** Prints task deleted successful message. */ + public void showDeleted() { System.out.println("Noted. I've removed this task:"); } + + /** Prints tasks listed message. */ + public void showListed() { System.out.println("Here are the task(s) in your list:"); } + + /** Prints search completed message. */ + public void showSearchCompleted() { System.out.println("Here are the matching task(s) in your list:"); } + + /** Prints exit message. */ + public void showExit() { System.out.println("Bye. Hope to see you again soon!"); } + + public void showScheduleFor(LocalDate date) { + System.out.println("Here are the task(s) scheduled on " + + date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) + ":"); + } + + /** + * Prints current task count. + * + * @param listSize Number of tasks in the list + */ + public void printTaskCount(int listSize) { System.out.println("Now you have " + listSize + " task(s) in the list"); } + + /** + * Prints single task. + * + * @param task Task to be printed + */ + public void printTask(String task) { System.out.println(task); } + + /** + * Prints all tasks in the list. + * + * @param taskList ArrayList of Tasks to be printed + */ + public void printAllTasks(ArrayList taskList) { + if (taskList.size() == 0){ + System.out.println("There's no task now :D"); + } else { + int index = 1; + for (Task task : taskList) { + System.out.println(index + ". " + task); + index++; + } + } + } +} diff --git a/src/main/resources/css/stylesheet.css b/src/main/resources/css/stylesheet.css new file mode 100644 index 00000000..f90eff46 --- /dev/null +++ b/src/main/resources/css/stylesheet.css @@ -0,0 +1,10 @@ +.label { + -fx-background-color: #C4F1F2; + -fx-padding: 15px; + -fx-background-insets: 5px; + -fx-background-radius: 5px; +} +.button { + -fx-background-color: #007BFF; + -fx-text-fill: white; +} \ No newline at end of file diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 00000000..d8936587 Binary files /dev/null and b/src/main/resources/images/DaDuke.png differ diff --git a/src/main/resources/images/DaUser.png b/src/main/resources/images/DaUser.png new file mode 100644 index 00000000..3c82f454 Binary files /dev/null and b/src/main/resources/images/DaUser.png differ diff --git a/src/main/resources/views/DialogBox.fxml b/src/main/resources/views/DialogBox.fxml new file mode 100644 index 00000000..e5aeb403 --- /dev/null +++ b/src/main/resources/views/DialogBox.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/views/MainWindow.fxml b/src/main/resources/views/MainWindow.fxml new file mode 100644 index 00000000..c90ec43d --- /dev/null +++ b/src/main/resources/views/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +