diff --git a/.gitignore b/.gitignore index 2873e189e1..84951c9b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,13 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +build.gradle +data/tasklist.txt +gradle/wrapper/gradle-wrapper.jar +gradle/wrapper/gradle-wrapper.properties +gradlew +text-ui-test/runtest.bat + +# Class files +src/main/java/*.class diff --git a/README.md b/README.md index 8715d4d915..7d63353154 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Duke project template +# chatbot.evan.Evan project template -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. +This is a project template for a greenfield Java project. It's named after my english name "Evan". Given below are instructions on how to use it. ## Setting up in Intellij @@ -13,12 +13,19 @@ Prerequisites: JDK 11, update Intellij to the most recent version. 1. If there are any further prompts, accept the defaults. 1. Configure the project to use **JDK 11** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
In the same dialog, set the **Project language level** field to the `SDK default` option. -3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: +3. After that, locate the `src/main/java/chatbot.evan.Evan.java` file, right-click it, and choose `Run chatbot.evan.Evan.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: ``` - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| + Hello! I'm Evan, your personal task planning assistant. + What can I do for you? + + Available commands: + todo: create a new todo task + deadline: create a new deadline task + event: create a new event task + mark: mark a task as complete + unmark: mark a task as incomplete + delete: delete a task from the list + find: find a task from the list + list: show a list of all the tasks saved ``` + For more information on how to use the commands, do check out our README documentation in the docs folder! diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..eb761a9b9a --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..39efb6e4ac --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/docs/README.md b/docs/README.md index 8077118ebe..fd450613c5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,313 @@ -# User Guide +# Evan - User Guide ## Features -### Feature-ABC +### Adding Tasks into the list -Description of the feature. +We allow you to add 3 different types of tasks into the list. +1. ToDo Task +2. Deadline Task +3. Event Task -### Feature-XYZ +A ToDo task allows you to add a task without any dates specified. +A Deadline task allows you to add a task with 1 date/time specified as it's deadline. +An Event task allows you to add a task with 2 date/times. The first is specified as the start time and the second is specified as the end time of the event. -Description of the feature. +### View List + +Users can view the entire list of their pending tasks. + +### Delete Task + +You can delete a specified task on the list. + +### Task Status + +Users can mark a task as incompleted or completed. + +### Find Task + +Users can find a particular task they want to see by searching keywords. ## Usage -### `Keyword` - Describe action +### `list` - Show the entire list of tasks + +All the user has to type is "list", no arguments required. + +Format: `list` + +Example of usage: + +`list` + +Expected outcome: + +The Chatbot will respond with a list of all the tasks that is tracked + +``` +Here are the tasks in your list: +1. [T][] Read Book +2. [D][X] Finish CS2100 Assignment (by: 18-Sep-2023 1300) +3. [D][X] Finish CS2103T ip (by: 22-Sep-2023) +4. [E][] NUSBS Dharma Camp (from: 1-Jul-2024 to: 2-Jul-2024) +``` + +### `todo` - Starts a process to add a ToDo entry into the list + +All the user has to type is "todo", no arguments required. +Further follow-up actions are required to complete the insertion of the task as instructed by the chatbot. +
+Follow-up actions required: +1. Type in name of the task + + +Format: `todo` + +Example of usage (2-steps): + +User types `todo` + +Expected outcome: + +The Chatbot will instruct users on follow up actions to create a ToDo task + +``` +So you want to add a ToDo task. Tell me what's the task. +``` + +User types in the `name of task` + +Expected outcome: + +The Chatbot will respond that the task has been inserted + +``` +Got it. I've added this task: +[T][] Read Book +Now you have 1 tasks in the list. +``` + +### `deadline` - Starts a process to add a Deadline entry into the list + +All the user has to type is "deadline", no arguments required. +Further follow-up actions are required to complete the insertion of the task as instructed by the chatbot. +
+Follow-up actions required: +1. Type in name of the task +2. Type in date of deadline +3. (Optional) Type in time of deadline + + +Format: `deadline` + +Example of usage (4-steps): + +User types `deadline` + +Expected outcome: + +The Chatbot will prompt for the name of the task. + +``` +So you want to add a task with deadline. Tell me what's the task. +``` + +User types in the `name of task` + +Expected outcome: + +The Chatbot will prompt for the deadline date + +``` +Now indicate the deadline date. +``` + +User types in the `date of deadline` + +Expected outcome: + +The Chatbot will prompt for the deadline time + +``` +Indicate a start time ranging from 0000 - 2359. You may enter 'Skip' to not indicate a time. +``` + +User types in the `time of deadline` or `skip` + +Expected outcome: + +The Chatbot will respond that the task has been inserted + +``` +Got it. I've added this task: +[D][] Finish CS2100 Assignment (by: 18-Sep-2023) +Now you have 2 tasks in the list. +``` + +### `event` - Starts a process to add a Event entry into the list + +All the user has to type is "event", no arguments required. +Further follow-up actions are required to complete the insertion of the task as instructed by the chatbot. +
+Follow-up actions required: +1. Type in name of the task +2. Type in date of start date +3. (Optional) Type in time of start date +4. Type in date of end date +5. (Optional) Type in time of end date + + +Format: `event` + +Example of usage (6-steps): + +User types `event` + +Expected outcome: + +The Chatbot will prompt for the name of the task. + +``` +So you want to add an event task. Tell me what's the task. +``` + +User types in the `name of task` + +Expected outcome: + +The Chatbot will prompt for the start date + +``` +Now indicate the start date. +``` + +User types in the `date of start date` + +Expected outcome: + +The Chatbot will prompt for the start time + +``` +Indicate a start time ranging from 0000 - 2359. You may enter 'Skip' to not indicate a time. +``` + +User types in the `start time` or `skip` -Describe the action and its outcome. +Expected outcome: + +The Chatbot will prompt for the end date + +``` +Now indicate the end date. +``` + +User types in the `date of end date` + +Expected outcome: + +The Chatbot will prompt for the end time + +``` +Indicate an end time ranging from 0000 - 2359. You may enter 'Skip' to not indicate a time. +``` + +User types in the `end time` or `skip` + +Expected outcome: + +The Chatbot will respond that the task has been inserted + +``` +Got it. I've added this task: +[E][] NUSBS Dharma Camp (from: 1-Jul-2024 0930 to: 2-Jul-2024 1800) +Now you have 3 tasks in the list. +``` + +### `mark` - Marks a specified task in the list as complete + +User has to type `mark` followed by the corresponding index number of the task in the list + +Format: `mark INDEX` +- Deletes the task at the specified `INDEX` +- `INDEX` must not exceed the size of the list +- `INDEX` must be a positive number + +Example of usage: + +`mark 1` + +Expected outcome: + +The Chatbot will respond that the task has been successfully marked as done + +``` +Nice! I've marked this task as done +[T][X] Read Book +``` + +### `unmark` - Marks a specified task in the list as incomplete + +User has to type `unmark` followed by the corresponding index number of the task in the list + +Format: `unmark INDEX` +- Deletes the task at the specified `INDEX` +- `INDEX` must not exceed the size of the list +- `INDEX` must be a positive number + +Example of usage: + +`unmark 1` + +Expected outcome: + +The Chatbot will respond that the task has been successfully marked as incomplete + +``` +OK! I've marked this task as not done yet: +[T][] Read Book +``` + +### `delete` - Delete a specified task in the list + +User has to type `delete` followed by the corresponding index number of the task in the list + +Format: `delete INDEX` +- Deletes the task at the specified `INDEX` +- `INDEX` must not exceed the size of the list +- `INDEX` must be a positive number Example of usage: -`keyword (optional arguments)` +`delete 1` Expected outcome: -Description of the outcome. +The Chatbot will respond that the task has been successfully deleted + +``` +Noted. I've removed this task: +[T][] Read Book +Now you have 2 tasks in the list +``` + +### `find` - Finds task with the associated specified keyword/phrase + +User has to type `find` followed by the keyword/phrase that they want to find. + +Format: `find WORDS` +- Finds the task with the specified `WORDS` +- `WORDS` can be a part of a word or phrase (e.g. find accum would find tasks with the word "accumulator" in it) + +Example of usage: + +`find read` + +Expected outcome: +The Chatbot will provide a list of the tasks with the word "read" in it ``` -expected output +Here are the matching tasks in your list with its correct corresponding index numbers: +1.[T][] Read Book ``` +Note - The Index number displayed in the chatbot response will correspond to the actual index number of the task in the full list diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..2dc32a5340 Binary files /dev/null and b/docs/Ui.png differ diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..6689b85bee --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/chatbot/evan/Evan.java b/src/main/java/chatbot/evan/Evan.java new file mode 100644 index 0000000000..2bf16504e4 --- /dev/null +++ b/src/main/java/chatbot/evan/Evan.java @@ -0,0 +1,94 @@ +package chatbot.evan; + +import enums.Command; +import exception.InvalidCommandException; +import process.ComplexProcess; +import process.Deadline; +import process.Delete; +import process.Event; +import process.Find; +import process.Mark; +import process.SimpleProcess; +import process.ToDo; +import process.Unmark; +import task.TaskManager; + +/** + * Main class of the chatbot + */ +public class Evan { + private ComplexProcess process = null; + + /** + * Returns the response of the chatbot for a given input + * @param input of user + * @return response of chatbot + */ + public String getResponse(String input) { + if (process != null) { + return getExistingProcessResponse(input); + } + + return startProcessThenGetResponse(input); + } + + private String getExistingProcessResponse(String input) { + String response = process.processInput(input); + if (process.isComplete()) { + process = null; + } + return response; + } + + private String startProcessThenGetResponse(String input) { + if (input.equals(Command.BYE.toString())) { + return "Bye. Hope to see you again soon!"; + } else if (input.equals(Command.LIST.toString())) { + TaskManager taskManager = TaskManager.init(); + return taskManager.getAllTasks(); + } else if (input.equals(Command.TODO.toString()) || input.equals("t")) { + process = new ToDo(); + return process.firstInstruction(); + } else if (input.equals(Command.DEADLINE.toString()) || input.equals("d")) { + process = new Deadline(); + return process.firstInstruction(); + } else if (input.equals(Command.EVENT.toString()) || input.equals("e")) { + process = new Event(); + return process.firstInstruction(); + } else if (input.startsWith(Command.DELETE.toString())) { + SimpleProcess simpleProcess = new Delete(); + return simpleProcess.processInput(input); + } else if (input.startsWith(Command.MARK.toString())) { + SimpleProcess simpleProcess = new Mark(); + return simpleProcess.processInput(input); + } else if (input.startsWith(Command.UNMARK.toString())) { + SimpleProcess simpleProcess = new Unmark(); + return simpleProcess.processInput(input); + } else if (input.startsWith(Command.FIND.toString())) { + SimpleProcess simpleProcess = new Find(); + return simpleProcess.processInput(input); + } else { + InvalidCommandException e = new InvalidCommandException(); + return e.toString(); + } + } + + /** + * Gets the string introduction of the bot and its commands + * @return string introduction of the chatbot and its commands + */ + public String getIntro() { + StringBuilder stringBuilder = new StringBuilder("Hello! I'm Evan, your personal task planning assistant.\n") + .append("What can I do for you?\n\n") + .append("Available commands:\n") + .append("todo: create a new todo task\n") + .append("deadline: create a new deadline task\n") + .append("event: create a new event task\n") + .append("mark: mark a task as complete\n") + .append("unmark: mark a task as incomplete\n") + .append("delete: delete a task from the list\n") + .append("find: find a task from the list\n") + .append("list: show a list of all the tasks saved\n"); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/enums/Command.java b/src/main/java/enums/Command.java new file mode 100644 index 0000000000..9a9278508d --- /dev/null +++ b/src/main/java/enums/Command.java @@ -0,0 +1,36 @@ +package enums; + +/** + * Enum class with all the commands listed + */ +public enum Command { + BYE("bye"), + LIST("list"), + MARK("mark"), + UNMARK("unmark"), + DELETE("delete"), + DEADLINE("deadline"), + TODO("todo"), + EVENT("event"), + FIND("find"), + SKIP("skip"); + + private String cmd; + + /** + * Constructor for the enum class + * @param cmd String of the command + */ + Command(String cmd) { + this.cmd = cmd; + } + + /** + * returns the String representation of the command + * @return the command String + */ + @Override + public String toString() { + return cmd; + } +} diff --git a/src/main/java/exception/InvalidCommandException.java b/src/main/java/exception/InvalidCommandException.java new file mode 100644 index 0000000000..676ca83fd6 --- /dev/null +++ b/src/main/java/exception/InvalidCommandException.java @@ -0,0 +1,15 @@ +package exception; + +/** + * Checked Exception for the event where the input command is not a valid command. + */ +public class InvalidCommandException extends Exception { + /** + * Returns a string to inform users that the command is invalid + * @return string information to inform user of exception + */ + @Override + public String toString() { + return "This is not a registered command that I know of"; + } +} diff --git a/src/main/java/exception/InvalidDateException.java b/src/main/java/exception/InvalidDateException.java new file mode 100644 index 0000000000..b88e73143e --- /dev/null +++ b/src/main/java/exception/InvalidDateException.java @@ -0,0 +1,16 @@ +package exception; + +/** + * Checked Exception for the event where the input date is not a valid date. + */ +public class InvalidDateException extends Exception { + + /** + * Returns a string to inform users that the date is invalid + * @return string information to inform user of exception + */ + @Override + public String toString() { + return "The given date is invalid. Please type in a valid date."; + } +} diff --git a/src/main/java/exception/InvalidInputException.java b/src/main/java/exception/InvalidInputException.java new file mode 100644 index 0000000000..da41c87519 --- /dev/null +++ b/src/main/java/exception/InvalidInputException.java @@ -0,0 +1,16 @@ +package exception; + +/** + * Checked Exception for the event where the input is not valid. + */ +public class InvalidInputException extends Exception { + + /** + * Returns a string to inform users that the input is invalid + * @return string information to inform user of exception + */ + @Override + public String toString() { + return "Your given input is invalid. Please type an appropriate input."; + } +} diff --git a/src/main/java/exception/InvalidTimeException.java b/src/main/java/exception/InvalidTimeException.java new file mode 100644 index 0000000000..58c92b6a7d --- /dev/null +++ b/src/main/java/exception/InvalidTimeException.java @@ -0,0 +1,17 @@ +package exception; + +/** + * Checked Exception for the event where the input time is not a valid time. + */ +public class InvalidTimeException extends Exception { + + /** + * Returns a string to inform users that the time is invalid + * @return string information to inform user of exception + */ + @Override + public String toString() { + return "The given time is invalid. " + + "Please type in a valid time in the range between 0000 - 2359"; + } +} diff --git a/src/main/java/exception/MissingArgumentException.java b/src/main/java/exception/MissingArgumentException.java new file mode 100644 index 0000000000..99fc156b23 --- /dev/null +++ b/src/main/java/exception/MissingArgumentException.java @@ -0,0 +1,11 @@ +package exception; + +/** + * Checked Exception for the event where there are missing arguments. + */ +public class MissingArgumentException extends Exception { + @Override + public String toString() { + return "There are missing arguments to this command"; + } +} diff --git a/src/main/java/exception/MissingIndexException.java b/src/main/java/exception/MissingIndexException.java new file mode 100644 index 0000000000..2dfd16db37 --- /dev/null +++ b/src/main/java/exception/MissingIndexException.java @@ -0,0 +1,11 @@ +package exception; + +/** + * Checked Exception for the event where there are missing index arguments. + */ +public class MissingIndexException extends MissingArgumentException { + @Override + public String toString() { + return "The following arguments are missing: index of the task"; + } +} diff --git a/src/main/java/exception/MissingKeywordException.java b/src/main/java/exception/MissingKeywordException.java new file mode 100644 index 0000000000..a232b6f818 --- /dev/null +++ b/src/main/java/exception/MissingKeywordException.java @@ -0,0 +1,11 @@ +package exception; + +/** + * Checked Exception for the event where there are missing keyword arguments. + */ +public class MissingKeywordException extends MissingArgumentException { + @Override + public String toString() { + return "The following arguments are missing: keyword of the task"; + } +} diff --git a/src/main/java/parser/CommandParser.java b/src/main/java/parser/CommandParser.java new file mode 100644 index 0000000000..01bcf7e3c4 --- /dev/null +++ b/src/main/java/parser/CommandParser.java @@ -0,0 +1,82 @@ +package parser; + +import enums.Command; +import exception.InvalidCommandException; +import exception.MissingArgumentException; +import exception.MissingIndexException; +import exception.MissingKeywordException; + +/** + * Class to help parse complex strings from user input + */ +public class CommandParser { + + /** + * Validates if the first word is a valid command and filters it to return the arguments + * @param input string of a single user input + * @return command arguments + * @throws MissingArgumentException if there are no arguments + * @throws InvalidCommandException if the first word is an invalid command + */ + public static String getCommandArguments(String input) throws + MissingArgumentException, + InvalidCommandException { + + assert input != null : "string input is null"; + + String firstWord = getFirstWord(input); + + //ensure first word is a valid command + if (!isValidCommand(firstWord)) { + throw new InvalidCommandException(); + } + + //ensure that arguments exist + String args = input.substring(firstWord.length()); + if (args.isBlank()) { + throwAppropriateMissingArgumentException(firstWord); + } + + return args.substring(1); + } + + /** + * Takes in a string of input and returns the first word + * @param input String of user input + * @return the first word of the string + */ + public static String getFirstWord(String input) { + + int spaceIndex = input.indexOf(' '); + if (spaceIndex == -1) { + return input; + } + return input.substring(0, spaceIndex); + } + + private static boolean isValidCommand(String word) { + try { + Command.valueOf(word.toUpperCase()); + } catch (IllegalArgumentException e) { + return false; + } + return true; + } + + private static void throwAppropriateMissingArgumentException(String command) + throws MissingArgumentException { + Command commandEnum = Command.valueOf(command.toUpperCase()); + switch (commandEnum) { + case MARK: + throw new MissingIndexException(); + case UNMARK: + throw new MissingIndexException(); + case DELETE: + throw new MissingIndexException(); + case FIND: + throw new MissingKeywordException(); + default: + throw new MissingArgumentException(); + } + } +} diff --git a/src/main/java/parser/Time.java b/src/main/java/parser/Time.java new file mode 100644 index 0000000000..56ada705a3 --- /dev/null +++ b/src/main/java/parser/Time.java @@ -0,0 +1,177 @@ +package parser; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import exception.InvalidDateException; +import exception.InvalidTimeException; + +/** + * Class for the parsing of a string of date/time data + */ +public class Time { + + private static final DateTimeFormatter[] FORMATS = new DateTimeFormatter[]{ + DateTimeFormatter.ofPattern("MMM-d-yyyy"), + DateTimeFormatter.ofPattern("MMM-dd-yyyy"), + DateTimeFormatter.ofPattern("d-MMM-yyyy"), + DateTimeFormatter.ofPattern("dd-MM-yyyy"), + DateTimeFormatter.ofPattern("d-MM-yyyy"), + DateTimeFormatter.ofPattern("MM-dd-yyyy"), + DateTimeFormatter.ofPattern("MM-d-yyyy"), + DateTimeFormatter.ofPattern("yyyy-MM-dd"), + DateTimeFormatter.ofPattern("yyyy-MM-d"), + DateTimeFormatter.ofPattern("yyyy-MM-dd"), + DateTimeFormatter.ofPattern("MMM-d-yyyy HHmm"), + DateTimeFormatter.ofPattern("MMM-dd-yyyy HHmm"), + DateTimeFormatter.ofPattern("d-MMM-yyyy HHmm"), + DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm"), + DateTimeFormatter.ofPattern("d-MM-yyyy HHmm"), + DateTimeFormatter.ofPattern("MM-dd-yyyy HHmm"), + DateTimeFormatter.ofPattern("MM-d-yyyy HHmm"), + DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"), + DateTimeFormatter.ofPattern("yyyy-MM-d HHmm"), + DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"), + DateTimeFormatter.ofPattern("HHmm MMM-d-yyyy"), + DateTimeFormatter.ofPattern("HHmm MMM-dd-yyyy"), + DateTimeFormatter.ofPattern("HHmm d-MMM-yyyy"), + DateTimeFormatter.ofPattern("HHmm dd-MM-yyyy"), + DateTimeFormatter.ofPattern("HHmm d-MM-yyyy"), + DateTimeFormatter.ofPattern("HHmm MM-dd-yyyy"), + DateTimeFormatter.ofPattern("HHmm MM-d-yyyy"), + DateTimeFormatter.ofPattern("HHmm yyyy-MM-dd"), + DateTimeFormatter.ofPattern("HHmm yyyy-MM-d"), + DateTimeFormatter.ofPattern("HHmm yyyy-MM-dd"), + + DateTimeFormatter.ofPattern("MMM d yyyy"), + DateTimeFormatter.ofPattern("MMM dd yyyy"), + DateTimeFormatter.ofPattern("d MMM yyyy"), + DateTimeFormatter.ofPattern("dd MM yyyy"), + DateTimeFormatter.ofPattern("d MM yyyy"), + DateTimeFormatter.ofPattern("MM dd yyyy"), + DateTimeFormatter.ofPattern("MM d yyyy"), + DateTimeFormatter.ofPattern("yyyy MM dd"), + DateTimeFormatter.ofPattern("yyyy MM d"), + DateTimeFormatter.ofPattern("yyyy MM dd"), + DateTimeFormatter.ofPattern("MMM d yyyy HHmm"), + DateTimeFormatter.ofPattern("MMM dd yyyy HHmm"), + DateTimeFormatter.ofPattern("d MMM yyyy HHmm"), + DateTimeFormatter.ofPattern("dd MM yyyy HHmm"), + DateTimeFormatter.ofPattern("d MM yyyy HHmm"), + DateTimeFormatter.ofPattern("MM dd yyyy HHmm"), + DateTimeFormatter.ofPattern("MM d yyyy HHmm"), + DateTimeFormatter.ofPattern("yyyy MM dd HHmm"), + DateTimeFormatter.ofPattern("yyyy MM d HHmm"), + DateTimeFormatter.ofPattern("yyyy MM dd HHmm"), + DateTimeFormatter.ofPattern("HHmm MMM d yyyy"), + DateTimeFormatter.ofPattern("HHmm MMM dd yyyy"), + DateTimeFormatter.ofPattern("HHmm d MMM yyyy"), + DateTimeFormatter.ofPattern("HHmm dd MM yyyy"), + DateTimeFormatter.ofPattern("HHmm d MM yyyy"), + DateTimeFormatter.ofPattern("HHmm MM dd yyyy"), + DateTimeFormatter.ofPattern("HHmm MM d yyyy"), + DateTimeFormatter.ofPattern("HHmm yyyy MM dd"), + DateTimeFormatter.ofPattern("HHmm yyyy MM d"), + DateTimeFormatter.ofPattern("HHmm yyyy MM dd"), + + DateTimeFormatter.ofPattern("MMM/d/yyyy"), + DateTimeFormatter.ofPattern("MMM/dd/yyyy"), + DateTimeFormatter.ofPattern("d/MMM/yyyy"), + DateTimeFormatter.ofPattern("dd/MM/yyyy"), + DateTimeFormatter.ofPattern("d/MM/yyyy"), + DateTimeFormatter.ofPattern("MM/dd/yyyy"), + DateTimeFormatter.ofPattern("MM/d/yyyy"), + DateTimeFormatter.ofPattern("yyyy/MM/dd"), + DateTimeFormatter.ofPattern("yyyy/MM/d"), + DateTimeFormatter.ofPattern("yyyy/MM/dd"), + DateTimeFormatter.ofPattern("MMM/d/yyyy HHmm"), + DateTimeFormatter.ofPattern("MMM/dd/yyyy HHmm"), + DateTimeFormatter.ofPattern("d/MMM/yyyy HHmm"), + DateTimeFormatter.ofPattern("dd/MM/yyyy HHmm"), + DateTimeFormatter.ofPattern("d/MM/yyyy HHmm"), + DateTimeFormatter.ofPattern("MM/dd/yyyy HHmm"), + DateTimeFormatter.ofPattern("MM/d/yyyy HHmm"), + DateTimeFormatter.ofPattern("yyyy/MM/dd HHmm"), + DateTimeFormatter.ofPattern("yyyy/MM/d HHmm"), + DateTimeFormatter.ofPattern("yyyy/MM/dd HHmm"), + DateTimeFormatter.ofPattern("HHmm MMM/d yyyy"), + DateTimeFormatter.ofPattern("HHmm MMM/dd yyyy"), + DateTimeFormatter.ofPattern("HHmm d/MMM/yyyy"), + DateTimeFormatter.ofPattern("HHmm dd/MM/yyyy"), + DateTimeFormatter.ofPattern("HHmm d/MM/yyyy"), + DateTimeFormatter.ofPattern("HHmm MM/dd/yyyy"), + DateTimeFormatter.ofPattern("HHmm MM/d/yyyy"), + DateTimeFormatter.ofPattern("HHmm yyyy/MM/dd"), + DateTimeFormatter.ofPattern("HHmm yyyy/MM/d"), + DateTimeFormatter.ofPattern("HHmm yyyy/MM/dd"), + }; + + /** + * Takes in a string containing the date and attempts to find a format that the date is written in + * @param date string of the date data + * @return DateTimeFormatter containing the corresponding date format + * @throws InvalidDateException if no appropriate date format is found + */ + private static DateTimeFormatter findDateFormat(String date) throws InvalidDateException { + for (DateTimeFormatter format: FORMATS) { + if (hasSameDateFormat(date, format)) { + return format; + } + } + throw new InvalidDateException(); + } + + /** + * Checks if the date string has the same date format in the formatter + * @param date string of the date data + * @param formatter date format to be checked with + * @return boolean indicating if its the same format or not + */ + private static boolean hasSameDateFormat(String date, DateTimeFormatter formatter) { + try { + LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + return false; + } + return true; + } + + /** + * Takes in a string containing date, validates if its a valid date and then + * returns the date in a standardized format + * @param date string containing the user input date + * @return date string in a standardised format + * @throws InvalidDateException if date is an invalid date + */ + public static String formatDate(String date) throws InvalidDateException { + DateTimeFormatter stdFormat = DateTimeFormatter.ofPattern("dd-MMM-yyyy"); + DateTimeFormatter currentFormat = findDateFormat(date); + return LocalDate.parse(date, currentFormat).format(stdFormat); + } + + /** + * Takes in a string containing time, validates if its a valid time and then + * returns the date/time in a standardized format + * @param date string date input by user + * @param time string time input by user + * @return date and time string in standardised format + * @throws InvalidTimeException if input time is not a valid time + * @throws InvalidDateException if date is not a valid date + */ + public static String formatTime(String date, String time) throws InvalidTimeException, InvalidDateException { + DateTimeFormatter stdFormat = DateTimeFormatter.ofPattern("dd-MMM-yyyy HHmm"); + DateTimeFormatter currentFormat = findDateFormat(date); + LocalDate lDate = LocalDate.parse(date, currentFormat); + int intTime = Integer.parseInt(time); + int hour = (int) Math.floor(intTime / 100.0); + int minute = intTime - (hour * 100); + try { + return lDate.atTime(hour, minute).format(stdFormat); + } catch (DateTimeException e) { + throw new InvalidTimeException(); + } + + } +} diff --git a/src/main/java/process/ComplexProcess.java b/src/main/java/process/ComplexProcess.java new file mode 100644 index 0000000000..a9a73769e5 --- /dev/null +++ b/src/main/java/process/ComplexProcess.java @@ -0,0 +1,18 @@ +package process; + +/** + * An interface to define the behavior of a process that requires multiple inputs + */ +public interface ComplexProcess extends Process { + /** + * Returns a string message to instruct the user on the first step in the process + * @return string message of first set of instructions to user + */ + public String firstInstruction(); + + /** + * Getter method to check if process is completed + * @return whether the process is complete (true) or not complete (false) + */ + public boolean isComplete(); +} diff --git a/src/main/java/process/Deadline.java b/src/main/java/process/Deadline.java new file mode 100644 index 0000000000..4ed671ac7f --- /dev/null +++ b/src/main/java/process/Deadline.java @@ -0,0 +1,82 @@ +package process; + +import exception.InvalidDateException; +import exception.InvalidTimeException; +import parser.Time; +import task.Deadlines; +import task.TaskManager; + +/** + * A class for the process of creating a Deadline Task object + */ +public class Deadline implements ComplexProcess { + private enum Stage { + FIRST, SECOND, THIRD + } + private TaskManager tasks = TaskManager.init(); + private Stage stage = Stage.FIRST; + private String name = null; + private String deadline = null; + private boolean isComplete = false; + + @Override + public String processInput(String input) { + String reply = ""; + + switch (stage) { + case FIRST: + reply = processFirstStep(input); + break; + case SECOND: + reply = processSecondStep(input); + break; + case THIRD: + reply = processLastStep(input); + break; + default: + assert false : "should not reach default case in deadline.java"; + } + return reply; + } + + private String processFirstStep(String input) { + name = input; + stage = Stage.SECOND; + return "Now indicate the deadline date."; + } + + private String processSecondStep(String input) { + try { + deadline = Time.formatDate(input); + } catch (InvalidDateException e) { + return e.toString(); + } + stage = Stage.THIRD; + return "Indicate a start time ranging from 0000 - 2359. " + + "You may enter 'Skip' to not indicate a time"; + } + + private String processLastStep(String input) { + if (!input.toLowerCase().equals(enums.Command.SKIP.toString())) { + try { + deadline = Time.formatTime(deadline, input); + } catch (InvalidTimeException | InvalidDateException e) { + return e.toString(); + } catch (NumberFormatException e) { + return "Non-numerical characters detected. Please enter numbers only."; + } + } + isComplete = true; + return tasks.addTask(new Deadlines(name, deadline)); + } + + @Override + public String firstInstruction() { + return "So you want to add a task with deadline. Tell me what's the task."; + } + + @Override + public boolean isComplete() { + return isComplete; + } +} diff --git a/src/main/java/process/Delete.java b/src/main/java/process/Delete.java new file mode 100644 index 0000000000..d99d90b1e1 --- /dev/null +++ b/src/main/java/process/Delete.java @@ -0,0 +1,29 @@ +package process; + +import exception.InvalidCommandException; +import exception.MissingArgumentException; +import parser.CommandParser; +import task.TaskManager; + +/** + * A class for the process of creating a Delete Task object + */ +public class Delete implements SimpleProcess { + private TaskManager tasks = TaskManager.init(); + @Override + public String processInput(String input) { + assert input.toLowerCase().startsWith("delete") : "user input does not start with the correct word"; + try { + String number = CommandParser.getCommandArguments(input); + return tasks.deleteTask(Integer.parseInt(number)); + } catch (MissingArgumentException e) { + return e.toString(); + } catch (InvalidCommandException e) { + return e.toString(); + } catch (NumberFormatException e) { + return "Strictly type 1 number only"; + } catch (IndexOutOfBoundsException e) { + return "Index number does not exist in our list"; + } + } +} diff --git a/src/main/java/process/Event.java b/src/main/java/process/Event.java new file mode 100644 index 0000000000..cbc09262bd --- /dev/null +++ b/src/main/java/process/Event.java @@ -0,0 +1,115 @@ +package process; + +import exception.InvalidDateException; +import exception.InvalidTimeException; +import parser.Time; +import task.Events; +import task.TaskManager; + +/** + * A class for the process of creating an event task + */ +public class Event implements ComplexProcess { + private enum Stage { + FIRST, SECOND, THIRD, FOURTH, FIFTH + } + private TaskManager tasks = TaskManager.init(); + private Stage stage = Stage.FIRST; + private String name = null; + private String from = null; + private String to = null; + private boolean isComplete = false; + + @Override + public String processInput(String input) { + String reply = ""; + switch (stage) { + case FIRST: + reply = processFirstStep(input); + break; + case SECOND: + reply = processSecondStep(input); + break; + case THIRD: + reply = processThirdStep(input); + break; + case FOURTH: + reply = processFourthStep(input); + break; + case FIFTH: + reply = processLastStep(input); + break; + default: + assert false : "should not reach default case in event.java"; + } + return reply; + } + + @Override + public String firstInstruction() { + return "So you want to add an event task. Tell me what's the task."; + } + + @Override + public boolean isComplete() { + return isComplete; + } + + private String processFirstStep(String name) { + this.name = name; + stage = Stage.SECOND; + return "Now indicate the start date."; + } + + private String processSecondStep(String date) { + try { + from = Time.formatDate(date); + } catch (InvalidDateException e) { + return e.toString(); + } + stage = Stage.THIRD; + return "Indicate a start time ranging from 0000 - 2359. " + + "You may enter 'Skip' to not indicate a time"; + } + + private String processFourthStep(String date) { + try { + to = Time.formatDate(date); + } catch (InvalidDateException e) { + return e.toString(); + } + stage = Stage.FIFTH; + return "Indicate an end time ranging from 0000 - 2359. " + + "You may enter 'Skip' to not indicate a time"; + } + + private String processThirdStep(String input) { + if (!input.toLowerCase().equals(enums.Command.SKIP.toString())) { + try { + from = Time.formatTime(from, input); + } catch (InvalidTimeException | InvalidDateException e) { + return e.toString(); + } catch (NumberFormatException e) { + return "Non-numerical characters detected. Please enter numbers only. " + + "Returning to homepage..."; + } + } + stage = Stage.FOURTH; + return "Now indicate the end date."; + } + + private String processLastStep(String input) { + if (!input.toLowerCase().equals(enums.Command.SKIP.toString())) { + try { + to = Time.formatTime(to, input); + } catch (InvalidTimeException | InvalidDateException e) { + return e.toString(); + } catch (NumberFormatException e) { + return "Non-numerical characters detected. Please enter numbers only. " + + "Returning to homepage..."; + } + } + isComplete = true; + return tasks.addTask(new Events(name, from, to)); + } +} diff --git a/src/main/java/process/Find.java b/src/main/java/process/Find.java new file mode 100644 index 0000000000..cbc75ddc75 --- /dev/null +++ b/src/main/java/process/Find.java @@ -0,0 +1,34 @@ +package process; + +import exception.InvalidCommandException; +import exception.InvalidInputException; +import exception.MissingArgumentException; +import parser.CommandParser; +import task.TaskManager; + +/** + * A class for the process of creating a find task + */ +public class Find implements SimpleProcess { + private TaskManager tasks = TaskManager.init(); + + @Override + public String processInput(String input) { + assert input.toLowerCase().startsWith("find") : "User input does not start with the correct word"; + + try { + String argument = CommandParser.getCommandArguments(input); + String[] split = argument.split(" "); + + //Checks if the number of arguments is correct + if (split.length > 1) { + throw new InvalidInputException(); + } + + //Performs the task and returns to response log of task + return tasks.findTask(argument); + } catch (MissingArgumentException | InvalidCommandException | InvalidInputException e) { + return e.toString(); + } + } +} diff --git a/src/main/java/process/Mark.java b/src/main/java/process/Mark.java new file mode 100644 index 0000000000..6f20b1ec03 --- /dev/null +++ b/src/main/java/process/Mark.java @@ -0,0 +1,29 @@ +package process; + +import exception.InvalidCommandException; +import exception.MissingArgumentException; +import parser.CommandParser; +import task.TaskManager; + +/** + * A class for the process of creating a mark task + */ +public class Mark implements SimpleProcess { + private TaskManager tasks = TaskManager.init(); + + @Override + public String processInput(String input) { + assert input.toLowerCase().startsWith("mark") : "user input does not start with the correct word"; + + try { + String number = CommandParser.getCommandArguments(input); + return tasks.markDone(Integer.parseInt(number)); + } catch (NumberFormatException e) { + return "Strictly type 1 number only"; + } catch (IndexOutOfBoundsException e) { + return "Index number does not exist in our list"; + } catch (MissingArgumentException | InvalidCommandException e) { + return e.toString(); + } + } +} diff --git a/src/main/java/process/Process.java b/src/main/java/process/Process.java new file mode 100644 index 0000000000..c4b1f1de47 --- /dev/null +++ b/src/main/java/process/Process.java @@ -0,0 +1,14 @@ +package process; + +/** + * An interface to define the behavior of a process of handling user input + */ +public interface Process { + /** + * Takes in a user input, validates it and processes the input in accordance to the respective task behavior. + * Then finally returns a string message response to be displayed for the user + * @param input String message captured by the user + * @return String message of appropriate response for the user input + */ + public String processInput(String input); +} diff --git a/src/main/java/process/SimpleProcess.java b/src/main/java/process/SimpleProcess.java new file mode 100644 index 0000000000..606889f1de --- /dev/null +++ b/src/main/java/process/SimpleProcess.java @@ -0,0 +1,7 @@ +package process; + +/** + * An interface to define the behavior of a process that can be finished in one input + */ +public interface SimpleProcess extends Process { +} diff --git a/src/main/java/process/ToDo.java b/src/main/java/process/ToDo.java new file mode 100644 index 0000000000..8634a6f44b --- /dev/null +++ b/src/main/java/process/ToDo.java @@ -0,0 +1,26 @@ +package process; + +import task.TaskManager; + +/** + * A class for the process of creating a todo task + */ +public class ToDo implements ComplexProcess { + private TaskManager tasks = TaskManager.init(); + private boolean isComplete = false; + @Override + public String firstInstruction() { + return "So you want to add a ToDo task. Tell me what's the task."; + } + + @Override + public boolean isComplete() { + return isComplete; + } + + @Override + public String processInput(String input) { + isComplete = true; + return tasks.addTask(new task.ToDo(input)); + } +} diff --git a/src/main/java/process/Unmark.java b/src/main/java/process/Unmark.java new file mode 100644 index 0000000000..6a76b9d937 --- /dev/null +++ b/src/main/java/process/Unmark.java @@ -0,0 +1,28 @@ +package process; + +import exception.InvalidCommandException; +import exception.MissingArgumentException; +import parser.CommandParser; +import task.TaskManager; + +/** + * A class for the process of creating a unmark task + */ +public class Unmark implements SimpleProcess { + private TaskManager tasks = TaskManager.init(); + + @Override + public String processInput(String input) { + assert input.toLowerCase().startsWith("unmark") : "user input does not start with the correct word"; + try { + String number = CommandParser.getCommandArguments(input); + return tasks.unmarkDone(Integer.parseInt(number)); + } catch (NumberFormatException e) { + return "Strictly type 1 number only"; + } catch (IndexOutOfBoundsException e) { + return "Index number does not exist in our list"; + } catch (MissingArgumentException | InvalidCommandException e) { + return e.toString(); + } + } +} diff --git a/src/main/java/storage/Database.java b/src/main/java/storage/Database.java new file mode 100644 index 0000000000..a7703f55b7 --- /dev/null +++ b/src/main/java/storage/Database.java @@ -0,0 +1,107 @@ +package storage; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +import enums.Command; +import task.Deadlines; +import task.Events; +import task.Task; +import task.ToDo; + +/** + * Class to interface with the text file storing the user task records + */ +public class Database { + + /** + * Takes in a list of tasks and overwrites it onto the text file + * @param list list of tasks + * @throws IOException from file handling errors + */ + public static void saveTasks(ArrayList list) throws IOException { + //Open file and feed it into writer + File file = getDataFile(); + FileWriter fileToWrite = new FileWriter(file); + + //Write all contents from the local list into file + Task[] tasks = list.toArray(new Task[0]); + for (int i = 0; i < tasks.length; i++) { + fileToWrite.write(tasks[i].dataFormat()); + fileToWrite.write(System.lineSeparator()); + } + + fileToWrite.close(); + } + + /** + * Retrieves the list of task records from the text file and returns it in an arraylist + * @return list of tasks saved in the text file + * @throws IOException from file handling errors + * @throws ArrayIndexOutOfBoundsException for file data corruption errors + */ + public static ArrayList loadTasks() throws IOException, ArrayIndexOutOfBoundsException { + ArrayList list = new ArrayList<>(); + + //Open the required file and feed it into the reader + File file = getDataFile(); + FileReader fileReader = new FileReader(file); + BufferedReader fileToRead = new BufferedReader(fileReader); + String nextLine = fileToRead.readLine(); + + //Read and add all contents into file + while (nextLine != null) { + String[] data = nextLine.split("/"); + String taskType = data[0]; + boolean status = data[1].equals("true"); + if (taskType.equals(Command.TODO.toString())) { + list.add(new ToDo(status, data[2])); + } else if (taskType.equals(Command.DEADLINE.toString())) { + list.add(new Deadlines(status, data[2], data[3])); + } else if (taskType.equals(Command.EVENT.toString())) { + list.add(new Events(status, data[2], data[3], data[4])); + } + nextLine = fileToRead.readLine(); + } + + fileReader.close(); + fileToRead.close(); + return list; + } + + private static File getDataDirectory() { + String jarPath = Database.class + .getProtectionDomain() + .getCodeSource() + .getLocation() + .getPath(); + File jarFile = new File(jarPath); + File parentDirectory = jarFile.getParentFile(); + File dataDirectory = new File(parentDirectory, "save_data"); + + // Check if the directory exists. If not, create it. + if (!dataDirectory.exists()) { + dataDirectory.mkdirs(); // This will create the directory. + } + return dataDirectory; + } + + private static File getDataFile() { + File directory = getDataDirectory(); + File file = new File(directory, "tasklist.txt"); + + // Check if the file exists. If not, create it. + if (!file.exists()) { + try { + file.createNewFile(); // This will create the file. + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return file; + } +} diff --git a/src/main/java/task/Deadlines.java b/src/main/java/task/Deadlines.java new file mode 100644 index 0000000000..bf96774f97 --- /dev/null +++ b/src/main/java/task/Deadlines.java @@ -0,0 +1,50 @@ +package task; + +/** + * Object class for instances of deadline tasks + */ +public class Deadlines extends Task { + protected String by; + + /** + * Public constructor of the class + * @param description description of the task + * @param by date of the deadline + */ + public Deadlines(String description, String by) { + super(description); + this.by = by; + } + + /** + * Overloaded constructor to create classes with known status + * @param status done status of task + * @param description description of the task + * @param by date of the deadline + */ + public Deadlines(boolean status, String description, String by) { + super(status, description); + this.by = by; + } + + /** + * Overriden toString implementation for a deadline task + * @return String representing a deadline task + */ + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by + ")"; + } + + /** + * formats the properties of the object into a single string line for saving + * @return String of formatted properties + */ + @Override + public String dataFormat() { + return "deadline/" + + super.isDone.toString() + "/" + + super.description + "/" + + this.by; + } +} diff --git a/src/main/java/task/Events.java b/src/main/java/task/Events.java new file mode 100644 index 0000000000..814d648840 --- /dev/null +++ b/src/main/java/task/Events.java @@ -0,0 +1,56 @@ +package task; + +/** + * Object class for instances of event tasks + */ +public class Events extends Task { + protected String from; + protected String to; + + /** + * Public constructor of the class + * @param description description of the task + * @param from start date + * @param to end date + */ + public Events(String description, String from, String to) { + super(description); + this.from = from; + this.to = to; + } + + /** + * Overloaded constructor of the class for a known status + * @param status status of the task + * @param description description of the task + * @param from start date + * @param to end date + */ + public Events(boolean status, String description, String from, String to) { + super(status, description); + this.from = from; + this.to = to; + } + + /** + * Overriden toString implementation for a event task + * @return String representing a event task + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + from + " to: " + to + ")"; + } + + /** + * formats the properties of the object into a single string line for saving + * @return String of formatted properties + */ + @Override + public String dataFormat() { + return "event/" + + super.isDone.toString() + + "/" + super.description + "/" + + this.from + "/" + + this.to; + } +} diff --git a/src/main/java/task/Task.java b/src/main/java/task/Task.java new file mode 100644 index 0000000000..cf2706c9e4 --- /dev/null +++ b/src/main/java/task/Task.java @@ -0,0 +1,65 @@ +package task; + +/** + * Abstract class for all possible tasks + */ +public abstract class Task { + protected String description; + protected Boolean isDone; + + /** + * Public constructor of the class + * @param description description of the task + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Overloaded constructor to create classes with known status + * @param status done status of task + * @param description description of the task + */ + public Task(boolean status, String description) { + this.description = description; + this.isDone = status; + } + + /** + * Churns out a string 'X' if its complete and ' ' otherwise + * @return display icon + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); + } + + /** + * marks this task as done + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * marks this task as not done + */ + public void markAsNotDone() { + this.isDone = false; + } + + /** + * Overriden toString implementation for a generic task + * @return String representing a generic task + */ + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + description; + } + + /** + * abstract declaration of a method to format properties of the instance + * @return String of formatted data of the instance + */ + public abstract String dataFormat(); +} diff --git a/src/main/java/task/TaskManager.java b/src/main/java/task/TaskManager.java new file mode 100644 index 0000000000..b2d498abe4 --- /dev/null +++ b/src/main/java/task/TaskManager.java @@ -0,0 +1,181 @@ +package task; + +import java.io.IOException; +import java.util.ArrayList; + +import storage.Database; + +/** + * Class for manipulating the list of tasks + */ +public class TaskManager { + private static TaskManager obj; + private static final String FILE_ERROR_MSG = "There is an issue with the file database. " + + "You are required to delete the file in your User/[username]/EvanData folder " + + "and rerun the program."; + private ArrayList list; + + /** + * Private constructor for TaskManager + */ + private TaskManager() { + try { + list = Database.loadTasks(); + } catch (IOException | ArrayIndexOutOfBoundsException e) { + e.printStackTrace(); + } + + } + + /** + * Factory method to enforce one instance of the list manager + * @return an instance of the list manager + */ + public static TaskManager init() { + if (obj == null) { + obj = new TaskManager(); + } + return obj; + } + + /** + * Takes in a task, adds it into the list and saves it into the database + * @param task task to be added + * @return string message log of adding the task + */ + public String addTask(Task task) { + list.add(task); + + try { + Database.saveTasks(list); + } catch (IOException e) { + return FILE_ERROR_MSG; + } + + StringBuilder dialog = new StringBuilder("Got it. I've added this task:\n") + .append(task) + .append("\n") + .append("Now you have ") + .append(list.size()) + .append(" tasks in the list."); + return dialog.toString(); + } + + /** + * Gets a string report of all the tasks in the list + * @return String message log of all tasks in the list + */ + public String getAllTasks() { + Task[] tasks = list.toArray(new Task[0]); + StringBuilder dialog = new StringBuilder("Here are the tasks in your list:\n"); + + for (int i = 0; i < tasks.length; i++) { + int listIndex = i + 1; + Task task = tasks[i]; + dialog.append(listIndex) + .append("."); + + if (i < tasks.length - 1) { + dialog.append(task) + .append("\n"); + } else { + dialog.append(task); + } + } + + return dialog.toString(); + } + + /** + * Takes in an integer index and marks the task associated with the index as done + * @param index index of the task + * @return String message report of marking the task as done + */ + public String markDone(int index) { + Task element = list.get(index - 1); + element.markAsDone(); + + try { + Database.saveTasks(list); + } catch (IOException e) { + return FILE_ERROR_MSG; + } + + StringBuilder dialog = new StringBuilder(); + dialog.append("Nice! I've marked this task as done:\n") + .append(element); + return dialog.toString(); + } + + /** + * Takes in an integer index and marks the task associated with the index as not done + * @param index index of the task + * @return String message report of marking the task as not done + */ + public String unmarkDone(int index) { + Task element = list.get(index - 1); + element.markAsNotDone(); + + try { + Database.saveTasks(list); + } catch (IOException e) { + return FILE_ERROR_MSG; + } + + StringBuilder dialog = new StringBuilder(); + dialog.append("OK! I've marked this task as not done yet:\n") + .append(element); + return dialog.toString(); + } + + /** + * Takes in an integer index and deletes the task associated with the index + * @param index index of the task + * @return String message report of deleting task + */ + public String deleteTask(int index) { + + Task element = list.get(index - 1); + list.remove(index - 1); + + try { + Database.saveTasks(list); + } catch (IOException e) { + return FILE_ERROR_MSG; + } + + StringBuilder dialog = new StringBuilder(); + dialog.append("Noted. I've removed this task:\n") + .append(element) + .append("\n") + .append("Now you have ") + .append(list.size()) + .append(" tasks in the list."); + return dialog.toString(); + } + + /** + * Finds all tasks which contains the keyword and returns a string report of filtered tasks + * @param keyword in the task description + * @return String message report of all tasks with the keyword + */ + public String findTask(String keyword) { + StringBuilder dialog = new StringBuilder("Here are the matching tasks in your list with its correct " + + "corresponding index numbers: \n"); + + Task[] tasks = list.toArray(new Task[0]); + + for (int i = 0; i < tasks.length; i++) { + Task task = tasks[i]; + int listIndex = i + 1; + + if (task.description.contains(keyword)) { + dialog.append(listIndex) + .append(".") + .append(task) + .append("\n"); + } + } + return dialog.toString(); + } +} diff --git a/src/main/java/task/ToDo.java b/src/main/java/task/ToDo.java new file mode 100644 index 0000000000..683be4798f --- /dev/null +++ b/src/main/java/task/ToDo.java @@ -0,0 +1,44 @@ +package task; + +/** + * Object class for instances of todo tasks + */ +public class ToDo extends Task { + + /** + * Public constructor of the class + * @param description description of the task + */ + public ToDo(String description) { + super(description); + } + + /** + * Overloaded constructor to create classes with known status + * @param status done status of task + * @param description description of the task + */ + public ToDo(boolean status, String description) { + super(status, description); + } + + /** + * Overriden toString implementation for a todo task + * @return String representing a todo task + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + + /** + * abstract declaration of a method to format properties of the todo task + * @return String of formatted data of the instance + */ + @Override + public String dataFormat() { + return "todo/" + + isDone.toString() + "/" + + description; + } +} diff --git a/src/main/java/ui/DialogBox.java b/src/main/java/ui/DialogBox.java new file mode 100644 index 0000000000..1973e704c9 --- /dev/null +++ b/src/main/java/ui/DialogBox.java @@ -0,0 +1,61 @@ +package 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 getEvanDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} diff --git a/src/main/java/ui/Launcher.java b/src/main/java/ui/Launcher.java new file mode 100644 index 0000000000..6982c7c6c5 --- /dev/null +++ b/src/main/java/ui/Launcher.java @@ -0,0 +1,12 @@ +package ui; + +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/ui/Main.java b/src/main/java/ui/Main.java new file mode 100644 index 0000000000..ec9ff5c0c0 --- /dev/null +++ b/src/main/java/ui/Main.java @@ -0,0 +1,37 @@ +package ui; + +import java.io.IOException; + +import chatbot.evan.Evan; +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 Evan evan = new Evan(); + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + MainWindow window = fxmlLoader.getController(); + window.setEvan(evan); + window.onStart(); + stage.show(); + + //Initialise introductory message + + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/ui/MainWindow.java b/src/main/java/ui/MainWindow.java new file mode 100644 index 0000000000..dc6b720b7f --- /dev/null +++ b/src/main/java/ui/MainWindow.java @@ -0,0 +1,63 @@ +package ui; + +import chatbot.evan.Evan; +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 Evan evan; + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/smol.png")); + private Image evanImage = new Image(this.getClass().getResourceAsStream("/images/big.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + public void setEvan(Evan d) { + evan = 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 = evan.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getEvanDialog(response, evanImage) + ); + userInput.clear(); + } + + /** + * Display chatbot introductory message upon application start + */ + public void onStart() { + String message = evan.getIntro(); + dialogContainer.getChildren().addAll( + DialogBox.getEvanDialog(message, evanImage) + ); + } +} diff --git a/src/main/resources/images/big.png b/src/main/resources/images/big.png new file mode 100644 index 0000000000..7cd2ef6d11 Binary files /dev/null and b/src/main/resources/images/big.png differ diff --git a/src/main/resources/images/smol.png b/src/main/resources/images/smol.png new file mode 100644 index 0000000000..fc042e735c Binary files /dev/null and b/src/main/resources/images/smol.png differ diff --git a/src/main/resources/images/user.png b/src/main/resources/images/user.png new file mode 100644 index 0000000000..b0996709fc Binary files /dev/null and b/src/main/resources/images/user.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..59d16ac91a --- /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..40813675a0 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +