diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..c361f9811 --- /dev/null +++ b/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '7.1.2' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("seedu.duke.Duke") +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +run{ + standardInput = System.in +} diff --git a/data/pythia.txt b/data/pythia.txt new file mode 100644 index 000000000..e716bc412 --- /dev/null +++ b/data/pythia.txt @@ -0,0 +1,2 @@ +true | read +T | false | got apple diff --git a/docs/README.md b/docs/README.md index 47b9f984f..f15b0c564 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,225 @@ -# Duke User Guide +# Pythia User Guide +

+ Description +

-// Update the title above to match the actual product name +Pythia chatbot is desktop app **designed to help you manage all of your tasks, +optimized for use via command-line interface**. +It allows for adding tasks, deleting, marking and saving them for later. -// Product screenshot goes here +# Table of Contents +- [Feature overview](#feature-overview) +- [Add `Task`](#add-task) +- [Add `ToDo`](#add-todo) +- [Add `Event`](#add-event) +- [Add `Deadline`](#add-deadline) +- [View all with `list`](#list-tasks) +- [Delete a task](#task-delete) +- [Mark task as done](#mark-task) +- [Find a task](#find-task) +- [Exit the app](#exit) -// Product intro goes here +# Feature overview +App supports several types of commands with different functionalities and synatx. +All of them summarized in the table below. -## Adding deadlines +| Feature | Syntax | +|---------------------|------------------------------------------------------| +| **List all tasks** | `list` | +| **Add a task** | `add ` | +| **Add a todo** | `todo ` | +| **Add a deadline** | `deadline /by ` | +| **Add an event** | `event /from /to `| +| **Mark task as done** | `mark ` | +| **Delete a task** | `delete ` | +| **Find a task** | `find ` | +| **Quit the app** | `bye` | -// Describe the action and its outcome. +> **Tip:** +> To know `` it is advised to execute `list` command first -// Give examples of usage +> **Warning:** +> Once a task is deleted it cannot be recovered -Example: `keyword (optional arguments)` +## Add `Task` +Adds a task to the list of tasks. Use syntax `add ` +where: +- `` is a task description of the task such +as a name or anything desired -// A description of the expected outcome goes here +### Example: +``` +add My first task + ____________________________________________________________ + added: my first task + ____________________________________________________________ +list + ____________________________________________________________ + 1. [ ] My first task + Now you have 1 task in the list. + ____________________________________________________________ +``` + +## Add `ToDo` +Adds a `TdDo` to the list of tasks. Use syntax `todo ` +where: +- `` is a `ToDo` description of the task such +as a name or anything desired. + +### Example: +``` +todo My first todo + ____________________________________________________________ + added: my first todo + ____________________________________________________________ +list + ____________________________________________________________ + 1. [ ] My first task + 2. [T][ ] My first todo + Now you have 2 tasks in the list. + ____________________________________________________________ +``` + +> **Note:** +> Unless a task is not deleted it will be kept indefinitely. + +## Add `Event` +Adds an `Event` to the list of tasks. Use syntax `event /from /to ` +where: +- `` is an `Event` description +- `` is the time at which the event starts, any sequence of characters is accepted +- `` is the time at which the event end, any sequence of characters is accepted + + +### Example: +``` +event My first event /from now /to some time in the future + ____________________________________________________________ + added: my first event + ____________________________________________________________ +list + ____________________________________________________________ + 1. [ ] My first task + 2. [T][ ] My first todo + 3. [E][ ] My first event (from now to some time in the future) + Now you have 3 tasks in the list. + ____________________________________________________________ +``` + +## Add `Deadline` +Adds a `Deadline` to the list of tasks. Use syntax `deadline /by ` +where: +- `` is a `Deadline` description +- `` date by which the deadline is due, any sequence of characters is accepted +### Example: ``` -expected output +deadline My first deadline /by tomorrow :) + ____________________________________________________________ + added: my first deadline + ____________________________________________________________ +list + ____________________________________________________________ + 1. [ ] My first task + 2. [T][ ] My first todo + 3. [E][ ] My first event (from now to some time in the future) + 4. [D][ ] My first deadline (by: tomorrow :)) + Now you have 4 tasks in the list. + ____________________________________________________________ ``` -## Feature ABC +## View all with `list` +You can view all tasks in the list with `list` command. Once a task is added, +it will be kept indefinitely unless deleted explicitly. Look above for examples (like [Add `Deadline`](#add-deadline)) -// Feature details +## Delete a task +Deletes a `Task` from the list of tasks. Use syntax `delete ` +where: +- `` is a number of the task in the list +> **Tip:** +> To know `` it is advised to execute `list` command first as task numbers change upon deletion -## Feature XYZ +### Example: +``` +delete 1 + ____________________________________________________________ + Nice! I've deleted this task: + [ ] My first task + ____________________________________________________________ +list + ____________________________________________________________ + 1. [T][ ] My first todo + 2. [E][ ] My first event (from now to some time in the future) + 3. [D][ ] My first deadline (by: tomorrow :)) + Now you have 3 tasks in the list. + ____________________________________________________________ +``` + +## Mark task as done +Marks a `Task` as done. Use syntax `mark ` +where: +- `` is a number of the task in the list + +> **Tip:** +> To know `` it is advised to execute `list` command first as task numbers change upon deletion + +### Example: +``` +mark 1 + ____________________________________________________________ + Nice! I've marked this task as done: + [T][X] My first todo + ____________________________________________________________ +list + ____________________________________________________________ + 1. [T][X] My first todo + 2. [E][ ] My first event (from now to some time in the future) + 3. [D][ ] My first deadline (by: tomorrow :)) + Now you have 3 tasks in the list. + ____________________________________________________________ +``` -// Feature details \ No newline at end of file +## Find a task +Finds a `Task` with a keyword contained in the task description. Use syntax `find `. + +### Example: +``` +list + ____________________________________________________________ + 1. [T][X] My first todo + 2. [E][ ] My first event (from now to some time in the future) + 3. [D][ ] My first deadline (by: tomorrow :)) + Now you have 3 tasks in the list. + ____________________________________________________________ +add My second task + ____________________________________________________________ + added: my second task + ____________________________________________________________ +list + ____________________________________________________________ + 1. [T][X] My first todo + 2. [E][ ] My first event (from now to some time in the future) + 3. [D][ ] My first deadline (by: tomorrow :)) + 4. [ ] My second task + Now you have 4 tasks in the list. + ____________________________________________________________ +find first + ____________________________________________________________ + Here are the matching tasks in your list: + 1. [T][X] My first todo + 2. [E][ ] My first event (from now to some time in the future) + 3. [D][ ] My first deadline (by: tomorrow :)) + ____________________________________________________________ +``` + +## Exit the app +To exit the app use command `bye`. Changes are saved automatically. + +### Example: +``` +bye + ____________________________________________________________ + Your path is set. Until we meet again. + ____________________________________________________________ +``` \ No newline at end of file diff --git a/docs/Screenshot.png b/docs/Screenshot.png new file mode 100644 index 000000000..03df5dba0 Binary files /dev/null and b/docs/Screenshot.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..033e24c4c 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 000000000..66c01cfeb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..fcb6fca14 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..6689b85be --- /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 5d313334c..000000000 --- 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/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 000000000..e6b32ee44 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: pythia.Pythia + diff --git a/src/main/java/pythia/Pythia.java b/src/main/java/pythia/Pythia.java new file mode 100644 index 000000000..c9ceecda6 --- /dev/null +++ b/src/main/java/pythia/Pythia.java @@ -0,0 +1,220 @@ +package pythia; + +import pythia.utility.Parser; +import pythia.utility.Ui; +import pythia.task.Task; +import pythia.task.ToDo; +import pythia.task.Deadline; +import pythia.task.Event; +import pythia.exceptions.PythiaException; +import pythia.utility.Storage; +import pythia.utility.TaskList; + +/** + * Main class of the Pythia application. This class is responsible for managing + * the system's core components, including parsing user commands, storing and + * retrieving tasks, and interacting with the user through the user interface. + */ +public class Pythia { + private static String botName = "Pythia"; + private static String logo = + "____ _ _ _ \n" + + "| _ \\ _ _| |_| |__ (_) __ _ \n" + + "| |_) | | | | __| '_ \\| |/ _` |\n" + + "| __/| |_| | |_| | | | | (_| |\n" + + "|_| \\__, |\\__|_| |_|_|\\__,_|\n" + + " |___/ "; + + private static boolean isByeSaid; + private static TaskList taskList; + private static Storage storage; + private static Ui ui; + + /** + * Constructs a Pythia object that initializes the system with task + * storage and user interaction components. + * + * @param filePath Path to the file where tasks are saved and loaded from. + */ + public Pythia(String filePath) { + isByeSaid = false; + storage = new Storage(filePath); + taskList = storage.load(); + ui = new Ui(); + } + + /** + * Greets the user by printing a welcome message to the console. + */ + public static void greet() { + String helloMsg = "Welcome, seeker. I am " + botName + ".\n" + + "What brings you here?"; + ui.printResponse(helloMsg); + } + + /** + * Prints a farewell message to the user and sets the exit flag. + */ + public static void sayBye() { + String byeMsg = "Your path is set. Until we meet again."; + ui.printResponse(byeMsg); + isByeSaid = true; + } + + /** + * Displays the current list of tasks, along with the total number of + * remaining tasks. + */ + public static void listTasks() { + StringBuilder comment = new StringBuilder(); + int remainingTasks = taskList.getNumberOfRemainingTasks(); + + comment.append("Now you have ").append(remainingTasks); + if (remainingTasks == 1) { + comment.append(" task in the list."); + } else { + comment.append(" tasks in the list."); + } + + ui.printTaskList(taskList, "", comment.toString()); + } + + /** + * Adds a new generic task with the given name to the task list. + * + * @param taskName The name of the task to be added. + */ + public static void addTask(String taskName) { + taskList.add(new Task(taskName)); + ui.printAddedTask("added: " + taskName); + storage.save(taskList); + } + + /** + * Adds a specific {@link Task} object to the task list. + * + * @param task The task to be added to the list. + */ + public static void addTask(Task task) { + taskList.add(task); + ui.printAddedTask("added: " + task.getName()); + storage.save(taskList); + } + + /** + * Adds a new {@link ToDo} task to the task list. + * + * @param todoName The name of the ToDo task to be added. + */ + public static void addToDo(String todoName) { + addTask(new ToDo(todoName)); + } + + /** + * Adds a new {@link Deadline} task to the task list with a specified due date. + * + * @param deadlineName The name of the deadline task. + * @param dueDate The due date of the deadline task. + */ + public static void addDeadline(String deadlineName, String dueDate) { + addTask(new Deadline(deadlineName, dueDate)); + } + + /** + * Adds a new {@link Event} task to the task list with specified start and end dates. + * + * @param eventName The name of the event task. + * @param startDate The start date of the event. + * @param endDate The end date of the event. + */ + public static void addEvent(String eventName, String startDate, String endDate) { + addTask(new Event(eventName, startDate, endDate)); + } + + /** + * Marks a task as completed by its task number. + * + * @param taskNumber The number of the task in the list (1-based index). + */ + public static void markTask(Integer taskNumber) { + try { + taskList.markAsDone(taskNumber - 1); + String msg = "Nice! I've marked this task as done:\n\t" + taskList.get(taskNumber - 1).toString(); + ui.printResponse(msg); + storage.save(taskList); + } catch (IndexOutOfBoundsException e) { + ui.printResponse("There is no such task :("); + } + } + + /** + * Deletes a task from the list by its task number. + * + * @param taskNumber The number of the task to be deleted (1-based index). + */ + public static void deleteTask(Integer taskNumber) { + try { + String msg = "Nice! I've deleted this task:\n\t" + taskList.get(taskNumber - 1).toString(); + taskList.remove(taskNumber - 1); + ui.printResponse(msg); + storage.save(taskList); + } catch (IndexOutOfBoundsException e){ + ui.printResponse("There is no such task :("); + } + } + + /** + * Finds tasks that contain the specified keyword in their name and displays + * them to the user. + * + * @param taskKeyword The keyword to search for in task names. + */ + public static void findTasks(String taskKeyword) { + TaskList filteredTaskList = new TaskList(); + for (Task task : taskList) { + String taskName = task.getName(); + if (taskName.contains(taskKeyword)) { + filteredTaskList.add(task); + } + } + + if (filteredTaskList.getNumberOfTasks() == 0) { + ui.printResponse("There is no such task :("); + } else { + String commentBefore = "Here are the matching tasks in your list:"; + String commentAfter = ""; + ui.printTaskList(filteredTaskList, commentBefore, commentAfter); + } + } + + /** + * Runs the main logic of the Pythia application. Continuously accepts user + * input and executes parsed commands until the user says "bye". + */ + private static void run() { + ui.init(); + greet(); + Parser parser = new Parser(); + + while (!isByeSaid) { + try { + String request = ui.getRequest(); + parser.parse(request); + parser.execute(); + } catch (PythiaException e) { + ui.printResponse(e.getUserMessage()); + } + } + } + + /** + * The main method that runs the Pythia application. It initializes the UI, + * greets the user, and enters a loop that processes user input until the user + * says "bye". + * + * @param args Command-line arguments (unused). + */ + public static void main(String[] args) { + new Pythia("data/pythia.txt").run(); + } +} diff --git a/src/main/java/pythia/exceptions/PythiaException.java b/src/main/java/pythia/exceptions/PythiaException.java new file mode 100644 index 000000000..1e188b167 --- /dev/null +++ b/src/main/java/pythia/exceptions/PythiaException.java @@ -0,0 +1,25 @@ +package pythia.exceptions; + +/** + * Represents a custom exception for the Pythia task management system. + * This exception extends {@link RuntimeException} and is used to signal + * errors that occur during the execution of Pythia's functionalities. + */ +public class PythiaException extends RuntimeException { + private String userMessage; + + /** + * Constructs a PythiaException with the specified detail message and user message. + * + * @param message The detail message for the exception, which is logged for debugging purposes. + * @param userMessage A user-friendly message that can be displayed to the user. + */ + public PythiaException(String message, String userMessage) { + super(message); + this.userMessage = userMessage; + } + + public String getUserMessage() { + return userMessage; + } +} diff --git a/src/main/java/pythia/task/Deadline.java b/src/main/java/pythia/task/Deadline.java new file mode 100644 index 000000000..22fbccfa3 --- /dev/null +++ b/src/main/java/pythia/task/Deadline.java @@ -0,0 +1,63 @@ +package pythia.task; + +import pythia.utility.WriteVisitor; + +import java.util.Date; + +/** + * Represents a deadline task that is part of the task management system. + * A deadline task includes a name and a due date, and can be marked as done or not done. + */ +public class Deadline extends Task { + private String dueDate; + private Date dueDateDay; + + /** + * Constructs a Deadline with the specified name and due date. + * The deadline is initially marked as not done. + * + * @param name The name of the deadline. + * @param dueDate The due date of the deadline. + */ + public Deadline(String name, String dueDate) { + super(name); + this.dueDate = dueDate; + } + + /** + * Constructs a Deadline with the specified name, done status, and due date. + * + * @param name The name of the deadline. + * @param isDone The initial status of the deadline, indicating whether it is done or not. + * @param dueDate The due date of the deadline. + */ + public Deadline(String name, boolean isDone, String dueDate) { + super(name); + this.dueDate = dueDate; + } + + public String getDueDate() { + return dueDate; + } + + /** + * Returns a string representation of the deadline, including its status and due date. + * + * @return A string indicating the type of task, its status, and its due date. + */ + @Override + public String toString() { + return "[D]" + super.toString() + " " + "(by: " + dueDate + ")"; + } + + /** + * Accepts a {@link WriteVisitor} to perform a write operation on the deadline. + * + * @param visitor The visitor that handles writing the deadline's data to a storage format. + * @return A string representation of the deadline's data formatted for saving. + */ + @Override + public String accept(WriteVisitor visitor) { + return visitor.visitDeadline(this); + } +} \ No newline at end of file diff --git a/src/main/java/pythia/task/Event.java b/src/main/java/pythia/task/Event.java new file mode 100644 index 000000000..d8b2fb7a2 --- /dev/null +++ b/src/main/java/pythia/task/Event.java @@ -0,0 +1,73 @@ +package pythia.task; + +import pythia.utility.WriteVisitor; + +import java.util.Date; + +/** + * Represents an event task that is part of the task management system. + * An event task includes a name, a start date, and an end date, and can be marked as done or not done. + */ +public class Event extends Task { + private String startDate; + private Date startDateDay; + private String endDate; + private Date endDateDay; + + /** + * Constructs an Event with the specified name, start date, and end date. + * The event is initially marked as not done. + * + * @param name The name of the event. + * @param startDate The start date of the event. + * @param endDate The end date of the event. + */ + public Event(String name, String startDate, String endDate) { + super(name); + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * Constructs an Event with the specified name, done status, start date, and end date. + * + * @param name The name of the event. + * @param isDone The initial status of the event, indicating whether it is done or not. + * @param startDate The start date of the event. + * @param endDate The end date of the event. + */ + public Event(String name, boolean isDone, String startDate, String endDate) { + super(name, isDone); + this.startDate = startDate; + this.endDate = endDate; + } + + public String getStartDate() { + return startDate; + } + + public String getEndDate() { + return endDate; + } + + /** + * Returns a string representation of the event, including its status and date range. + * + * @return A string indicating the type of task, its status, and its date range. + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from " + startDate + " to " + endDate + ")"; + } + + /** + * Accepts a {@link WriteVisitor} to perform a write operation on the event. + * + * @param visitor The visitor that handles writing the event's data to a storage format. + * @return A string representation of the event's data formatted for saving. + */ + @Override + public String accept(WriteVisitor visitor) { + return visitor.visitEvent(this); + } +} diff --git a/src/main/java/pythia/task/Savable.java b/src/main/java/pythia/task/Savable.java new file mode 100644 index 000000000..4abc90ca5 --- /dev/null +++ b/src/main/java/pythia/task/Savable.java @@ -0,0 +1,18 @@ +package pythia.task; + +import pythia.utility.WriteVisitor; + +/** + * Represents a contract for objects that can be saved. + * Classes implementing this interface must define how they interact with a + * {@link WriteVisitor} for the purpose of saving their state or data. + */ +public interface Savable { + /** + * Accepts a {@link WriteVisitor} to perform a write operation on the implementing object. + * + * @param visitor The visitor that handles writing the object's data to a storage format. + * @return A string representation of the object's data formatted for saving. + */ + public String accept(WriteVisitor visitor); +} diff --git a/src/main/java/pythia/task/Task.java b/src/main/java/pythia/task/Task.java new file mode 100644 index 000000000..18f88d641 --- /dev/null +++ b/src/main/java/pythia/task/Task.java @@ -0,0 +1,65 @@ +package pythia.task; + +import pythia.utility.WriteVisitor; + +/** + * Represents a task with a name and a completion status (whether it's done or not). + * It provides methods for marking the task as done or not done, and allows for + * string representations of the task for display and saving purposes. + */ +public class Task implements Savable { + private String name; + private boolean isDone; + + public Task(String name) { + this.name = name; + this.isDone = false; + } + + public Task(String name, boolean isDone) { + this.name = name; + this.isDone = isDone; + } + + public void markAsDone() { + this.isDone = true; + } + + public void markAsNotDone() { + this.isDone = false; + } + + public String getName() { + return name; + } + + public boolean getDone() { + return isDone; + } + + /** + * Returns a string representation of the task. + * The string indicates whether the task is done or not, followed by the task name. + * + * @return A string in the format: "[X] " if done, "[ ] " if not done. + */ + @Override + public String toString() { + if (isDone == false) { + return "[ ] " + name; + } else { + return "[X] " + name; + } + } + + /** + * Accepts a visitor to perform a write operation on this task for saving purposes. + * + * @param visitor The {@link WriteVisitor} that handles writing the task to a storage format. + * @return The formatted string representation of this task. + */ + @Override + public String accept(WriteVisitor visitor) { + return visitor.visitTask(this); + } +} diff --git a/src/main/java/pythia/task/ToDo.java b/src/main/java/pythia/task/ToDo.java new file mode 100644 index 000000000..4cc717a33 --- /dev/null +++ b/src/main/java/pythia/task/ToDo.java @@ -0,0 +1,38 @@ +package pythia.task; + +import pythia.utility.WriteVisitor; + +/** + * Represents a to-do task that is part of the task management system. + * A to-do task can be marked as done or not done and has a name associated with it. + */ +public class ToDo extends Task { + public ToDo(String name) { + super(name); + } + + public ToDo(String name, boolean isDone) { + super(name, isDone); + } + + /** + * Returns a string representation of the to-do task, including its status. + * + * @return A string indicating the type of task and its status. + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + + /** + * Accepts a {@link WriteVisitor} to perform a write operation on the to-do task. + * + * @param visitor The visitor that handles writing the task's data to a storage format. + * @return A string representation of the to-do task's data formatted for saving. + */ + @Override + public String accept(WriteVisitor visitor) { + return visitor.visitToDo(this); + } +} \ No newline at end of file diff --git a/src/main/java/pythia/utility/Parser.java b/src/main/java/pythia/utility/Parser.java new file mode 100644 index 000000000..0085368b9 --- /dev/null +++ b/src/main/java/pythia/utility/Parser.java @@ -0,0 +1,215 @@ +package pythia.utility; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import pythia.exceptions.PythiaException; +import pythia.Pythia; + +/** + * The {@code Parser} class is responsible for parsing and executing + * user commands in the Pythia application. + * It validates commands, extracts necessary arguments, and invokes + * corresponding actions. + */ +public class Parser { + private String commandType = null; + private ArrayList argumentList = null; + private String parsingErrorMessage = "Parsing of add command unsuccessful."; + + /** + * Constructs a {@code Parser} object. + */ + public Parser() { + } + + /** + * Parses the command type from the given raw text. + * The command type is identified as the first word before any spaces. + * + * @param rawText the whole, unaltered command input from the user + * @return the command type as a string + */ + private String parseCommandType(String rawText) { + int spaceIndex = rawText.indexOf(' '); + + String firstPart; + if (spaceIndex != -1) { + firstPart = rawText.substring(0, spaceIndex); + } else { + firstPart = rawText; + } + return firstPart; + } + + /** + * Handles parsing for the "bye" command. + * As no arguments are required for this command, this method is intentionally left empty. + * + * @param rawText the whole, unaltered command input from the user + */ + private void parseBye(String rawText) { + } + + /** + * Handles parsing for the "list" command. + * As no arguments are required for this command, this method is intentionally left empty. + * + * @param rawText the whole, unaltered command input from the user + */ + private void parseList(String rawText) { + } + + /** + * Parses the "add" command, checking if the input format is correct. + * + * @param rawText the whole, unaltered command input from the user + * @throws PythiaException if the command lacks a valid task description + */ + private void parseAdd(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "add\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 1; + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "I am not sure what to add."); + } + } + + /** + * Parses the "mark" command, ensuring it includes a valid task number. + * + * @param rawText the whole, unaltered command input from the user + * @throws PythiaException if the command lacks a valid task number + */ + private void parseMark(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "mark\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 1 && argumentList.get(0).matches("\\d+"); + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "Please specify what should I mark as done."); + } + } + + private void parseToDo(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "todo\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 1; + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "Todo should have a description."); + } + } + + private void parseDeadline(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "deadline\\s(.+)\\s/by\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 2; + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "Deadline should have description and a date."); + } + } + + private void parseEvent(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "event\\s(.+)\\s/from\\s(.+)\\s/to\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 3; + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "Event should have description and from and to dates."); + } + } + + public void parseDelete(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "delete\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 1 && argumentList.get(0).matches("\\d+"); + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "Please specify what should I delete."); + } + } + + public void parseFind(String rawText) throws PythiaException { + argumentList = tokenize(rawText, "find\\s(.+)"); + boolean isCorrectFormat = argumentList.size() == 1; + + if (!isCorrectFormat) { + throw new PythiaException(parsingErrorMessage, "Please specify what should I find."); + } + } + + /** + * Splits the given command string into tokens based on the provided regular expression. + * + *

Example usage:

+ *
{@code
+     * String rawText = "event  /from  /to ";
+     * String regex = "event\\s(.+)\\s/from\\s(.+)\\s/to\\s(.+)";
+     * ArrayList tokens = tokenize(rawText, regex);
+     * // Resulting tokens = {, , }
+     * }
+ * + * @param rawText the whole, unaltered command input from the user + * @param regex the regular expression used to capture command arguments + * @return a list of tokens extracted from the input text + */ + private ArrayList tokenize(String rawText, String regex) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(rawText); + + ArrayList tokens = new ArrayList<>(); + + if (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + tokens.add(matcher.group(i)); // Get specific captured groups + } + } + + return tokens; + } + + /** + * Parses the entire command input and determines which specific parsing method to call. + * The command is identified by its type and validated accordingly. + * + * @param rawText the whole, unaltered command input from the user + * @throws PythiaException if the command is invalid + */ + public void parse(String rawText) throws PythiaException { + commandType = parseCommandType(rawText); + + switch (commandType) { + case "bye" -> parseBye(rawText); + case "list" -> parseList(rawText); + case "add" -> parseAdd(rawText); + case "mark" -> parseMark(rawText); + case "todo" -> parseToDo(rawText); + case "deadline" -> parseDeadline(rawText); + case "event" -> parseEvent(rawText); + case "delete" -> parseDelete(rawText); + case "find" -> parseFind(rawText); + } + } + + /** + * Executes the parsed command by invoking the corresponding method in the {@link Pythia} class. + * If no command has been parsed, it throws a {@link PythiaException}. + * + * @throws PythiaException if no command was parsed or the command is unrecognized + */ + public void execute() throws PythiaException { + if (commandType == null) { + throw new PythiaException(parsingErrorMessage, "Sorry, I am busy."); + } + + switch (commandType) { + case "bye" -> Pythia.sayBye(); + case "list" -> Pythia.listTasks(); + case "add" -> Pythia.addTask(argumentList.get(0)); + case "mark" -> Pythia.markTask(Integer.parseInt(argumentList.get(0))); + case "todo" -> Pythia.addToDo(argumentList.get(0)); + case "deadline" -> Pythia.addDeadline(argumentList.get(0), argumentList.get(1)); + case "event" -> Pythia.addEvent(argumentList.get(0), argumentList.get(1), argumentList.get(2)); + case "delete" -> Pythia.deleteTask(Integer.parseInt(argumentList.get(0))); + case "find" -> Pythia.findTasks(argumentList.get(0)); + default -> throw new PythiaException(parsingErrorMessage, "Hmm. I am not sure what you mean."); + } + } +} \ No newline at end of file diff --git a/src/main/java/pythia/utility/Storage.java b/src/main/java/pythia/utility/Storage.java new file mode 100644 index 000000000..b60b94593 --- /dev/null +++ b/src/main/java/pythia/utility/Storage.java @@ -0,0 +1,97 @@ +package pythia.utility; + +import pythia.task.Task; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.io.*; + +/** + * The {@code Storage} class is responsible for saving + * and loading {@link TaskList} objects to and from a file. + * It provides methods to serialize a task list into a file + * and deserialize a task list from a file. + */ +public class Storage { + private String filePath; + private WriteVisitor writeVisitor; + + /** + * Constructs a {@code Storage} object with the specified file path. + * + * @param filePath the path to the file where the {@link TaskList} + * will be stored or read from + */ + public Storage(String filePath) { + this.filePath = filePath; + this.writeVisitor = new WriteVisitor(); + } + + /** + * Saves the given {@link TaskList} to a file at the specified path. + * Each task in the task list is serialized into a string representation + * using the {@link WriteVisitor} and written to the file. + * + * @param taskList the {@link TaskList} object to be saved to the file + */ + public void save(TaskList taskList) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, false))) { + for (Task task : taskList) { + String taskAsString = task.accept(writeVisitor); + writer.write(taskAsString); + writer.newLine(); + } + } catch (IOException e) { + System.err.println("Error writing to file: " + e.getMessage()); + } + } + + /** + * Loads a {@link TaskList} from the file at the specified path. + * Each line in the file is deserialized into a {@link Task} object using + * the {@link TaskFromStringFactory} and added to a new task list. + * + * @return the newly created {@link TaskList} object loaded from the file + */ + public TaskList load() { + TaskList taskList = new TaskList(); + TaskFromStringFactory factory = new TaskFromStringFactory(); + Path path = Paths.get(filePath); + Path directory = path.getParent(); // Get the directory part of the path + + // Check if the directory exists, if not create it + if (directory != null && Files.notExists(directory)) { + try { + Files.createDirectories(directory); // Create all missing directories + } catch (IOException e) { + System.err.println("Error creating directory: " + e.getMessage()); + return taskList; // Return empty task list if directory creation fails + } + } + + // Check if the file exists, if not create an empty file + if (Files.notExists(path)) { + try { + Files.createFile(path); + } catch (IOException e) { + System.err.println("Error creating new file: " + e.getMessage()); + return taskList; // Return empty task list if file creation fails + } + return taskList; // Return empty task list for a newly created file + } + + // If file exists, read the tasks from the file + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String taskAsString; + while ((taskAsString = reader.readLine()) != null) { + Task task = factory.create(taskAsString); + taskList.add(task); + } + } catch (IOException e) { + System.err.println("Error reading from file: " + e.getMessage()); + } + + return taskList; + } +} diff --git a/src/main/java/pythia/utility/TaskFromStringFactory.java b/src/main/java/pythia/utility/TaskFromStringFactory.java new file mode 100644 index 000000000..1f5079c2c --- /dev/null +++ b/src/main/java/pythia/utility/TaskFromStringFactory.java @@ -0,0 +1,84 @@ +package pythia.utility; + +import pythia.task.Deadline; +import pythia.task.Event; +import pythia.task.Task; +import pythia.task.ToDo; + +/** + * A factory class used for creating {@link Task} objects and its + * derived classes ({@link ToDo}, {@link Event}, {@link Deadline}) + * from a string representation. The factory parses the string and + * creates the appropriate type of task based on the information. + */ +public class TaskFromStringFactory { + /** + * Creates a new {@link Task} object or one of its derived classes + * from the given string representation. The string must follow + * a specific format with fields separated by " | ". The first field + * determines the type of task: + *
    + *
  • {@code "T"} - {@link ToDo}
  • + *
  • {@code "E"} - {@link Event}
  • + *
  • {@code "D"} - {@link Deadline}
  • + *
  • Any other identifier creates a generic {@link Task}
  • + *
+ * + * @param taskAsString a string representation of the task, + * with fields separated by " | " + * @return an object of the appropriate task type ({@link ToDo}, + * {@link Event}, {@link Deadline}, or {@link Task}) + * @throws IllegalArgumentException if the string doesn't have at + * least two fields or the type identifier is missing + */ + public Task create(String taskAsString) { + String[] fields = taskAsString.split(" \\| "); + + if (fields.length < 2) { + throw new IllegalArgumentException("Task should have at least 2 fields"); + } + + String typeIdentifier = fields[0]; + return switch (typeIdentifier) { + case "T" -> createToDo(fields); + case "E" -> createEvent(fields); + case "D" -> createDeadline(fields); + default -> createTask(fields); + }; + } + + /** + * Creates a generic {@link Task} from the parsed string fields. + * This is used if no specific type identifier is found in the string. + * + * @param fields an array of string fields representing the task attributes + * @return a new {@link Task} object + */ + private Task createTask(String[] fields) { + boolean isDone = Boolean.parseBoolean(fields[0]); + String name = fields[1]; + return new Task(name, isDone); + } + + private ToDo createToDo(String[] fields) { + boolean isDone = Boolean.parseBoolean(fields[1]); + String name = fields[2]; + return new ToDo(name, isDone); + } + + private Event createEvent(String[] fields) { + boolean isDone = Boolean.parseBoolean(fields[1]); + String name = fields[2]; + String startDate = fields[3]; + String endDate = fields[4]; + return new Event(name, isDone, startDate, endDate); + } + + private Deadline createDeadline(String[] fields) { + boolean isDone = Boolean.parseBoolean(fields[1]); + String name = fields[2]; + + String dueDate = fields[3]; + return new Deadline(name, isDone, dueDate); + } +} diff --git a/src/main/java/pythia/utility/TaskList.java b/src/main/java/pythia/utility/TaskList.java new file mode 100644 index 000000000..8721496c9 --- /dev/null +++ b/src/main/java/pythia/utility/TaskList.java @@ -0,0 +1,122 @@ +package pythia.utility; +import pythia.task.Task; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Class used for storing and managing a list of {@link Task} objects. + * This class allows adding, removing, marking tasks as done, and + * iterating over the task list. It also keeps track of the number of + * remaining tasks that have not been completed. + */ +public class TaskList implements Iterable { + private ArrayList taskList = new ArrayList(); + private int remainingTasks = 0; // number of tasks which are not done yet + // might be different from array size + /** + * Constructs an empty {@code TaskList}. + */ + public TaskList() {} + + /** + * Returns the number of tasks that have not been marked as done. + * + * @return the number of remaining tasks + */ + public int getNumberOfRemainingTasks() { + return remainingTasks; + } + + /** + * Returns the total number of tasks in the list, regardless of their completion status. + * + * @return the total number of tasks + */ + public int getNumberOfTasks() { + return taskList.size(); + } + + /** + * Adds a new task to the list and increases the count of remaining tasks. + * + * @param task the {@link Task} to be added to the list + */ + public void add(Task task) { + taskList.add(task); + remainingTasks++; + } + + /** + * Removes the task at the specified index from the list and decreases the count of remaining tasks. + * + * @param taskNumber the index of the task to be removed + * @throws IndexOutOfBoundsException if the index is out of range (less than 0 or greater than or equal to the list size) + */ + public void remove(int taskNumber) throws IndexOutOfBoundsException { + if (taskNumber < 0 || taskNumber >= taskList.size()) { + throw new IndexOutOfBoundsException(); + } + taskList.remove(taskNumber); + remainingTasks--; + } + + /** + * Returns the task at the specified index in the list. + * + * @param taskNumber the index of the task to retrieve + * @return the {@link Task} at the specified index + * @throws IndexOutOfBoundsException if the index is out of range (less than 0 or greater than or equal to the list size) + */ + public Task get(int taskNumber) { + return taskList.get(taskNumber); + } + + /** + * Marks the task at the specified index as done and decreases the count of remaining tasks. + * + * @param taskNumber the index of the task to mark as done + * @throws IndexOutOfBoundsException if the index is out of range (less than 0 or greater than or equal to the list size) + */ + public void markAsDone(int taskNumber) { + if (taskNumber < 0 || taskNumber >= taskList.size()) { + throw new IndexOutOfBoundsException(); + } + taskList.get(taskNumber).markAsDone(); + remainingTasks--; + } + + /** + * Returns an iterator over the tasks in the list in proper sequence. + * + * @return an {@link Iterator} over the tasks in the list + */ + @Override + public Iterator iterator() { + return taskList.iterator(); + } + + /** + * Returns a string representation of the task list. Each task is prefixed with its index number + * and listed in sequence. The tasks are separated by new lines. + * + * @return a string representing the task list + */ + @Override + public String toString() { + if (taskList.isEmpty()) { + return ""; + } + + StringBuilder taskListString = new StringBuilder(); + for (int i = 0; i < taskList.size() - 1; i++) { + taskListString.append(i + 1).append(". "); + taskListString.append(taskList.get(i).toString()); + taskListString.append("\n"); + } + int lastIndex = taskList.size() - 1; + taskListString.append(lastIndex + 1).append(". "); + taskListString.append(taskList.get(lastIndex).toString()); + + return taskListString.toString(); + } +} diff --git a/src/main/java/pythia/utility/Ui.java b/src/main/java/pythia/utility/Ui.java new file mode 100644 index 000000000..7c05aa55c --- /dev/null +++ b/src/main/java/pythia/utility/Ui.java @@ -0,0 +1,74 @@ +package pythia.utility; + +import java.util.Scanner; + +/** + * A class responsible for handling user interaction and displaying information to the console. + * It provides methods to display responses, get user input, and format task lists for output. + */ +public class Ui { + private static Scanner scanner;; + + /** + * Initializes the scanner object for reading user input. + */ + public void init() { + scanner = new Scanner(System.in); + } + + /** + * Prints a formatted response to the console with line separators. + * + * @param text The message to be printed. + */ + public void printResponse(String text) { + String lineSeparator = "____________________________________________________________"; + text = lineSeparator + "\n" + text + "\n" + lineSeparator + "\n"; + String formattedText = text.replaceAll("(?m)^", "\t"); + System.out.print(formattedText); + } + + /** + * Retrieves a user's input from the console. + * + * @return The raw string input from the user. + */ + public String getRequest() { + return scanner.nextLine(); + } + + /** + * Prints a response for when a task is added, formatting the text to be all lowercase. + * + * @param text The message to be printed, typically confirming a task addition. + */ + public void printAddedTask(String text) { + printResponse(text.toLowerCase()); + } + + /** + * Prints the entire task list to the console, including optional comments + * before and after the list. + * + * @param taskList The list of tasks to be printed. + * @param commentBefore An optional comment to be printed before the task list. + * @param commentAfter An optional comment to be printed after the task list. + */ + public void printTaskList(TaskList taskList, String commentBefore, String commentAfter) { + StringBuilder response = new StringBuilder(); + if (commentBefore != null && !commentBefore.isEmpty()) { + response.append(commentBefore).append("\n"); + } + + String taskListAsString = taskList.toString(); + if (taskListAsString != null && !taskListAsString.isEmpty()) { + response.append(taskListAsString); + } + + if (commentAfter != null && !commentAfter.isEmpty()) { + response.append("\n").append(commentAfter); + } + + printResponse(response.toString()); + } +} diff --git a/src/main/java/pythia/utility/WriteVisitor.java b/src/main/java/pythia/utility/WriteVisitor.java new file mode 100644 index 000000000..3530b832f --- /dev/null +++ b/src/main/java/pythia/utility/WriteVisitor.java @@ -0,0 +1,57 @@ +package pythia.utility; + +import pythia.task.Deadline; +import pythia.task.Event; +import pythia.task.Task; +import pythia.task.ToDo; + +/** + * A visitor class responsible for converting various task objects ({@link Task}, {@link ToDo}, + * {@link Deadline}, {@link Event}) into their respective string representations. The resulting + * string format is suitable for storage in text files. + */ +public class WriteVisitor { + + /** + * Generates a string representation of a generic task. + * + * @param task The task to be converted to a string. + * @return A string representing the task in the format "doneStatus | taskName". + */ + public String visitTask(Task task) { + return task.getDone() + " | " + task.getName(); + } + + /** + * Generates a string representation of a {@link ToDo} task. + * + * @param toDo The {@link ToDo} task to be converted to a string. + * @return A string representing the {@link ToDo} task in the format "T | doneStatus | taskName". + */ + public String visitToDo(ToDo toDo) { + return "T" + " | " + visitTask(toDo); + } + + /** + * Generates a string representation of an {@link Event} task, including its start + * and end dates. + * + * @param event The {@link Event} task to be converted to a string. + * @return A string representing the {@link Event} task in the format + * "E | doneStatus | taskName | startDate | endDate". + */ + public String visitEvent(Event event) { + return "E" + " | " + visitTask(event) + " | " + event.getStartDate() + " | " + event.getEndDate(); + } + + /** + * Generates a string representation of a Deadline task, including its due date. + * + * @param deadline The {@link Deadline} task to be converted to a string. + * @return A string representing the {@link Deadline} task in the format + * "D | doneStatus | taskName | dueDate". + */ + public String visitDeadline(Deadline deadline) { + return "D" + " | " + visitTask(deadline) + " | " + deadline.getDueDate(); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 657e74f6e..8a3016cae 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,7 +1,53 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - + ____________________________________________________________ + Welcome, seeker. I am Pythia. + What brings you here? + ____________________________________________________________ + ____________________________________________________________ + Now you have 0 tasks in the list. + ____________________________________________________________ + ____________________________________________________________ + added: read a book + ____________________________________________________________ + ____________________________________________________________ + 1. [ ] read a book + Now you have 1 task in the list. + ____________________________________________________________ + ____________________________________________________________ + added: borrow book + ____________________________________________________________ + ____________________________________________________________ + 1. [ ] read a book + 2. [T][ ] borrow book + Now you have 2 tasks in the list. + ____________________________________________________________ + ____________________________________________________________ + Nice! I've marked this task as done: + [T][X] borrow book + ____________________________________________________________ + ____________________________________________________________ + 1. [ ] read a book + 2. [T][X] borrow book + Now you have 1 task in the list. + ____________________________________________________________ + ____________________________________________________________ + added: return book + ____________________________________________________________ + ____________________________________________________________ + 1. [ ] read a book + 2. [T][X] borrow book + 3. [D][ ] return book (by: Sunday) + Now you have 2 tasks in the list. + ____________________________________________________________ + ____________________________________________________________ + added: project meeting + ____________________________________________________________ + ____________________________________________________________ + 1. [ ] read a book + 2. [T][X] borrow book + 3. [D][ ] return book (by: Sunday) + 4. [E][ ] project meeting (from Mon 2pm to 4pm) + Now you have 3 tasks in the list. + ____________________________________________________________ + ____________________________________________________________ + Your path is set. Until we meet again. + ____________________________________________________________ diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29bb..9f5d4ae31 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1,12 @@ +list +add read a book +list +todo borrow book +list +mark 2 +list +deadline return book /by Sunday +list +event project meeting /from Mon 2pm /to 4pm +list +bye \ No newline at end of file diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh old mode 100644 new mode 100755 index c9ec87003..d67b4adb5 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -20,7 +20,7 @@ then fi # run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT -java -classpath ../bin Duke < input.txt > ACTUAL.TXT +java -classpath ../bin Pythia < input.txt > ACTUAL.TXT # convert to UNIX format cp EXPECTED.TXT EXPECTED-UNIX.TXT