diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..0fd214b11a --- /dev/null +++ b/build.gradle @@ -0,0 +1,60 @@ +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.Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.29' +} + +run{ + standardInput = System.in +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..7c8adb24c1 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/.gitignore b/data/.gitignore new file mode 100644 index 0000000000..e7a210ec7d --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,3 @@ +* +*/ +!.gitignore \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index fd44069597..c7664c3bcf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,19 +2,178 @@ ## Features -### Feature 1 -Description of feature. +### Basic Add/Delete/Done features + +Simple addition, deletion, of task, and the ability to mark them as done + +### Continuous Saving + +Duke will continuously save your current list of tasks as you work. + +### Search tasks + +Search tasks with a intuitive `search` command. + +### Different types of tasks to handle different time information + +Simple `todo`, free-form `event`, and easy `deadline` ## Usage -### `Keyword` - Describe action -Describe action and its outcome. +### `todo`/`event`/`deadline` - Add tasks + +Adds a task with the corresponding type to the end of the list + +Usage: + +``` +todo (Description) +event (Description) /at (Event Time) +deadline (Description) /by (Deadline*) +``` +\* The deadline format has special syntax. See below for details. + +Example of usage: + +`event blah /at end of the world` + +Expected response: + +``` +The following task has been added: +Entry 1|[E][ ]: blah (Event Time: end of the world) +``` +Example of usage: + +`todo See the World` + +Expected response: + +``` +The following task has been added: +Entry 2|[T][ ]: See the World +``` + +Example of usage: + +`deadline Surprise /by 20 02 20201` + +Expected response: + +``` +The following task has been added: +Entry 3|[D][ ]: Surprise (Deadline: February 20, 2021) +``` + +#### Deadline date format + +Keywords such as `yesterday`, `today`/`now`, and `tmr`/`tomorrow`, will create a deadline with the appropriate date. + +Days of the week from `Monday`, `Tuesday` ... `Sunday`, will create a deadline for the next date with that day, excluding today. + +Eg: Today is Friday, so `Friday` will result in a date exactly one week from now. + +There is also absolute dates, which must strictly follow the format of `DD MM YYYY`. So `07 03 2020` will be accepted, but **not** `7 03 2020`, `07 3 2020`, or `07 03 20`. + + +### `ls`/`list` - List all tasks + +List all tasks in order, showing details and "done" status. + +Example of usage: + +`list` + +Example outcome: + +``` +Entry 1|[E][*]: blah (Event Time: end of the world) +Entry 2|[T][ ]: See the World +Entry 3|[D][ ]: Surprise (Deadline: February 20, 2021) +``` + +### `rm`/`del`/`remove`/`delete` - Remove task + +Removes a task based on its list index. + +Example of usage: + +`rm 2` + +Expected outcome: + +``` +The following Task has been deleted: +Entry 3|[D][ ]: Surprise (Deadline: February 20, 2021) +``` + +### `done` - Mark task as done + +Marks a task as completed. + +Example of usage: + +`done 2` + +Expected outcome: + +``` +The following task is now marked as done: +Entry 2|[T][*]: See the World +``` + +The resultant list should be as follows: +``` +Entry 1|[E][*]: blah (Event Time: end of the world) +Entry 2|[T][*]: See the World +``` + + +### `find`/`search` - Describe action + +Searches tasks based on a search string + +Suppose our current list looks like this: + +``` +Entry 1|[E][*]: blah (Event Time: end of the world) +Entry 2|[T][*]: See the World +Entry 3|[T][ ]: see tokyo +``` + +Example of usage: +`find see` + +Expected outcome: + +``` +Matching Task(s): +Entry 2|[T][*]: See the World +Entry 3|[T][ ]: see tokyo +``` +#### More detailed search behaviour: + +##### Default search behaviour +By default, Duke matches from the beginning of each word in a task, so `find he` would match "**He**ro of the sea" but not "T**he** tales of Genji" + +This is *disabled* if you have a multi word search, like `find he tale`, which triggers a substring search, and would successfully match "T**he tale**s of Genji" + +##### Case insensitivity +When searching in lowercase, Eg: `find see`, we ignore case and match both "**see** the world", and "**See** Tokyo" + +To disable this, search with at least one capital letter, Eg: `find See`, which will match only "**See** Tokyo". + +Note that a single capital letter will make the entire search case sensitive. So `find See`, will **not** match "**SEE** HOW TO GO FAST" + +### `exit`/`bye` - Exit duke + +Exits duke. Example of usage: -`keyword (optional arguments)` +`exit` Expected outcome: -`outcome` +Duke exits. :) \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..9f9a006b1e Binary files /dev/null and b/docs/Ui.png differ diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..c4192631f2 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..f3d88b1c2f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..b7c8c5dbf5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/duke/Duke.java b/src/main/java/duke/Duke.java new file mode 100644 index 0000000000..9270f75efb --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,117 @@ +package duke; + +import java.io.IOException; +import java.text.ParseException; + +import duke.command.Command; +import duke.exception.BadDateArgumentException; +import duke.exception.BadIndexException; +import duke.exception.EmptyArgumentException; +import duke.exception.InvalidCommandException; + + +public class Duke { + public static final String FILE_DIR = "data"; + public static final String FILE_NAME = "duke.txt"; + private final Ui ui; + private final Storage storage; + private TaskList taskList; + private boolean isTerminated; + + /** + * Constructor for Duke + */ + public Duke() { + ui = new Ui(); + storage = new Storage(FILE_DIR, FILE_NAME); + isTerminated = false; + } + public boolean getIsTerminated() { + return isTerminated; + } + + /** + * Handle Startup Procedure, which includes messages and loading from file. + * + * @return dialog for startup. + */ + public String startUpProcedure() { + ui.printStartUp(); + try { + ui.printLoadStart(); + taskList = storage.loadTaskList(); + ui.printLoadSuccess(); + } catch (IOException e) { + ui.printLoadFail(); + } + return ui.flushMessage(); + } + + /** + * Checks whether the command is a termination command. + * + * @param c Command to check + * @return Whether the command is a termination command + */ + private boolean isCommandTerminate(Command c) { + return c == null; + } + + /** + * The main application loop + * + * @param line The input to be processed + * @return Whether we can process new inputs + */ + public String processInput(String line) { + try { + Command c = Parser.parse(line); + if (isCommandTerminate(c)) { //Bye command + handleTermination(); + } else { + updateAndGenerateOutput(c); + } + } catch (ParseException e) { + ui.handleParsingError(e); + } catch (InvalidCommandException e) { + ui.handleInvalidCommand(e); + } catch (EmptyArgumentException e) { + ui.handleEmptyArgument(e); + } catch (BadDateArgumentException e) { + ui.handleBadDate(e); + } catch (BadIndexException e) { + ui.handleBadIndex(e); + } finally { + saveIfNeeded(); + } + return ui.flushMessage(); + } + private void updateAndGenerateOutput(Command c) + throws EmptyArgumentException, BadDateArgumentException, + InvalidCommandException, BadIndexException { + String data = taskList.run(c); + ui.printCommandMessage(c, data); + } + + /** + * Saves the content of taskList if it has been changed by the command + */ + private void saveIfNeeded() { + if (taskList.isEdited()) { + try { + storage.saveTaskList(taskList); + taskList.markSaved(); + } catch (IOException e) { + ui.dumpState(taskList); + } + } + } + + /** + * Handle Termination, including private variables and messages. + */ + private void handleTermination() { + isTerminated = true; + ui.generateShutDownMessage(); + } +} diff --git a/src/main/java/duke/Launcher.java b/src/main/java/duke/Launcher.java new file mode 100644 index 0000000000..e4ef6b4628 --- /dev/null +++ b/src/main/java/duke/Launcher.java @@ -0,0 +1,12 @@ +package duke; + +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/duke/Main.java b/src/main/java/duke/Main.java new file mode 100644 index 0000000000..acce71d318 --- /dev/null +++ b/src/main/java/duke/Main.java @@ -0,0 +1,28 @@ +package duke; + +import java.io.IOException; + +import duke.ui.MainWindow; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +public class Main extends Application { + + @Override + public void start(Stage stage) { + Duke d = new Duke(); + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + fxmlLoader.getController().setDuke(d); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/duke/Parser.java b/src/main/java/duke/Parser.java new file mode 100644 index 0000000000..ba01f4be71 --- /dev/null +++ b/src/main/java/duke/Parser.java @@ -0,0 +1,91 @@ +package duke; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; + +import duke.command.AddCommand; +import duke.command.Command; +import duke.command.DeleteCommand; +import duke.command.DoneCommand; +import duke.command.ListCommand; +import duke.command.SearchCommand; +import duke.exception.InvalidCommandException; + + +public class Parser { + /** + * Parses line of input for commands to the application + * + * @param line The line to be parsed + * @return Command to the application + * @throws ParseException If the line could not be reasonably interpreted + * @throws InvalidCommandException If there is an unknown instruction at the start + */ + public static Command parse(String line) throws ParseException, InvalidCommandException { + line = line.trim(); + String[] singleTokens = {"bye", "list", "exit", "ls"}; + String[] tokens = splitTokenIntoTwo(line, " ", singleTokens); + Command c = null; + switch (tokens[0].toLowerCase()) { + case "exit": + //Fall-through + case "bye": + //Leave as null + break; + case "ls": + //Fall-through + case "list": + c = new ListCommand(); + break; + case "done": + c = new DoneCommand(Integer.parseInt(tokens[1])); + break; + case "todo": + c = new AddCommand(new String[]{"T", tokens[1]}); + break; + case "deadline": + tokens = splitTokenIntoTwo(tokens[1], " /by "); + c = new AddCommand(new String[]{"D", tokens[0], tokens[1]}); + break; + case "event": + tokens = splitTokenIntoTwo(tokens[1], " /at "); + c = new AddCommand(new String[]{"E", tokens[0], tokens[1]}); + break; + case "rm": + //Fall-through + case "remove": + //Fall-through + case "del": + //Fall-through + case "delete": + c = new DeleteCommand(Integer.parseInt(tokens[1])); + break; + case "find": + //Fall-through + case "search": + c = new SearchCommand(tokens[1]); + break; + default: + throw new InvalidCommandException(tokens[0]); + } + return c; + } + private static String[] splitTokenIntoTwo(String parseTarget, String delimiter) throws ParseException { + String[] tokens = parseTarget.split(delimiter, 2); + if (tokens.length < 2) { + throw new ParseException("Expected delimiter '" + delimiter + "'", tokens[0].length()); + } + return tokens; + } + + private static String[] splitTokenIntoTwo(String parseTarget, String delimiter, String[] exception) + throws ParseException { + List exceptionsList = Arrays.asList(exception); + String[] tokens = parseTarget.split(delimiter, 2); + if (!exceptionsList.contains(tokens[0].toLowerCase()) && tokens.length < 2) { + throw new ParseException("Expected delimiter '" + delimiter + "'", tokens[0].length()); + } + return tokens; + } +} diff --git a/src/main/java/duke/Storage.java b/src/main/java/duke/Storage.java new file mode 100644 index 0000000000..31dedb1ccf --- /dev/null +++ b/src/main/java/duke/Storage.java @@ -0,0 +1,129 @@ +package duke; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import duke.exception.BadDateArgumentException; +import duke.exception.EmptyArgumentException; +import duke.task.Deadline; +import duke.task.Event; +import duke.task.Task; +import duke.task.ToDos; + +public class Storage { + private final String fileDir; + private final String fileName; + + /** + * Create new Storage interface with a particular folder path and file name. + * + * @param directory Relative directory path to store data in + * @param fileName Name of file to store data in + */ + public Storage(String directory, String fileName) { + this.fileDir = directory; + this.fileName = fileName; + } + + /** + * Attempts to parse a line from a file and load a corresponding Task. + * + * @param line The line to be parsed + * @return A task if successful, or null otherwise + */ + private Task parseLineFromFile(String line) { + Task t; + String pattern = "([TED]),([01]),(\\d*),(.*)"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(line); + if (!m.find()) { + return null; + } + String type = m.group(1); + boolean isDone = m.group(2).equals("1"); + int taskLength = Integer.parseInt(m.group(3)); + String task = m.group(4).substring(0, taskLength); + String leftover = m.group(4).substring(taskLength); + try { + if (type.equals("E") || type.equals("D")) { + line = leftover.substring(1); + pattern = "(\\d*),(.*)"; + r = Pattern.compile(pattern); + m = r.matcher(line); + if (!m.find()) { + return null; + } + int timeLength = Integer.parseInt(m.group(1)); + String timeData = m.group(2).substring(0, timeLength); + + switch (type) { + case "E": + t = new Event(task, timeData); + break; + case "D": + t = new Deadline(task, timeData); + break; + default: + return null; + } + } else { + t = new ToDos(task); + } + } catch (EmptyArgumentException | BadDateArgumentException e) { + return null; + } + if (isDone) { + t.setDone(); + } + return t; + } + + /** + * Loads data from a fixed constant, location relative to the program location + * + * @return TaskList that corresponds to the loaded data + * @throws IOException Uncontrollable IO Error + */ + public TaskList loadTaskList() throws IOException { + List taskList = new ArrayList<>(); + File file = getOrCreateFile(); + Scanner s = new Scanner(file); + while (s.hasNextLine()) { + Task t = parseLineFromFile(s.nextLine()); + if (t != null) { + taskList.add(t); + } + } + s.close(); + return new TaskList(taskList); + } + + /** + * Saves TaskList to disk. + * + * @param data TaskList to save to disk + * @throws IOException Unable to create subfolder + */ + public void saveTaskList(TaskList data) throws IOException { + String saveText = data.toFileString(); + File f = getOrCreateFile(); + FileWriter writer = new FileWriter(f); + writer.write(saveText); + writer.close(); + } + + private File getOrCreateFile() throws IOException { + Files.createDirectories(Paths.get(fileDir)); + File file = new File(fileDir, fileName); + file.createNewFile(); + return file; + } +} diff --git a/src/main/java/duke/TaskList.java b/src/main/java/duke/TaskList.java new file mode 100644 index 0000000000..1a54710fdd --- /dev/null +++ b/src/main/java/duke/TaskList.java @@ -0,0 +1,178 @@ +package duke; + +import java.util.List; +import java.util.stream.IntStream; + +import duke.command.Command; +import duke.exception.BadDateArgumentException; +import duke.exception.BadIndexException; +import duke.exception.EmptyArgumentException; +import duke.exception.InvalidCommandException; +import duke.task.Deadline; +import duke.task.Event; +import duke.task.Task; +import duke.task.ToDos; + +public class TaskList { + public enum Action { + ADD, + LIST, + DONE, + DELETE, + SEARCH, + } + private boolean edited = false; + private final List store; + public TaskList(List store) { + this.store = store; + } + /** + * Runs command on TaskList and returns command specific output. + * Side effects are present on some commands + * + * @param c Command to be run + * @return Output meant for Ui Class + * @throws EmptyArgumentException At least one argument is missing + * @throws BadDateArgumentException An argument that is expected to be a date is ill formatted + * @throws InvalidCommandException A bad command has been passed that cannot be handled + * @throws BadIndexException An index in the command is out of bounds and needs to be communicated + */ + public String run(Command c) + throws EmptyArgumentException, BadDateArgumentException, + InvalidCommandException, BadIndexException { + String[] args = c.getCommandParameters(); + String result; + Action action = c.getType(); + switch (action) { + case ADD: + result = addTask(args); + edited = true; + break; + case DONE: + checkIndex(Integer.parseInt(args[0])); + result = setDone(Integer.parseInt(args[0])); + edited = true; + break; + case DELETE: + checkIndex(Integer.parseInt(args[0])); + result = delete(Integer.parseInt(args[0])); + edited = true; + break; + case LIST: + result = getList(); + break; + case SEARCH: + result = getFilteredList(args[0]); + break; + default: + result = ""; + break; + } + return result; + } + + /** + * Mark the changes in the TaskList as saved to disk. + */ + public void markSaved() { + edited = false; + } + + /** + * Check whether TaskList has been edited from when it has been last saved to disk + * + * @return Whether the TaskList has changed + */ + public boolean isEdited() { + return this.edited; + } + private String addTask(String[] tokens) + throws EmptyArgumentException, BadDateArgumentException, InvalidCommandException { + generateTask(tokens); + int lastIndex = store.size() - 1; + return formatOrderedPrint(lastIndex); + } + private void generateTask(String[] tokens) + throws EmptyArgumentException, BadDateArgumentException, InvalidCommandException { + String type = tokens[0]; + String task = tokens[1]; + String additional = tokens.length >= 3 ? tokens[2] : null; + Task t; + switch (type) { + case "D": + assert tokens.length == 3; + t = new Deadline(task, additional); + break; + case "E": + assert tokens.length == 3; + t = new Event(task, additional); + break; + case "T": + assert tokens.length == 2; + t = new ToDos(task); + break; + default: + assert false + : "This assertion failed because an un-processable command has been received"; + throw new InvalidCommandException("of type " + type); + } + store.add(t); + } + private void checkIndex(int index) throws BadIndexException { + if (index < 0 || index >= store.size()) { + throw new BadIndexException(index); + } + } + private String setDone(int doneIndex) { + assert doneIndex >= 0 && doneIndex < store.size(); + Task t = store.get(doneIndex); + t.setDone(); + return formatOrderedPrint(doneIndex); + } + private String delete(int deleteIndex) { + assert deleteIndex >= 0 && deleteIndex < store.size(); + String returnValue = formatOrderedPrint(deleteIndex); + store.remove(deleteIndex); + return returnValue; + } + + /** + * Gets a user friendly list of all the task in TaskList + * + * @return User friendly state of TaskList + */ + public String getList() { + return IntStream.range(0, store.size()) + .parallel() + .mapToObj(this::formatOrderedPrint) + .map(str -> str + "\n") + .reduce("", (a, b) -> a + b); + } + + private String getFilteredList(String searchTerm) { + return IntStream.range(0, store.size()) + .parallel() + .filter(i -> store.get(i).containsSearch(searchTerm)) + .mapToObj(this::formatOrderedPrint) + .map(str -> str + "\n") + .reduce("", (a, b) -> a + b); + } + private String formatOrderedPrint(int i) { + return "Entry " + (i + 1) + "|" + store.get(i).toString(); + } + + /** + * Generates a string that represents the state of TaskList, + * such that the program can recreate it. + * + * @return A machine interpretable representation of TaskList + */ + public String toFileString() { + StringBuilder saveText = new StringBuilder(); + for (Task t: store) { + saveText.append(t.toFileString()); + saveText.append('\n'); + } + return saveText.toString(); + } +} diff --git a/src/main/java/duke/Ui.java b/src/main/java/duke/Ui.java new file mode 100644 index 0000000000..fdd4be5b28 --- /dev/null +++ b/src/main/java/duke/Ui.java @@ -0,0 +1,175 @@ +package duke; + +import java.text.ParseException; + +import duke.command.Command; +import duke.exception.BadDateArgumentException; +import duke.exception.BadIndexException; +import duke.exception.EmptyArgumentException; +import duke.exception.InvalidCommandException; + +public class Ui { + private static final String LOGO = + "The Wondrous Duke of Singapore(TM)\n" + + "(wipe your feet)\n"; + private static final String SEPARATOR = "------------------\n"; + private final StringBuilder builder; + + /** + * Ui constructor + */ + public Ui() { + builder = new StringBuilder(); + } + + /** + * Returns all the accumulated messages while flushing the buffer. + * + * @return The message to be printed + */ + public String flushMessage() { + String message = builder.toString(); + builder.setLength(0); + return message; + } + + /** + * Prints a start up message for when the program starts. + */ + public void printStartUp() { + builder.append(SEPARATOR); + builder.append("Hello from\n" + LOGO + "\n"); + builder.append("No unicode allowed" + "\n"); + builder.append(SEPARATOR); + } + + /** + * Prints a shutdown message for when the program ends. + */ + public void generateShutDownMessage() { + builder.append(SEPARATOR); + builder.append("Goodbye from\n" + LOGO); + builder.append(SEPARATOR); + } + + /** + * Indicate to the user that we are loading a file. + */ + public void printLoadStart() { + builder.append("Loading From File...\n"); + } + + /** + * Indicate to the user that we successfully loaded a file. + */ + public void printLoadSuccess() { + builder.append("Loaded\n"); + } + + /** + * Indicates to the user that the file could not be loaded and that we cannot continue. + */ + public void printLoadFail() { + builder.append("Failed to Load file. Aborting.\n"); + } + + /** + * Dumps the state of the task list visually in a manner suitable for the user + * to manually copy and save. + * + * @param store TaskList that needs to be dumped + */ + public void dumpState(TaskList store) { + builder.append("Unable to save list. Dumping ...\n"); + builder.append(store.getList()); + builder.append("Continuing Normal operation\n"); + } + + /** + * Generate and print message based on command and results from that command + * + * @param command The command that has been issued + * @param data The results of that command, in a pre-processed format + */ + public void printCommandMessage(Command command, String data) { + builder.append(SEPARATOR); + switch (command.getType()) { + case LIST: + builder.append(data); + break; + case DONE: + assert data.length() > 0; + builder.append("The following task is now marked as done:\n" + + data + "\n"); + break; + case ADD: + assert data.length() > 0; + builder.append("The following task has been added:\n" + + data + "\n"); + break; + case DELETE: + assert data.length() > 0; + builder.append("The following Task has been deleted:\n"); + builder.append(data + "\n"); + break; + case SEARCH: + if (data.length() > 0) { + builder.append("Matching Task(s):\n"); + builder.append(data + "\n"); + } else { + builder.append("No Matching Task has been found\n"); + } + break; + default: + assert false : "This Really should not happen ever"; + builder.append("ERROR: Unhandled Case!\n"); + } + builder.append(SEPARATOR); + } + + /** + * Generates an UI alert for parsing error + * + * @param e Exception that requires an error message + */ + public void handleParsingError(ParseException e) { + builder.append("Command has invalid parsing.\n"); + builder.append(e.getMessage() + "\n"); + } + /** + * Generates an UI alert for an invalid command. + * + * @param e Exception that requires an error message + */ + public void handleInvalidCommand(InvalidCommandException e) { + builder.append(e.getMessage() + "\n"); + } + /** + * Generates an UI alert for empty arguments. + * + * @param e Exception that requires an error message + */ + public void handleEmptyArgument(EmptyArgumentException e) { + builder.append("Cannot have empty argument\n"); + } + /** + * Generates an UI alert for bad date formatting. + * + * @param e Exception that requires an error message + */ + public void handleBadDate(BadDateArgumentException e) { + builder.append("Date must be one of the following:\n"); + builder.append(" Special keyword: Yesterday, Today/Now, Tomorrow/Tmr"); + builder.append(" Days of the week; Eg: Mon/M/Mo/Monday"); + builder.append(" 'dd MM yyyy'; Eg: 27 08 2044\n"); + } + + /** + * Generates an UI alert for an out of bounds error + * + * @param e Exception that requires an error message + */ + public void handleBadIndex(BadIndexException e) { + builder.append(e.getMessage()); + } +} diff --git a/src/main/java/duke/command/AddCommand.java b/src/main/java/duke/command/AddCommand.java new file mode 100644 index 0000000000..c6f22cf4ae --- /dev/null +++ b/src/main/java/duke/command/AddCommand.java @@ -0,0 +1,27 @@ +package duke.command; + +import duke.TaskList.Action; + +public class AddCommand extends Command { + private final String[] args; + + /** + * Create new Add command + * + * @param args Parameters describing what to add + */ + public AddCommand(String[] args) { + assert args.length > 1 : "Need at least the type and description"; + this.args = args; + } + + @Override + public String[] getCommandParameters() { + return args; + } + + @Override + public Action getType() { + return Action.ADD; + } +} diff --git a/src/main/java/duke/command/Command.java b/src/main/java/duke/command/Command.java new file mode 100644 index 0000000000..342e69ceac --- /dev/null +++ b/src/main/java/duke/command/Command.java @@ -0,0 +1,8 @@ +package duke.command; + +import duke.TaskList; + +public abstract class Command { + public abstract String[] getCommandParameters(); + public abstract TaskList.Action getType(); +} diff --git a/src/main/java/duke/command/DeleteCommand.java b/src/main/java/duke/command/DeleteCommand.java new file mode 100644 index 0000000000..5f408b14cb --- /dev/null +++ b/src/main/java/duke/command/DeleteCommand.java @@ -0,0 +1,19 @@ +package duke.command; + +import duke.TaskList.Action; + +public class DeleteCommand extends IndexedCommand { + /** + * Create command to delete Task from TaskList + * + * @param position One index position in TaskList of Task to delete + */ + public DeleteCommand(int position) { + super(position); + } + + @Override + public Action getType() { + return Action.DELETE; + } +} diff --git a/src/main/java/duke/command/DoneCommand.java b/src/main/java/duke/command/DoneCommand.java new file mode 100644 index 0000000000..39378e2629 --- /dev/null +++ b/src/main/java/duke/command/DoneCommand.java @@ -0,0 +1,19 @@ +package duke.command; + +import duke.TaskList.Action; + +public class DoneCommand extends IndexedCommand { + /** + * Create command to mark Task in TaskList as done + * + * @param position One index position in TaskList of Task to mark as done + */ + public DoneCommand(int position) { + super(position); + } + + @Override + public Action getType() { + return Action.DONE; + } +} diff --git a/src/main/java/duke/command/IndexedCommand.java b/src/main/java/duke/command/IndexedCommand.java new file mode 100644 index 0000000000..ee0eb0cac1 --- /dev/null +++ b/src/main/java/duke/command/IndexedCommand.java @@ -0,0 +1,19 @@ +package duke.command; + +abstract class IndexedCommand extends Command { + private final int index; + + /** + * Create command to manipulate Task in TaskList based on position index + * + * @param position One index position in TaskList of Task to manipulate + */ + public IndexedCommand(int position) { + this.index = position - 1; + } + + @Override + public String[] getCommandParameters() { + return new String[]{String.valueOf(index)}; + } +} diff --git a/src/main/java/duke/command/ListCommand.java b/src/main/java/duke/command/ListCommand.java new file mode 100644 index 0000000000..d20412b013 --- /dev/null +++ b/src/main/java/duke/command/ListCommand.java @@ -0,0 +1,16 @@ +package duke.command; + +import duke.TaskList.Action; + +public class ListCommand extends Command { + + @Override + public String[] getCommandParameters() { + return new String[0]; + } + + @Override + public Action getType() { + return Action.LIST; + } +} diff --git a/src/main/java/duke/command/SearchCommand.java b/src/main/java/duke/command/SearchCommand.java new file mode 100644 index 0000000000..7cb6e989d0 --- /dev/null +++ b/src/main/java/duke/command/SearchCommand.java @@ -0,0 +1,26 @@ +package duke.command; + +import duke.TaskList; + +public class SearchCommand extends Command { + private final String searchTerm; + + /** + * Create command to search for Task with a particular substring. + * + * @param searchTerm Case and whitespace sensitive search input + */ + public SearchCommand(String searchTerm) { + this.searchTerm = searchTerm; + } + + @Override + public String[] getCommandParameters() { + return new String[]{this.searchTerm}; + } + + @Override + public TaskList.Action getType() { + return TaskList.Action.SEARCH; + } +} diff --git a/src/main/java/duke/exception/BadDateArgumentException.java b/src/main/java/duke/exception/BadDateArgumentException.java new file mode 100644 index 0000000000..a8c415524b --- /dev/null +++ b/src/main/java/duke/exception/BadDateArgumentException.java @@ -0,0 +1,4 @@ +package duke.exception; + +public class BadDateArgumentException extends Exception { +} diff --git a/src/main/java/duke/exception/BadIndexException.java b/src/main/java/duke/exception/BadIndexException.java new file mode 100644 index 0000000000..09a5d4850d --- /dev/null +++ b/src/main/java/duke/exception/BadIndexException.java @@ -0,0 +1,18 @@ +package duke.exception; + +public class BadIndexException extends Exception { + private final int index; + + /** + * Indicates that a requested index is out of bounds. + * + * @param index Index that was requested + */ + public BadIndexException(int index) { + this.index = index; + } + @Override + public String getMessage() { + return "Index " + (index + 1) + " is out of bounds."; + } +} diff --git a/src/main/java/duke/exception/EmptyArgumentException.java b/src/main/java/duke/exception/EmptyArgumentException.java new file mode 100644 index 0000000000..0fa096b598 --- /dev/null +++ b/src/main/java/duke/exception/EmptyArgumentException.java @@ -0,0 +1,4 @@ +package duke.exception; + +public class EmptyArgumentException extends Exception { +} diff --git a/src/main/java/duke/exception/InvalidCommandException.java b/src/main/java/duke/exception/InvalidCommandException.java new file mode 100644 index 0000000000..54c19de9a6 --- /dev/null +++ b/src/main/java/duke/exception/InvalidCommandException.java @@ -0,0 +1,18 @@ +package duke.exception; + +public class InvalidCommandException extends Exception { + private final String badCommand; + + /** + * Create Exception to indicate an invalid command + * + * @param badCommand Invalid command fragment + */ + public InvalidCommandException(String badCommand) { + this.badCommand = badCommand; + } + @Override + public String getMessage() { + return "Command '" + badCommand + "' is not recognized."; + } +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 0000000000..146c6e2199 --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,157 @@ +package duke.task; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; + +import duke.exception.BadDateArgumentException; +import duke.exception.EmptyArgumentException; + +public class Deadline extends Task { + protected final LocalDate deadline; + + /** + * Creates a Task with a description and a deadline date that it needs + * to be done by. + * + * @param description Description of Deadline + * @param by When the task needs to be completed + * @throws EmptyArgumentException Some argument, either 'description' or 'by' is empty. + * @throws BadDateArgumentException When 'by' is not well formatted + */ + public Deadline(String description, String by) throws EmptyArgumentException, BadDateArgumentException { + super(description); + LocalDate potentialDeadline; + potentialDeadline = attemptToParseAsKeyword(by); + if (potentialDeadline != null) { + deadline = potentialDeadline; + return; + } + potentialDeadline = attemptToParseAsRelativeDay(by); + if (potentialDeadline != null) { + deadline = potentialDeadline; + return; + } + potentialDeadline = attemptToParseAsAbsolute(by); + if (potentialDeadline != null) { + deadline = potentialDeadline; + return; + } + throw new BadDateArgumentException(); + } + + /** + * Attempts to parse a string as a numeric date and obtain a LocalDate object + * Eg: "07 12 2021" -> 7 Dec, 2020 + * + * @param date The String that might be an absolute numeric date + * @return Either a date if parsing is successful or null otherwise. + */ + private static LocalDate attemptToParseAsAbsolute(String date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MM yyyy"); + try { + return LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + return null; + } + } + + /** + * Attempts to parse a string as a day of the week (Eg: Wednesday) + * + * @param day String that may or may not be a day of the week + * @return Either a DayOfWeek if successfully parsed or null otherwise. + */ + private static DayOfWeek attemptToParseAsDayOfWeek(String day) { + String[] mondayStrings = {"m", "mo", "mon", "monday"}; + String[] tuesdayStrings = {"tu", "tue", "tues", "tuesday"}; + String[] wednesdayStrings = {"w", "we", "wed", "weds", "wednesday"}; + String[] thursdayStrings = {"th", "thu", "thur", "thurs", "thursday"}; + String[] fridayStrings = {"f", "fr", "fri", "friday"}; + String[] saturdayStrings = {"sa", "sat", "saturday"}; + String[] sundayStrings = {"su", "sun", "sunday"}; + String[][] dayStrings = {mondayStrings, tuesdayStrings, wednesdayStrings, + thursdayStrings, fridayStrings, saturdayStrings, + sundayStrings}; + DayOfWeek[] dayOfWeeks = {DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY}; + assert dayStrings.length == dayOfWeeks.length; + String lowerCaseDay = day.toLowerCase().trim(); + for (int i = 0; i < dayStrings.length; i++) { + if (Arrays.asList(dayStrings[i]).contains(lowerCaseDay)) { + return dayOfWeeks[i]; + } + } + return null; + } + + /** + * Interprets a DayOfWeek as a day relative to today. + * Eg: Next Monday if today is Saturday is two days from today. + * + * @param day DayOfWeek relative to today + * @return A date relative to the passed in day + */ + private static LocalDate getNextRelativeDay(DayOfWeek day) { + LocalDate candidateDate = LocalDate.now().plusDays(1); + while (!candidateDate.getDayOfWeek().equals(day)) { + candidateDate = candidateDate.plusDays(1); + } + return candidateDate; + } + + /** + * Attempts to parse a string as a day relative to today. + * Eg: Next Monday if today is Saturday is two days from today. + * + * @param day String that might be a day + * @return Either a date if parsing is successful or null otherwise. + */ + private static LocalDate attemptToParseAsRelativeDay(String day) { + DayOfWeek dayOfWeek = attemptToParseAsDayOfWeek(day); + if (dayOfWeek == null) { + return null; + } + return getNextRelativeDay(dayOfWeek); + } + + /** + * Attempts to parse a string as a keyword and generate a date + * + * @param date String to parse + * @return Either a date if parsing is successful or null otherwise. + */ + private static LocalDate attemptToParseAsKeyword(String date) { + LocalDate now = LocalDate.now(); + switch (date.toLowerCase()) { + case "yesterday": + return now.minusDays(1); + case "now": + //Fall-through + case "today": + return now; + case "tomorrow": + //Fall-through + case "tmr": + return now.plusDays(1); + default: + return null; + } + } + + @Override + public String toString() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy"); + return "[D]" + super.toString() + " (Deadline: " + deadline.format(formatter) + ")"; + } + + @Override + public String toFileString() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MM yyyy"); + String date = deadline.format(formatter); + return "D," + super.toBaseFileString() + "," + date.length() + "," + date; + } +} diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 0000000000..df5a0848e8 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,33 @@ +package duke.task; + +import duke.exception.EmptyArgumentException; + +public class Event extends Task { + private final String eventPeriod; + + /** + * Creates an Event that has a description and a duration. + * + * @param description Description of event + * @param eventPeriod When the event takes place + * @throws EmptyArgumentException When an empty description or eventPeriod is passed + */ + public Event(String description, String eventPeriod) throws EmptyArgumentException { + super(description); + eventPeriod = eventPeriod.trim(); + if (eventPeriod.isEmpty()) { + throw new EmptyArgumentException(); + } + this.eventPeriod = eventPeriod; + } + + @Override + public String toString() { + return "[E]" + super.toString() + " (Event Time: " + eventPeriod + ")"; + } + + @Override + public String toFileString() { + return "E," + super.toBaseFileString() + "," + eventPeriod.length() + "," + eventPeriod; + } +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 0000000000..7eebda6f1c --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,93 @@ +package duke.task; + +import duke.exception.EmptyArgumentException; + +public abstract class Task { + private final String description; + private boolean isDone; + + /** + * Creates a Task that has a description + * + * @param description Description of Task + * @throws EmptyArgumentException Description is empty or whitespace + */ + public Task(String description) throws EmptyArgumentException { + description = description.trim(); + if (description.isEmpty()) { + throw new EmptyArgumentException(); + } + this.description = description; + this.isDone = false; + } + + /** + * Marks the task as done + */ + public void setDone() { + this.isDone = true; + } + + /** + * Gets the symbol for the status of the task. + * @return Symbol representing the task status + */ + public String getStatusIcon() { + return (isDone ? "*" : " "); //Don't use unicode, cause it can't test properly + } + + @Override + public String toString() { + return "[" + this.getStatusIcon() + "]: " + description; + } + + /** + * Converts the Task into a format suitable for file system storage + * @return A savable string. + */ + public abstract String toFileString(); + + @Override + public int hashCode() { + return this.toString().hashCode(); + } + + /** + * Converts raw Task data common to all Tasks into + * a format suitable for file system storage. + * @return A common partial savable string. + */ + String toBaseFileString() { + return (isDone ? "1" : "0") + "," + description.length() + "," + description; + } + + /** + * Checks whether the Task contains the following search substring, + * subject to special rules and behaviours based on capitalization and whitespace. + * + * @param search Case and whitespace sensitive search substring + * @return Whether the task matches the search query. + */ + public boolean containsSearch(String search) { + String targetString = description; + boolean isCaseInsensitive = search.toLowerCase().contains(search); + if (isCaseInsensitive) { + targetString = targetString.toLowerCase(); + } + if (search.contains(" ")) { //Literal multi word matching + return targetString.contains(search); + } else { //Smart per word start matching + String[] words = targetString.split(" "); + for (String word: words) { + if (word.length() < search.length()) { + continue; + } + String subWord = word.substring(0, search.length()); + if (subWord.equals(search)) { + return true; + } + } + return false; + } + } +} diff --git a/src/main/java/duke/task/ToDos.java b/src/main/java/duke/task/ToDos.java new file mode 100644 index 0000000000..8538e21f12 --- /dev/null +++ b/src/main/java/duke/task/ToDos.java @@ -0,0 +1,25 @@ +package duke.task; + +import duke.exception.EmptyArgumentException; + +public class ToDos extends Task { + /** + * Creates a To Do object that is essentially a wrapper on task. + * + * @param description Description of To Do + * @throws EmptyArgumentException when Description is empty or whitespace + */ + public ToDos(String description) throws EmptyArgumentException { + super(description); + } + + @Override + public String toString() { + return "[T]" + super.toString(); + } + + @Override + public String toFileString() { + return "T," + super.toBaseFileString(); + } +} diff --git a/src/main/java/duke/ui/DialogBox.java b/src/main/java/duke/ui/DialogBox.java new file mode 100644 index 0000000000..f27ca93eba --- /dev/null +++ b/src/main/java/duke/ui/DialogBox.java @@ -0,0 +1,61 @@ +package duke.ui; + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * An example of a custom control using FXML. + * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label + * containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} diff --git a/src/main/java/duke/ui/MainWindow.java b/src/main/java/duke/ui/MainWindow.java new file mode 100644 index 0000000000..ddb67c5b01 --- /dev/null +++ b/src/main/java/duke/ui/MainWindow.java @@ -0,0 +1,70 @@ +package duke.ui; + +import duke.Duke; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +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; +import javafx.util.Duration; + +/** + * 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; + String message = duke.startUpProcedure(); + + dialogContainer.getChildren().addAll( + DialogBox.getDukeDialog(message, dukeImage) + ); + System.out.println(message); + } + + /** + * 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 output = duke.processInput(input); + System.out.println(input); + System.out.println(output); + + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(output, dukeImage) + ); + userInput.clear(); + if (duke.getIsTerminated()) { + PauseTransition delay = new PauseTransition(Duration.seconds(3)); + delay.setOnFinished(event -> Platform.exit()); + delay.play(); + } + } +} diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 0000000000..d893658717 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 0000000000..3c82f45461 Binary files /dev/null and b/src/main/resources/images/DaUser.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..c4b3ccd7dd --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..c888558e1b --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +