diff --git a/FETCH_HEAD b/FETCH_HEAD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Ui.png b/Ui.png new file mode 100644 index 0000000000..165195e7ad Binary files /dev/null and b/Ui.png differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..b774351d4b --- /dev/null +++ b/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'application' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'checkstyle' + id 'org.openjfx.javafxplugin' version '0.1.0' +} + +group = 'org.example' +version = '0.1.0' + +repositories { + mavenCentral() +} + +application { + // GUI entrypoint (change to your launcher class) + mainClass = 'duke.Launcher' +} + +javafx { + version = '21.0.3' + modules = ['javafx.controls', 'javafx.fxml'] +} + +tasks.shadowJar { + archiveClassifier.set('all') +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' +} + +test { + useJUnitPlatform() +} + diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..0fce6c8996 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,450 @@ +<<<<<<< HEAD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +======= + + + + + + + + + + + +>>>>>>> branch-A-Assertions + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..39efb6e4ac --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/data/duke.txt b/data/duke.txt new file mode 100644 index 0000000000..2ea44e1fab --- /dev/null +++ b/data/duke.txt @@ -0,0 +1,2 @@ +T | 0 | read book +T | 0 | read book diff --git a/docs/AI.md b/docs/AI.md new file mode 100644 index 0000000000..16b7276719 --- /dev/null +++ b/docs/AI.md @@ -0,0 +1,9 @@ +# AI Assistance in Bob the TaskBot + +This project used AI tools to assist in development: + +- **Tool used:** ChatGPT (OpenAI) +- **How it helped:** + - Adding new features (e.g., `edit` command for C-Update). + - Improving documentation (UG.md). + - Explaining Gradle setup issues. diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..30ad35149a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,96 @@ -# Duke User Guide -// Update the title above to match the actual product name +# Bob User Guide -// Product screenshot goes here +![Ui](Ui.png) -// Product intro goes here +## Introduction +Bob is a simple task management chatbot that helps you keep track of todos, deadlines, and events. It runs in a chat-style interface where you type commands and Bob responds. -## Adding deadlines +--- -// Describe the action and its outcome. +## Quick Start -// Give examples of usage +1. **Download the JAR** + - Ensure you have Java 17 or later installed. + - Run Bob with: + java -jar bob.jar -Example: `keyword (optional arguments)` +2. **Type a command** and press Enter. + - Example: + ``` + todo read book + ``` -// A description of the expected outcome goes here +3. **Exit** by typing: +bye -``` -expected output -``` +--- -## Feature ABC +## Features -// Feature details +### Add a ToDo +Adds a task without a date. +todo +Example: +todo read book -## Feature XYZ +### Add a Deadline +Adds a task with a due date. +deadline /by -// Feature details \ No newline at end of file +Example: +deadline return book /by 2025-10-15 + +### Add an Event +Adds a task with a start and end date/time. +event /from /to + +Example: +event project meeting /from 2025-10-01 /to 2025-10-02 + +### List Tasks +Shows all the tasks currently tracked. +list + +### Mark/Unmark Tasks +- Mark task as done: +mark + +- Mark task as not done: +unmark + +### Delete a Task +Deletes a task by its index. +delete + +### Find Tasks +Finds tasks that contain a keyword. +find + +Example: +find book + +### Save Data Automatically +Bob saves your tasks to disk after every change. Next time you run the app, your tasks will be loaded automatically. + +--- + +## Command Summary + +| Command | Example | +|----------------------------------------|-----------------------------------| +| `todo ` | `todo read book` | +| `deadline /by ` | `deadline return book /by 2025-10-15` | +| `event /from /to ` | `event project meeting /from 2025-10-01 /to 2025-10-02` | +| `list` | `list` | +| `mark ` | `mark 2` | +| `unmark ` | `unmark 2` | +| `delete ` | `delete 3` | +| `find ` | `find book` | +| `bye` | `bye` | + +--- + +📌 **Tip**: The screenshot above (`Ui.png`) shows the full Bob GUI window, including the product name in the title bar. +MD diff --git a/docs/UG.md b/docs/UG.md new file mode 100644 index 0000000000..12fef101c6 --- /dev/null +++ b/docs/UG.md @@ -0,0 +1,12 @@ +# Bob the TaskBot + +A simple chatbot that helps you keep track of tasks, deadlines, and events — with a friendly text-based or GUI interface. + +--- + +## Running the App + +### Using Gradle +```bash +./gradlew run + diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..165195e7ad Binary files /dev/null and b/docs/Ui.png differ diff --git a/docs/images/ui.png b/docs/images/ui.png new file mode 100644 index 0000000000..165195e7ad Binary files /dev/null and b/docs/images/ui.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..afba109285 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..3499ded5c1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..65dcd68d65 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/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 + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&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 + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..6689b85bee --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hello.txt b/hello.txt new file mode 100644 index 0000000000..c4d3763182 --- /dev/null +++ b/hello.txt @@ -0,0 +1 @@ +Hello iP! diff --git a/sources.txt b/sources.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/duke/AppInfo.java b/src/main/java/duke/AppInfo.java new file mode 100644 index 0000000000..a075d2688e --- /dev/null +++ b/src/main/java/duke/AppInfo.java @@ -0,0 +1,7 @@ +package duke; + +public final class AppInfo { + public static final String APP_NAME = "Bob"; // pick any name, just not "Duke" + private AppInfo() {} +} + diff --git a/src/main/java/duke/Bob$Deadline.class b/src/main/java/duke/Bob$Deadline.class new file mode 100644 index 0000000000..50afefe2d0 Binary files /dev/null and b/src/main/java/duke/Bob$Deadline.class differ diff --git a/src/main/java/duke/Bob$DukeException.class b/src/main/java/duke/Bob$DukeException.class new file mode 100644 index 0000000000..7ddfbc5423 Binary files /dev/null and b/src/main/java/duke/Bob$DukeException.class differ diff --git a/src/main/java/duke/Bob$Event.class b/src/main/java/duke/Bob$Event.class new file mode 100644 index 0000000000..6c1e268fec Binary files /dev/null and b/src/main/java/duke/Bob$Event.class differ diff --git a/src/main/java/duke/Bob$Task.class b/src/main/java/duke/Bob$Task.class new file mode 100644 index 0000000000..8eba8e5be9 Binary files /dev/null and b/src/main/java/duke/Bob$Task.class differ diff --git a/src/main/java/duke/Bob$Todo.class b/src/main/java/duke/Bob$Todo.class new file mode 100644 index 0000000000..20ec13c331 Binary files /dev/null and b/src/main/java/duke/Bob$Todo.class differ diff --git a/src/main/java/duke/Bob.class b/src/main/java/duke/Bob.class new file mode 100644 index 0000000000..5464dae521 Binary files /dev/null and b/src/main/java/duke/Bob.class differ diff --git a/src/main/java/duke/Bob.java b/src/main/java/duke/Bob.java new file mode 100644 index 0000000000..ef5cc74db0 --- /dev/null +++ b/src/main/java/duke/Bob.java @@ -0,0 +1,223 @@ +package duke; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +// Level-8 date/time +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +public class Bob { + private static final String LINE = "____________________________________________________________"; + + // ===== Exceptions ===== + private static class DukeException extends Exception { + DukeException(String message) { super(message); } + } + + // ===== Model ===== + private static class Task { + protected final String description; + protected boolean isDone; + Task(String description) { this.description = description; this.isDone = false; } + void mark() { this.isDone = true; } + void unmark() { this.isDone = false; } + String statusIcon() { return isDone ? "X" : " "; } + @Override public String toString() { return "[" + statusIcon() + "] " + description; } + } + + private static class Todo extends Task { + Todo(String description) { super(description); } + @Override public String toString() { return "[T]" + super.toString(); } + } + + /** Level-8: store a real LocalDateTime and pretty-print it */ + private static class Deadline extends Task { + private final LocalDateTime by; + Deadline(String description, LocalDateTime by) { super(description); this.by = by; } + @Override public String toString() { + return "[D]" + super.toString() + " (by: " + pretty(by) + ")"; + } + } + + private static class Event extends Task { + private final String from, to; + Event(String description, String from, String to) { super(description); this.from = from; this.to = to; } + @Override public String toString() { return "[E]" + super.toString() + " (from: " + from + " to: " + to + ")"; } + } + + public static void main(String[] args) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + + printLogo(); + greet(); + + ArrayList tasks = new ArrayList<>(100); + + while (true) { + String input = br.readLine(); + if (input == null) { exit(); break; } + String cmd = input.trim(); + if (cmd.isEmpty()) { error("Please enter a command (try: todo, deadline, event, list, mark, unmark, delete, bye)."); continue; } + + try { + if (cmd.equals("bye")) { + exit(); break; + + } else if (cmd.equals("list")) { + printList(tasks); + + } else if (cmd.startsWith("mark")) { + int idx = requireIndex(cmd, "mark"); + requireInRange(idx, tasks.size(), "mark"); + tasks.get(idx - 1).mark(); + block(" Nice! I've marked this task as done:\n " + tasks.get(idx - 1)); + + } else if (cmd.startsWith("unmark")) { + int idx = requireIndex(cmd, "unmark"); + requireInRange(idx, tasks.size(), "unmark"); + tasks.get(idx - 1).unmark(); + block(" OK, I've marked this task as not done yet:\n " + tasks.get(idx - 1)); + + } else if (cmd.startsWith("delete")) { + int idx = requireIndex(cmd, "delete"); + requireInRange(idx, tasks.size(), "delete"); + Task removed = tasks.remove(idx - 1); + block(" Noted. I've removed this task:\n " + removed + + "\n Now you have " + tasks.size() + " tasks in the list."); + + } else if (cmd.startsWith("todo")) { + String desc = afterKeyword(cmd, "todo"); + if (desc.isEmpty()) throw new DukeException("A todo needs a description. Example: todo borrow book"); + addTask(tasks, new Todo(desc)); + + } else if (cmd.startsWith("deadline")) { + // Level-8: parse '/by ' and store LocalDateTime + String rest = afterKeyword(cmd, "deadline"); + String[] p = splitOnce(rest, "/by"); + String desc = p[0].trim(); + String when = p[1].trim(); + if (desc.isEmpty()) throw new DukeException("Deadline needs a description. Example: deadline return book /by 2019-12-02 1800"); + if (when.isEmpty()) throw new DukeException("Deadline needs a /by . Example: deadline return book /by 2019-12-02 1800"); + + LocalDateTime by = parseFlexibleDateTime(when); + addTask(tasks, new Deadline(desc, by)); + + } else if (cmd.startsWith("event")) { + String rest = afterKeyword(cmd, "event"); + String[] p1 = splitOnce(rest, "/from"); + String desc = p1[0].trim(); + String[] p2 = splitOnce(p1[1].trim(), "/to"); + String from = p2[0].trim(); + String to = p2[1].trim(); + if (desc.isEmpty()) throw new DukeException("Event needs a description. Example: event project meeting /from Mon 2pm /to 4pm"); + if (from.isEmpty()) throw new DukeException("Event needs a /from . Example: event ... /from Mon 2pm /to 4pm"); + if (to.isEmpty()) throw new DukeException("Event needs a /to . Example: event ... /from Mon 2pm /to 4pm"); + addTask(tasks, new Event(desc, from, to)); + + } else { + throw new DukeException("I don't recognise that command. Try: todo, deadline, event, list, mark, unmark, delete, bye."); + } + + } catch (DukeException ex) { + error(ex.getMessage()); + } catch (DateTimeParseException dtpe) { + error("Sorry, I couldn't parse that date/time. Try formats like: 2019-12-02 1800, 2019-12-02, 2/12/2019 1800, 2/12/2019."); + } + } + } + + // ===== UI helpers ===== + private static void printLogo() { + String logo = " ____ ___ ____ \n" + + "| __ ) / _ \\ | __ ) \n" + + "| _ \\| | | || _ \\ \n" + + "| |_) | |_| || |_) |\n" + + "|____/ \\___/ |____/ \n"; + System.out.println("Hello from\n" + logo); + } + private static void greet() { block(" Hello! I'm Bob\n What can I do for you?"); } + private static void exit() { block(" Bye. Hope to see you again soon!"); } + private static void block(String body) { + System.out.println(LINE); + for (String s : body.split("\n", -1)) System.out.println(s); + System.out.println(LINE); + } + private static void error(String msg) { block(" " + msg); } + + private static void printList(ArrayList tasks) { + StringBuilder sb = new StringBuilder(); + sb.append(" Here are the tasks in your list:\n"); + for (int i = 0; i < tasks.size(); i++) sb.append(" ").append(i + 1).append(".").append(tasks.get(i)).append("\n"); + String body = sb.toString().endsWith("\n") ? sb.substring(0, sb.length() - 1) : sb.toString(); + block(body); + } + + // ===== logic helpers ===== + private static void addTask(ArrayList tasks, Task t) throws DukeException { + if (tasks.size() >= 100) throw new DukeException("Sorry, I can only store up to 100 items."); + tasks.add(t); + block(" Got it. I've added this task:\n " + t + "\n Now you have " + tasks.size() + " tasks in the list."); + } + private static int parseIndex(String s) { + try { return Integer.parseInt(s.trim()); } catch (NumberFormatException e) { return -1; } + } + private static int requireIndex(String cmd, String keyword) throws DukeException { + String rest = cmd.length() > keyword.length() ? cmd.substring(keyword.length()).trim() : ""; + int idx = parseIndex(rest); + if (idx <= 0) throw new DukeException("Please provide a valid index. Example: " + keyword + " 2"); + return idx; + } + private static void requireInRange(int idx, int size, String op) throws DukeException { + if (idx < 1 || idx > size) throw new DukeException("Index out of range for " + op + ". Use 1.." + size + "."); + } + private static String afterKeyword(String cmd, String keyword) { + if (cmd.equals(keyword)) return ""; + return cmd.substring(keyword.length()).trim(); + } + /** Split by first token like "/by", "/from", "/to". Always returns length 2. */ + private static String[] splitOnce(String text, String token) { + int i = indexOfToken(text, token); + if (i < 0) return new String[]{text, ""}; + String left = text.substring(0, i); + String right = text.substring(i + token.length()); + return new String[]{left, right}; + } + /** Finds token allowing optional surrounding spaces. */ + private static int indexOfToken(String text, String token) { + int i = text.indexOf(" " + token + " "); + if (i >= 0) return i + 1; + i = text.indexOf(" " + token); + if (i >= 0) return i + 1; + i = text.indexOf(token + " "); + if (i >= 0) return i; + return text.indexOf(token); + } + + // ===== Level-8 date/time helpers ===== + private static final DateTimeFormatter OUT_DATE = DateTimeFormatter.ofPattern("MMM d yyyy"); + private static final DateTimeFormatter OUT_DT = DateTimeFormatter.ofPattern("MMM d yyyy, h:mma"); + + private static String pretty(LocalDateTime dt) { + return dt.toLocalTime().equals(LocalTime.MIDNIGHT) + ? dt.toLocalDate().format(OUT_DATE) + : dt.format(OUT_DT); + } + + /** Accepts: "yyyy-MM-dd HHmm", "yyyy-MM-dd", "d/M/yyyy HHmm", "d/M/yyyy". */ + private static LocalDateTime parseFlexibleDateTime(String s) { + String x = s.trim(); + // datetime forms + try { return LocalDateTime.parse(x, DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")); } catch (DateTimeParseException ignore) {} + try { return LocalDateTime.parse(x, DateTimeFormatter.ofPattern("d/M/yyyy HHmm")); } catch (DateTimeParseException ignore) {} + // date-only forms -> midnight + try { return LocalDate.parse(x, DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay(); } catch (DateTimeParseException ignore) {} + try { return LocalDate.parse(x, DateTimeFormatter.ofPattern("d/M/yyyy")).atStartOfDay(); } catch (DateTimeParseException ignore) {} + throw new DateTimeParseException("Unrecognized date/time", x, 0); + } +} diff --git a/src/main/java/duke/DateTimeUtil.java b/src/main/java/duke/DateTimeUtil.java new file mode 100644 index 0000000000..a41cb0dcf1 --- /dev/null +++ b/src/main/java/duke/DateTimeUtil.java @@ -0,0 +1,36 @@ +package duke; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +public final class DateTimeUtil { + private DateTimeUtil() {} + + // Accept: "yyyy-MM-dd HHmm", "yyyy-MM-dd", "d/M/yyyy HHmm", "d/M/yyyy" + private static final List DT = List.of( + DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"), + DateTimeFormatter.ofPattern("d/M/yyyy HHmm") + ); + private static final List D = List.of( + DateTimeFormatter.ofPattern("yyyy-MM-dd"), + DateTimeFormatter.ofPattern("d/M/yyyy") + ); + + public static LocalDateTime parseFlexible(String s) { + String x = s.trim(); + for (var f : DT) { try { return LocalDateTime.parse(x, f); } catch (DateTimeParseException ignore) {} } + for (var f : D) { try { return LocalDate.parse(x, f).atStartOfDay(); } catch (DateTimeParseException ignore) {} } + throw new DateTimeParseException("Unrecognized date/time", x, 0); + } + + private static final DateTimeFormatter OUT_DATE = DateTimeFormatter.ofPattern("MMM d yyyy"); + private static final DateTimeFormatter OUT_DT = DateTimeFormatter.ofPattern("MMM d yyyy, h:mma"); + + public static String pretty(LocalDateTime dt) { + return dt.toLocalTime().equals(LocalTime.MIDNIGHT) ? dt.toLocalDate().format(OUT_DATE) + : dt.format(OUT_DT); + } +} + diff --git a/src/main/java/duke/Deadline.java b/src/main/java/duke/Deadline.java new file mode 100644 index 0000000000..9922854e6b --- /dev/null +++ b/src/main/java/duke/Deadline.java @@ -0,0 +1,27 @@ +package duke; + +import java.time.LocalDateTime; + +public class Deadline extends Task { + private LocalDateTime by; + + public Deadline(String description, boolean isDone, LocalDateTime by) { + super(description, isDone); + this.by = by; + } + + public LocalDateTime getBy() { return by; } + public void setBy(LocalDateTime newBy) { this.by = newBy; } + + @Override + public String serialize() { + // store ISO for persistence + return String.format("D | %d | %s | %s", isDone ? 1 : 0, description, by); + } + + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + DateTimeUtil.pretty(by) + ")"; + } +} + diff --git a/src/main/java/duke/DukeCore.java b/src/main/java/duke/DukeCore.java new file mode 100644 index 0000000000..757b9264fa --- /dev/null +++ b/src/main/java/duke/DukeCore.java @@ -0,0 +1,338 @@ +package duke; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +/** + * Core command engine that turns one input line into a reply string. + * Reusable by both CLI and GUI. + */ +public class DukeCore +{ + private final TaskList tasks; + + public DukeCore() + { + // If you want persistence here, construct TaskList with a Storage later. + this.tasks = new TaskList(); + } + + public String welcome() + { + return "Hello! I'm Bob\nWhat can I do for you?"; + } + + public boolean isExit(final String line) + { + return line != null && line.trim().equals("bye"); + } + + /** Handle a single command and return the reply text (no console printing). */ + public String reply(final String input) + { + if (input == null) + { + return ""; + } + final String cmd = input.trim(); + if (cmd.isEmpty()) + { + return "Please enter a command (try: todo, deadline, event, list, mark, unmark, delete, edit, bye)."; + } + + try + { + if (cmd.equals("bye")) + { + return "Bye. Hope to see you again soon!"; + } + else if (cmd.equals("list")) + { + return renderList(); + } + else if (cmd.startsWith("mark")) + { + final int idx = requireIndex(cmd, "mark"); + assert idx > 0 && idx <= tasks.size() : "Index must be 1.." + tasks.size(); + requireInRange(idx, tasks.size(), "mark"); + tasks.get(idx - 1).mark(); + tasks.save(); + return "Nice! I've marked this task as done:\n " + tasks.get(idx - 1); + } + else if (cmd.startsWith("unmark")) + { + final int idx = requireIndex(cmd, "unmark"); + assert idx > 0 && idx <= tasks.size() : "Index must be 1.." + tasks.size(); + requireInRange(idx, tasks.size(), "unmark"); + tasks.get(idx - 1).unmark(); + tasks.save(); + return "OK, I've marked this task as not done yet:\n " + tasks.get(idx - 1); + } + else if (cmd.startsWith("delete")) + { + final int idx = requireIndex(cmd, "delete"); + assert idx > 0 && idx <= tasks.size() : "Index must be 1.." + tasks.size(); + requireInRange(idx, tasks.size(), "delete"); + final Task removed = tasks.remove(idx - 1); + tasks.save(); + return "Noted. I've removed this task:\n " + removed + + "\nNow you have " + tasks.size() + " tasks in the list."; + } + else if (cmd.startsWith("todo")) + { + final String desc = afterKeyword(cmd, "todo"); + if (desc.isEmpty()) + { + return "A todo needs a description. Example: todo borrow book"; + } + // Matches: Todo(String desc, boolean isDone) + tasks.add(new Todo(desc, false)); + tasks.save(); + return "Got it. I've added this task:\n " + tasks.get(tasks.size() - 1) + + "\nNow you have " + tasks.size() + " tasks in the list."; + } + else if (cmd.startsWith("deadline")) + { + final String rest = afterKeyword(cmd, "deadline"); + final String[] p = splitOnce(rest, "/by"); + final String desc = p[0].trim(); + final String when = p[1].trim(); + + if (desc.isEmpty()) + { + return "Deadline needs a description. Example: deadline return book /by 2019-12-02 1800"; + } + if (when.isEmpty()) + { + return "Deadline needs a /by . Example: deadline return book /by 2019-12-02 1800"; + } + + final LocalDateTime by = DateTimeUtil.parseFlexible(when); + // Matches: Deadline(String desc, boolean isDone, LocalDateTime by) + tasks.add(new Deadline(desc, false, by)); + tasks.save(); + return "Got it. I've added this task:\n " + tasks.get(tasks.size() - 1) + + "\nNow you have " + tasks.size() + " tasks in the list."; + } + else if (cmd.startsWith("event")) + { + final String rest = afterKeyword(cmd, "event"); + final String[] p1 = splitOnce(rest, "/from"); + final String desc = p1[0].trim(); + final String[] p2 = splitOnce(p1[1].trim(), "/to"); + final String from = p2[0].trim(); + final String to = p2[1].trim(); + + if (desc.isEmpty()) + { + return "Event needs a description. Example: event project meeting /from Mon 2pm /to 4pm"; + } + if (from.isEmpty()) + { + return "Event needs a /from . Example: event ... /from Mon 2pm /to 4pm"; + } + if (to.isEmpty()) + { + return "Event needs a /to . Example: event ... /from Mon 2pm /to 4pm"; + } + + final String timeslot = from + " /to " + to; + // Matches: Event(String desc, boolean isDone, String timeslot) + tasks.add(new Event(desc, false, timeslot)); + tasks.save(); + return "Got it. I've added this task:\n " + tasks.get(tasks.size() - 1) + + "\nNow you have " + tasks.size() + " tasks in the list."; + } + else if (cmd.startsWith("edit")) + { + return handleEdit(cmd); + } + else + { + return "I don't recognise that command. Try: todo, deadline, event, list, mark, unmark, delete, edit, bye."; + } + } + catch (final DateTimeParseException dtpe) + { + return "Sorry, I couldn't parse that date/time. Try formats like: " + + "2019-12-02 1800, 2019-12-02, 2/12/2019 1800, 2/12/2019."; + } + catch (final IOException ioe) + { + return "I tried to save but ran into a problem: " + ioe.getMessage(); + } + catch (final Exception e) + { + return "Oops: " + e.getMessage(); + } + } + + // ===== C-Update: edit command ===== + + private String handleEdit(final String cmd) throws IOException + { + // Syntax: + // edit INDEX desc NEW_DESCRIPTION + // edit INDEX by NEW_DATE_OR_DATETIME + // edit INDEX timeslot NEW_TEXT + final String rest = afterKeyword(cmd, "edit"); + final String[] firstTwo = rest.split("\\s+", 3); // idx, field, payload + if (firstTwo.length < 3) + { + return "Usage:\n" + + " edit INDEX desc NEW_DESCRIPTION\n" + + " edit INDEX by NEW_DATE_OR_DATETIME\n" + + " edit INDEX timeslot NEW_FROM /to NEW_TO"; + } + + final int idx = parseIndex(firstTwo[0]); + assert idx > 0 && idx <= tasks.size() : "Index must be 1.." + tasks.size(); + if (idx <= 0 || idx > tasks.size()) + { + return "Please provide a valid index within 1.." + tasks.size() + "."; + } + + final String field = firstTwo[1].toLowerCase(); + final String payload = firstTwo[2].trim(); + final Task t = tasks.get(idx - 1); + + switch (field) + { + case "desc": + { + if (payload.isEmpty()) + { + return "Description cannot be empty."; + } + t.setDescription(payload); // requires Task#setDescription + tasks.save(); + return "Updated description:\n " + t; + } + case "by": + { + if (!(t instanceof Deadline)) + { + return "Task #" + idx + " is not a deadline."; + } + if (payload.isEmpty()) + { + return "Please provide a date/time (e.g., 2019-12-02 1800)."; + } + final LocalDateTime newBy = DateTimeUtil.parseFlexible(payload); + ((Deadline) t).setBy(newBy); // requires Deadline#setBy + tasks.save(); + return "Updated deadline time:\n " + t; + } + case "timeslot": + { + if (!(t instanceof Event)) + { + return "Task #" + idx + " is not an event."; + } + if (payload.isEmpty()) + { + return "Please provide a timeslot, e.g., 'Mon 2pm /to 4pm'."; + } + ((Event) t).setTimeslot(payload); // requires Event#setTimeslot + tasks.save(); + return "Updated event time:\n " + t; + } + default: + return "Unknown field '" + field + "'. Use: desc | by | timeslot"; + } + } + + // ===== helpers ===== + + private String renderList() + { + final StringBuilder sb = new StringBuilder("Here are the tasks in your list:\n"); + for (int i = 0; i < tasks.size(); i++) + { + sb.append(i + 1).append(". ").append(tasks.get(i)).append("\n"); + } + if (tasks.size() == 0) + { + sb.append("(no tasks yet)\n"); + } + return sb.toString().stripTrailing(); + } + + private static String afterKeyword(final String cmd, final String keyword) + { + if (cmd.equals(keyword)) + { + return ""; + } + return cmd.substring(keyword.length()).trim(); + } + + /** Split by first token like "/by", "/from", "/to". Always returns length 2. */ + private static String[] splitOnce(final String text, final String token) + { + final int i = indexOfToken(text, token); + if (i < 0) + { + return new String[] { text, "" }; + } + final String left = text.substring(0, i); + final String right = text.substring(i + token.length()); + return new String[] { left, right }; + } + + /** Finds token allowing optional surrounding spaces. */ + private static int indexOfToken(final String text, final String token) + { + int i = text.indexOf(" " + token + " "); + if (i >= 0) + { + return i + 1; + } + i = text.indexOf(" " + token); + if (i >= 0) + { + return i + 1; + } + i = text.indexOf(token + " "); + if (i >= 0) + { + return i; + } + return text.indexOf(token); + } + + private static int parseIndex(final String s) + { + try + { + return Integer.parseInt(s.trim()); + } + catch (final NumberFormatException e) + { + return -1; + } + } + + private static int requireIndex(final String cmd, final String keyword) throws Exception + { + final String rest = cmd.length() > keyword.length() + ? cmd.substring(keyword.length()).trim() + : ""; + final int idx = parseIndex(rest); + if (idx <= 0) + { + throw new Exception("Please provide a valid index. Example: " + keyword + " 2"); + } + return idx; + } + + private static void requireInRange(final int idx, final int size, final String op) throws Exception + { + if (idx < 1 || idx > size) + { + throw new Exception("Index out of range for " + op + ". Use 1.." + size + "."); + } + } +} + diff --git a/src/main/java/duke/Event.java b/src/main/java/duke/Event.java new file mode 100644 index 0000000000..da167f1904 --- /dev/null +++ b/src/main/java/duke/Event.java @@ -0,0 +1,24 @@ +package duke; + +public class Event extends Task { + private String timeslot; // not final (DukeCore mutates) + + public Event(String description, boolean isDone, String timeslot) { + super(description, isDone); + this.timeslot = timeslot; + } + + public String getTimeslot() { return timeslot; } + public void setTimeslot(String timeslot) { this.timeslot = timeslot; } + + @Override + public String serialize() { + return String.format("E | %d | %s | %s", isDone ? 1 : 0, description, timeslot); + } + + @Override + public String toString() { + return "[E]" + super.toString() + " (at: " + timeslot + ")"; + } +} + diff --git a/src/main/java/duke/Launcher.java b/src/main/java/duke/Launcher.java new file mode 100644 index 0000000000..d7eb497898 --- /dev/null +++ b/src/main/java/duke/Launcher.java @@ -0,0 +1,8 @@ +package duke; + +public class Launcher { + public static void main(String[] args) { + MainApp.launch(MainApp.class, args); + } +} + diff --git a/src/main/java/duke/MainApp.java b/src/main/java/duke/MainApp.java new file mode 100644 index 0000000000..e409f43f6a --- /dev/null +++ b/src/main/java/duke/MainApp.java @@ -0,0 +1,60 @@ +package duke; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +public class MainApp extends Application +{ + private DukeCore core; + + @Override + public void start(final Stage stage) + { + core = new DukeCore(); + + final TextArea display = new TextArea(); + display.setEditable(false); + display.appendText(core.welcome() + "\n"); + + final TextField input = new TextField(); + input.setPromptText("Type a command (e.g., 'todo read book', 'list', 'bye')"); + final Button send = new Button("Send"); + + final HBox bottom = new HBox(8, input, send); + final VBox root = new VBox(8, display, bottom); + root.setStyle("-fx-padding: 12;"); + + // submit on click + send.setOnAction(e -> { + final String line = input.getText().trim(); + if (line.isEmpty()) + { + return; + } + display.appendText(">> " + line + "\n"); + final String reply = core.reply(line); + display.appendText(reply + "\n"); + input.clear(); + + if (core.isExit(line)) + { + Platform.exit(); + } + }); + + // submit on Enter + input.setOnAction(send.getOnAction()); + + stage.setTitle(AppInfo.APP_NAME); + stage.setScene(new Scene(root, 560, 380)); + stage.show(); + } +} + diff --git a/src/main/java/duke/Parser.java b/src/main/java/duke/Parser.java new file mode 100644 index 0000000000..260b4a2646 --- /dev/null +++ b/src/main/java/duke/Parser.java @@ -0,0 +1,7 @@ +package duke; + +public class Parser { + public static String parse(String input) { + return input == null ? "" : input.trim(); + } +} diff --git a/src/main/java/duke/ParserUtil.java b/src/main/java/duke/ParserUtil.java new file mode 100644 index 0000000000..43871db889 --- /dev/null +++ b/src/main/java/duke/ParserUtil.java @@ -0,0 +1,56 @@ +package duke; + +import java.time.LocalDateTime; +import java.util.List; + +public final class ParserUtil +{ + private ParserUtil() { } + + /** + * Convert one serialized line into a Task. + * Unknown/corrupt lines are skipped by returning null. + */ + public static Task parseLine(final String line) + { + try { + final String[] parts = line.split("\\s*\\|\\s*"); + final String type = parts[0]; + final boolean done = "1".equals(parts[1]); + final String desc = parts[2]; + + switch (type) { + case "T": + return new Todo(desc, done); + case "D": + { + final LocalDateTime by = LocalDateTime.parse(parts[3]); // ISO + return new Deadline(desc, done, by); + } + case "E": + { + final String timeslot = parts[3]; + return new Event(desc, done, timeslot); + } + default: + // unknown type (treat as corrupted) + return null; + } + } catch (final Exception e) { + // corrupted line—skip gracefully for Level-7 stretch goal + return null; + } + } + + /** Parse all lines; skip nulls. */ + public static void loadInto(final List lines, final TaskList taskList) + { + for (final String line : lines) { + final Task t = parseLine(line); + if (t != null) { + taskList.addSilently(t); // add without saving during initial load + } + } + } +} + diff --git a/src/main/java/duke/Storage.java b/src/main/java/duke/Storage.java new file mode 100644 index 0000000000..f432d60118 --- /dev/null +++ b/src/main/java/duke/Storage.java @@ -0,0 +1,33 @@ +package duke; + +import java.io.IOException; +import java.util.List; + +public class Storage { + private final String filePath; + + public Storage() { + this("data/duke.txt"); + } + + public Storage(String filePath) { + this.filePath = filePath; + } + + public List load() throws IOException { + return List.of(); + } + + public List loadLines() throws IOException { + return List.of(); + } + + public void save(List tasks) throws IOException { + // no-op for now (Level-7/Storage can replace this later) + } + + public String getFilePath() { + return filePath; + } +} + diff --git a/src/main/java/duke/Task.java b/src/main/java/duke/Task.java new file mode 100644 index 0000000000..3eee7cedb7 --- /dev/null +++ b/src/main/java/duke/Task.java @@ -0,0 +1,33 @@ +package duke; + +public abstract class Task { + protected String description; // not final (DukeCore mutates) + protected boolean isDone; + + protected Task(String description, boolean isDone) { + this.description = description; + this.isDone = isDone; + } + + public String getDescription() { return description; } + public void setDescription(String newDesc) { this.description = newDesc; } + + public boolean isDone() { return isDone; } + + // canonical mutators + public void markDone() { this.isDone = true; } + public void markUndone() { this.isDone = false; } + + // compatibility aliases expected by DukeCore + public void mark() { markDone(); } + public void unmark() { markUndone(); } + + /** e.g., "T | 1 | read book" */ + public abstract String serialize(); + + @Override + public String toString() { + return "[" + (isDone ? "X" : " ") + "] " + description; + } +} + diff --git a/src/main/java/duke/TaskList.java b/src/main/java/duke/TaskList.java new file mode 100644 index 0000000000..66fc6555de --- /dev/null +++ b/src/main/java/duke/TaskList.java @@ -0,0 +1,79 @@ +package duke; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TaskList +{ + private final List tasks = new ArrayList<>(); + private final Storage storage; // may be null + + public TaskList() + { + this.storage = null; + } + + public TaskList(final Storage storage) + { + this.storage = storage; + } + + public int size() + { + return tasks.size(); + } + + public List asList() + { + return Collections.unmodifiableList(tasks); + } + + public Task get(final int index) + { + return tasks.get(index); + } + + public void add(final Task t) throws IOException + { + tasks.add(t); + save(); + } + + /** Add without triggering save (useful for initial load). */ + public void addSilently(final Task t) + { + tasks.add(t); + } + + public Task remove(final int index) throws IOException + { + final Task removed = tasks.remove(index); + save(); + return removed; + } + + /** Persist the current list if storage is present; otherwise no-op. */ + public void save() throws IOException + { + if (storage != null) + { + storage.save(tasks); + } + } + + public List find(String keyword) { + String q = (keyword == null ? "" : keyword.trim()).toLowerCase(); + List matches = new ArrayList<>(); + for (Task t : this.tasks) { + String text = t.getDescription(); + if (text.toLowerCase().contains(q)) { + matches.add(t); + } + } + return matches; +} + +} + diff --git a/src/main/java/duke/Todo.java b/src/main/java/duke/Todo.java new file mode 100644 index 0000000000..cab429b1c8 --- /dev/null +++ b/src/main/java/duke/Todo.java @@ -0,0 +1,22 @@ +package duke; + +public class Todo extends Task { + public Todo(String description) { + super(description, false); + } + + public Todo(String description, boolean isDone) { + super(description, isDone); + } + + @Override + public String serialize() { + return String.format("T | %d | %s", isDone() ? 1 : 0, getDescription()); + } + + @Override + public String toString() { + return "[T]" + super.toString(); + } +} + diff --git a/src/main/java/duke/Ui.java b/src/main/java/duke/Ui.java new file mode 100644 index 0000000000..b646d08765 --- /dev/null +++ b/src/main/java/duke/Ui.java @@ -0,0 +1,32 @@ +package duke; + +public class Ui { + private static final String LINE = "____________________________________________________________"; + + public void showWelcome() { + System.out.println(LINE); + System.out.println(" Hello! I'm Duke"); + System.out.println(" What can I do for you?"); + System.out.println(LINE); + } + + public void showLine() { + System.out.println(LINE); + } + + public void showMessage(String msg) { + System.out.println(msg); + } + + public void showError(String msg) { + showLine(); + System.out.println(" " + msg); + showLine(); + } + + public void showBye() { + showLine(); + System.out.println(" Bye. Hope to see you again soon!"); + showLine(); + } +} diff --git a/src/main/java/duke/command/FindCommand.java b/src/main/java/duke/command/FindCommand.java new file mode 100644 index 0000000000..5a126b9dd4 --- /dev/null +++ b/src/main/java/duke/command/FindCommand.java @@ -0,0 +1,24 @@ +package duke.command; + +import duke.TaskList; +import duke.Task; +import java.util.List; + +public final class FindCommand { + private FindCommand() {} // utility class + + /** Runs a find and returns the formatted result text. */ + public static String run(TaskList tasks, String keyword) { + String q = keyword == null ? "" : keyword.trim(); + List matches = tasks.find(q); + if (matches.isEmpty()) { + return "No matching tasks found."; + } + StringBuilder sb = new StringBuilder("Here are the matching tasks in your list:\n"); + for (int i = 0; i < matches.size(); i++) { + sb.append(String.format(" %d.%s%n", i + 1, matches.get(i))); + } + return sb.toString().trim(); + } +} + diff --git a/src/test/java/duke/DateTimeUtilTest.java b/src/test/java/duke/DateTimeUtilTest.java new file mode 100644 index 0000000000..97215c875a --- /dev/null +++ b/src/test/java/duke/DateTimeUtilTest.java @@ -0,0 +1,44 @@ +package duke; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import org.junit.jupiter.api.Test; + +class DateTimeUtilTest { + + @Test + void parseFlexible_acceptsMultipleFormats() { + assertEquals( + LocalDateTime.of(2019, 12, 2, 18, 0), + DateTimeUtil.parseFlexible("2019-12-02 1800") + ); + assertEquals( + LocalDateTime.of(2019, 12, 2, 0, 0), + DateTimeUtil.parseFlexible("2019-12-02") + ); + assertEquals( + LocalDateTime.of(2019, 12, 2, 18, 0), + DateTimeUtil.parseFlexible("2/12/2019 1800") + ); + assertEquals( + LocalDate.of(2019, 12, 2).atTime(LocalTime.MIDNIGHT), + DateTimeUtil.parseFlexible("2/12/2019") + ); + } + + @Test + void pretty_rendersMidnightAsDate_andOthersAsDateTime() { + LocalDateTime midnight = LocalDate.of(2019, 12, 2).atStartOfDay(); + String prettyMidnight = DateTimeUtil.pretty(midnight); + assertTrue(prettyMidnight.equals("Dec 2 2019") || prettyMidnight.equals("Dec 2 2019")); + + LocalDateTime evening = LocalDateTime.of(2019, 12, 2, 18, 0); + String prettyEvening = DateTimeUtil.pretty(evening); + assertTrue(prettyEvening.contains("Dec 2 2019") && prettyEvening.contains("6:00")); + } +} + diff --git a/src/test/java/duke/DukeCoreTest.java b/src/test/java/duke/DukeCoreTest.java new file mode 100644 index 0000000000..5d034d313c --- /dev/null +++ b/src/test/java/duke/DukeCoreTest.java @@ -0,0 +1,33 @@ +package duke; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class DukeCoreTest { + + @Test + void reply_todoAndList_roundTrip() { + DukeCore core = new DukeCore(); + + String r1 = core.reply("todo read book"); + assertTrue(r1.contains("[T][ ] read book")); + + String list = core.reply("list"); + assertTrue(list.contains("1. [T][ ] read book")); + } + + @Test + void reply_deadline_parsesAndPrettyPrints() { + DukeCore core = new DukeCore(); + + String r1 = core.reply("deadline return book /by 2019-12-02 1800"); + // Should include pretty date (Dec 2 2019, 6:00PM) somewhere + assertTrue(r1.contains("return book")); + // Don’t over-specify formatting; just check parts + String list = core.reply("list"); + assertTrue(list.contains("return book")); + assertTrue(list.contains("Dec 2 2019")); // pretty date part + } +} + diff --git a/src/test/java/duke/TaskListTest.java b/src/test/java/duke/TaskListTest.java new file mode 100644 index 0000000000..256433739e --- /dev/null +++ b/src/test/java/duke/TaskListTest.java @@ -0,0 +1,16 @@ +package duke; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class TaskListTest { + + @Test + void add_increasesSize() throws Exception { + TaskList list = new TaskList(); + int oldSize = list.size(); + list.addSilently(new Todo("read book")); + assertEquals(oldSize + 1, list.size()); + } +} + diff --git a/src/test/java/duke/TodoTest.java b/src/test/java/duke/TodoTest.java new file mode 100644 index 0000000000..c6a97ebfdc --- /dev/null +++ b/src/test/java/duke/TodoTest.java @@ -0,0 +1,14 @@ +package duke; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class TodoTest { + + @Test + void serialize_returnsCorrectFormat() { + Todo todo = new Todo("read book", true); + assertEquals("T | 1 | read book", todo.serialize()); + } +} + diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 657e74f6e7..ec22a71b50 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,7 +1,23 @@ Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + ____ ___ ____ +| __ ) / _ \ | __ ) +| _ \| | | || _ \ +| |_) | |_| || |_) | +|____/ \___/ |____/ +____________________________________________________________ + Hello! I'm Bob + What can I do for you? +____________________________________________________________ +____________________________________________________________ + Got it. I've added this task: + [T][ ] read book + Now you have 1 tasks in the list. +____________________________________________________________ +____________________________________________________________ + Here are the tasks in your list: + 1.[T][ ] read book +____________________________________________________________ +____________________________________________________________ + Bye. Hope to see you again soon! +____________________________________________________________ diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29bb2..a6ab61b392 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1,3 @@ +todo read book +list +bye diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh old mode 100644 new mode 100755 index c9ec870033..84b763d02d --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -1,38 +1,36 @@ #!/usr/bin/env bash +set -euo pipefail -# create bin directory if it doesn't exist -if [ ! -d "../bin" ] -then - mkdir ../bin -fi +MAIN_CLASS="duke.Bob" -# delete output from previous run -if [ -e "./ACTUAL.TXT" ] -then - rm ACTUAL.TXT -fi +# cd to script directory so relative paths are stable +cd "$(dirname "$0")" + +# prepare bin and clean previous output +mkdir -p ../bin +rm -f ACTUAL.TXT -# compile the code into the bin folder, terminates if error occurred -if ! javac -cp ../src/main/java -Xlint:none -d ../bin ../src/main/java/*.java -then - echo "********** BUILD FAILURE **********" - exit 1 +# Compile only console classes (exclude JavaFX GUI files) +SOURCES=$(find ../src/main/java -type f -name "*.java" \ + ! -path "*/duke/MainApp.java" \ + ! -path "*/duke/Launcher.java" \ + ! -path "*/duke/gui/*") + +if [ -z "${SOURCES}" ]; then + echo "No console sources found to compile. Check your paths/exclusions." + exit 1 fi -# run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT -java -classpath ../bin Duke < input.txt > ACTUAL.TXT +# Compile (no warnings; output into ../bin) +javac -Xlint:none -d ../bin ${SOURCES} -# convert to UNIX format -cp EXPECTED.TXT EXPECTED-UNIX.TXT -dos2unix ACTUAL.TXT EXPECTED-UNIX.TXT +# Run with redirected I/O +java -cp ../bin "$MAIN_CLASS" < input.txt > ACTUAL.TXT -# compare the output to the expected output -diff ACTUAL.TXT EXPECTED-UNIX.TXT -if [ $? -eq 0 ] -then - echo "Test result: PASSED" - exit 0 +# Compare outputs +if diff -u EXPECTED.TXT ACTUAL.TXT; then + echo "=== PASS: Output matches EXPECTED.TXT ===" else - echo "Test result: FAILED" - exit 1 -fi \ No newline at end of file + echo "=== FAIL: Output differs from EXPECTED.TXT ===" + exit 1 +fi