diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 00000000..57f68586 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,45 @@ +on: push +name: Build, Test and Upload JAR +jobs: + checks: + name: Run Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + + - name: Mount bazel cache + uses: actions/cache@v1 + with: + path: "/home/runner/.cache/bazel" + key: bazel + + - name: Install bazelisk + run: | + curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64" + mkdir -p "${GITHUB_WORKSPACE}/bin/" + mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel" + chmod +x "${GITHUB_WORKSPACE}/bin/bazel" + + - name: Run Unit Tests + run: | + "${GITHUB_WORKSPACE}/bin/bazel" test //... + + - name: Run UI and Serialization Tests + working-directory: ./text-ui-test + run: | + chmod +x runtest.sh + ./runtest.sh + + - name: Build JAR File + run: | + "${GITHUB_WORKSPACE}/bin/bazel" build //:TerminalDuke_deploy.jar + + - name: "Move file to non-symlink directory (a workaround for some bug See: https://github.com/actions/upload-artifact/issues/92)" + run: | + mv ${{ github.workspace }}/bazel-bin/TerminalDuke_deploy.jar ${{ github.workspace }}/TerminalDuke_deploy.jar + + - name: Upload JAR File + uses: actions/upload-artifact@v2 + with: + name: TerminalDuke_deploy.jar + path: ${{ github.workspace }}/TerminalDuke_deploy.jar diff --git a/.gitignore b/.gitignore index f69985ef..d08823f6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,12 @@ src/main/resources/docs/ bin/ /text-ui-test/ACTUAL.txt +/text-ui-test/tasks.json text-ui-test/EXPECTED-UNIX.TXT + +# Ignore all bazel-* symlinks. There is no full list since this can change +# based on the name of the directory bazel is cloned into. +/bazel-* + +# for pre-commit git-hooks +/.cache/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f4b6f43b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/maltzj/google-style-precommit-hook + rev: b7e9e7fcba4a5aea463e72fe9964c14877bd8130 + hooks: + - id: google-style-java diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 00000000..1641d4ff --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,8 @@ +# Pre-commit hook to run google-format-java right before commiting +# Source: https://github.com/maltzj/google-style-precommit-hook +- id: google-style-java + name: Google Java Code Style for Java + description: Formats code in Google's Java codestyle. + entry: ./tools/format-code.sh + language: script + files: \.java$ # We don't technically need this, as the script will filter for us, but this will allow the hook to be skipped if no Java is changed. diff --git a/BUILD b/BUILD new file mode 100644 index 00000000..ee30cb50 --- /dev/null +++ b/BUILD @@ -0,0 +1,54 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_test") + +java_binary( + name = "TerminalDuke", # Production Build. + srcs = glob([ + "src/main/java/com/lockarhythm/**/*.java", + ]), + deps = [ + "@maven//:org_apache_commons_commons_lang3", + "@maven//:com_google_code_gson_gson", + "@maven//:org_ocpsoft_prettytime_prettytime_nlp", + ], +) + +java_binary( + name = "TerminalDukeQA", # QA Build. has assertions enabled. + main_class = "com.lockarhythm.application.TerminalDuke", + srcs = glob([ + "src/main/java/com/lockarhythm/**/*.java", + ]), + deps = [ + "@maven//:org_apache_commons_commons_lang3", + "@maven//:com_google_code_gson_gson", + "@maven//:org_ocpsoft_prettytime_prettytime_nlp", + ], + jvm_flags = [ + "-enableassertions", + ], +) + +[ + java_test( + name = class_name, + size = "small", + srcs = glob([ + "src/test/java/com/lockarhythm/**/*.java", + "src/main/java/com/lockarhythm/**/*.java", + ]), + deps = [ + "@maven//:org_apache_commons_commons_lang3", + "@maven//:com_google_code_gson_gson", + "@maven//:org_ocpsoft_prettytime_prettytime_nlp", + ] + ) + for class_name in [ + "TestTerminalDuke", + "TestEchoResponder", + "TestExitResponder", + "TestListResponder", + "TestMarkAsDoneResponder", + "TestTodoTask", + "TestTaskList", + ] +] diff --git a/README.md b/README.md index 8715d4d9..0dfacebc 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,61 @@ -# Duke project template + + +# Duke This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. -## Setting up in Intellij - -Prerequisites: JDK 11, update Intellij to the most recent version. - -1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first) -1. Open the project into Intellij as follows: - 1. Click `Open`. - 1. Select the project directory, and click `OK`. - 1. If there are any further prompts, accept the defaults. -1. Configure the project to use **JDK 11** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
- In the same dialog, set the **Project language level** field to the `SDK default` option. -3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: - ``` - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - ``` +## Demo +![output](https://user-images.githubusercontent.com/88638946/131530452-cbe5ab59-9e43-4c08-affe-f9fdf2c75427.gif) + + +## Build +Prerequisites: Have `bazel` installed. + +To build for local development on macOS: +```bash +# bazel makes a wrapper script around the jar file. +bazel build //:TerminalDuke && ./bazel-bin/TerminalDuke + +# or simply +bazel run //:TerminalDuke + +# Use bazelisk for M1 Macs (Apple Silicon) +USE_BAZEL_VERSION=ac9353fab161efae4af72e73fbb657a762b3620d bazelisk run //:TerminalDuke + +# For QA build, for example, having assertions enabled, build TerminalDukeQA instead +USE_BAZEL_VERSION=ac9353fab161efae4af72e73fbb657a762b3620d bazelisk run //:TerminalDukeQA +``` + +To build jar file for deployment purposes: +```bash +bazel build //:Duke_deploy.jar + +java -jar ./bazel-bin/Duke_deploy.jar +``` + +## Testing +For unit tests: +```bash +bazel test --test_output=all //... +``` +For terminal ui tests (`text-ui-test`): +```bash +cd text-ui-test +./runtest.sh +``` + +## Code Formatting & Style +Use [google-java-format](https://github.com/google/google-java-format): +```bash +google-java-format --replace **/*.java # executes recursively +``` + +## Pre-commit Git Hook (Tested on macOS) +`pre-commit` is a python tool that helps to create git pre-commit hook handlers. For this project, it is used to enforce Google Java Style (via `google-java-format`) on new commits. + +To setup, ensure you have [`pre-commit`](https://pre-commit.com/#install) installed, then run: +```bash +pre-commit install +``` +This only needs to be executed once, so that `pre-commit` can generate the script and write it to `.git/hooks/pre-commit` +Thereafter, it should be executed right before every commit! diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000..7c359742 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,27 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +RULES_JVM_EXTERNAL_TAG = "4.0" +RULES_JVM_EXTERNAL_SHA = "31701ad93dbfe544d597dbe62c9a1fdd76d81d8a9150c2bf1ecf928ecdf97169" + +http_archive( + name = "rules_jvm_external", + strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, + sha256 = RULES_JVM_EXTERNAL_SHA, + url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, +) + +load("@rules_jvm_external//:defs.bzl", "maven_install") + +maven_install( + artifacts = [ + #"com.google.guava:guava:21.0", + "org.apache.commons:commons-lang3:3.11", + "com.google.code.gson:gson:2.8.8", + "org.ocpsoft.prettytime:prettytime-nlp:5.0.2.Final" + ], + repositories = [ + # Private repositories are supported through HTTP Basic auth + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], +) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000..4c001417 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 00000000..39efb6e4 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334..00000000 --- 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/com/lockarhythm/application/Application.java b/src/main/java/com/lockarhythm/application/Application.java new file mode 100644 index 00000000..5ca8f930 --- /dev/null +++ b/src/main/java/com/lockarhythm/application/Application.java @@ -0,0 +1,52 @@ +package com.lockarhythm.application; + +import com.lockarhythm.query.DukeException; +import com.lockarhythm.query.QueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.storage.Storage; +import com.lockarhythm.ui.UI; +import java.io.IOException; + +/** + * Application contains the common logic of all Duke Applications (GUI, TUI). + * + *

The application architecture is heavily inspired by the Ports & Adapters architecture. + */ +abstract class Application { + static String logo = + " \t____ _ \n" + + "\t| _ \\ _ _| | _____ \n" + + "\t| | | | | | | |/ / _ \\\n" + + "\t| |_| | |_| | < __/\n" + + "\t|____/ \\__,_|_|\\_\\___|\n"; + + /** + * run should be supplied with concrete implementations of its UI, QueryInterpreter and Storage. + * + *

For example, UI could be the Terminal UI or GUI. Storage could be in-memory or disk. + * + * @param ui a concrete UI implementation + * @param q a concrete QueryInterpreter implementation + * @param storage any Storage implementation + */ + public static void run(UI ui, QueryInterpreter q, Storage storage) { + Result result; + + ui.print("Hello I'm\n" + logo, "What can I do for you?"); + + while (ui.hasNext()) { + try { + result = q.interpret(ui.nextLine()); + storage.overwrite(); + ui.print(result); + if (result.shouldExit()) { + break; + } + } catch (IOException e) { + ui.print(String.format("Sorry, I cannot save the task to file: %s", e)); + } catch (DukeException e) { + ui.print("Sorry, I don't understand that yet!"); + } + } + } +} diff --git a/src/main/java/com/lockarhythm/application/TerminalDuke.java b/src/main/java/com/lockarhythm/application/TerminalDuke.java new file mode 100644 index 00000000..d961ba6e --- /dev/null +++ b/src/main/java/com/lockarhythm/application/TerminalDuke.java @@ -0,0 +1,23 @@ +package com.lockarhythm.application; + +import com.lockarhythm.query.SimpleQueryInterpreter; +import com.lockarhythm.storage.Storage; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskList; +import com.lockarhythm.ui.TerminalUI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; + +/** + * TerminalDuke is the entrypoint of the Terminal UI application of Duke. + */ +final class TerminalDuke extends Application { + public static void main(String[] args) { + Path path = Paths.get(".", "tasks.json"); + Storage storage = new Storage(path.toString()); + ArrayList list = storage.load(Task.class); + storage.registerList(list); + run(new TerminalUI(), new SimpleQueryInterpreter(new TaskList(list)), storage); + } +} diff --git a/src/main/java/com/lockarhythm/query/AddedTaskResult.java b/src/main/java/com/lockarhythm/query/AddedTaskResult.java new file mode 100644 index 00000000..fe81905a --- /dev/null +++ b/src/main/java/com/lockarhythm/query/AddedTaskResult.java @@ -0,0 +1,20 @@ +package com.lockarhythm.query; + +import com.lockarhythm.tasks.Task; + +public class AddedTaskResult extends Result { + private Task task; + private int size; // the max size on add + + public AddedTaskResult(Task task, int size) { + super(task.toString()); + this.task = task; + this.size = size; + } + + @Override + public String getText() { + return String.format( + "Got it. I've added this task:\n\t%s\nNow you have %d tasks in the list.", task, size); + } +} diff --git a/src/main/java/com/lockarhythm/query/DeletedTaskResult.java b/src/main/java/com/lockarhythm/query/DeletedTaskResult.java new file mode 100644 index 00000000..128ac60a --- /dev/null +++ b/src/main/java/com/lockarhythm/query/DeletedTaskResult.java @@ -0,0 +1,20 @@ +package com.lockarhythm.query; + +import com.lockarhythm.tasks.Task; + +public class DeletedTaskResult extends Result { + private Task task; + private int size; // new size after deletion + + public DeletedTaskResult(Task task, int size) { + super(task.toString()); + this.task = task; + this.size = size; + } + + @Override + public String getText() { + return String.format( + "Noted. I've removed this task:\n\t%s\nNow you have %d tasks in the list.", task, size); + } +} diff --git a/src/main/java/com/lockarhythm/query/DukeException.java b/src/main/java/com/lockarhythm/query/DukeException.java new file mode 100644 index 00000000..42f0711f --- /dev/null +++ b/src/main/java/com/lockarhythm/query/DukeException.java @@ -0,0 +1,3 @@ +package com.lockarhythm.query; + +public class DukeException extends Exception {} diff --git a/src/main/java/com/lockarhythm/query/QueryInterpreter.java b/src/main/java/com/lockarhythm/query/QueryInterpreter.java new file mode 100644 index 00000000..9d856d31 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/QueryInterpreter.java @@ -0,0 +1,5 @@ +package com.lockarhythm.query; + +public interface QueryInterpreter { + public Result interpret(String query) throws DukeException; +} diff --git a/src/main/java/com/lockarhythm/query/RegexQueryInterpreter.java b/src/main/java/com/lockarhythm/query/RegexQueryInterpreter.java new file mode 100644 index 00000000..1c19bd8e --- /dev/null +++ b/src/main/java/com/lockarhythm/query/RegexQueryInterpreter.java @@ -0,0 +1,34 @@ +package com.lockarhythm.query; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class RegexQueryInterpreter implements QueryInterpreter { + private Pattern pattern; + + public RegexQueryInterpreter() { + pattern = Pattern.compile(commandRegex()); + } + + public Result interpret(String query) { + Matcher matcher = pattern.matcher(query); + if (matcher.find()) { + return onMatch(toGroupStrings(matcher)); + } + return null; + } + + private String[] toGroupStrings(Matcher matcher) { + String[] groups = new String[matcher.groupCount() + 1]; + for (int i = 0; i <= matcher.groupCount(); i++) { + groups[i] = matcher.group(i); + } + return groups; + } + + // onMatch is called when regexp pattern matches. The regexp capturing groups is passed as + // arguments. + public abstract Result onMatch(String[] groups); + + protected abstract String commandRegex(); +} diff --git a/src/main/java/com/lockarhythm/query/Result.java b/src/main/java/com/lockarhythm/query/Result.java new file mode 100644 index 00000000..0a974d05 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/Result.java @@ -0,0 +1,23 @@ +package com.lockarhythm.query; + +public class Result { + private String text; + private boolean shouldExit; + + public Result(String text, boolean shouldExit) { + this.text = text; + this.shouldExit = shouldExit; + } + + public Result(String text) { + this(text, false); + } + + public String getText() { + return text; + } + + public boolean shouldExit() { + return shouldExit; + } +} diff --git a/src/main/java/com/lockarhythm/query/SimpleQueryInterpreter.java b/src/main/java/com/lockarhythm/query/SimpleQueryInterpreter.java new file mode 100644 index 00000000..b7dc2e39 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/SimpleQueryInterpreter.java @@ -0,0 +1,42 @@ +package com.lockarhythm.query; + +import com.lockarhythm.query.deadline.DeadlineResponder; +import com.lockarhythm.query.delete.DeleteResponder; +import com.lockarhythm.query.event.EventResponder; +import com.lockarhythm.query.exit.ExitResponder; +import com.lockarhythm.query.find.FindResponder; +import com.lockarhythm.query.list.ListResponder; +import com.lockarhythm.query.markasdone.MarkAsDoneResponder; +import com.lockarhythm.query.sort.SortResponder; +import com.lockarhythm.query.todo.TodoResponder; +import com.lockarhythm.tasks.TaskList; + +/** SimpleQueryInterpreter finds the first QueryInterpreter that responds a non-null result. */ +public final class SimpleQueryInterpreter implements QueryInterpreter { + static QueryInterpreter[] interpreters; + + public SimpleQueryInterpreter(TaskList list) { + QueryInterpreter[] res = { + new ExitResponder(), + new MarkAsDoneResponder(list), + new TodoResponder(list), + new ListResponder(list), + new DeadlineResponder(list), + new EventResponder(list), + new DeleteResponder(list), + new SortResponder(list), + new FindResponder(list), + }; + interpreters = res; + } + + public Result interpret(String query) throws DukeException { + for (QueryInterpreter interpreter : interpreters) { + Result res = interpreter.interpret(query); + if (res != null) { + return res; + } + } + throw new DukeException(); + } +} diff --git a/src/main/java/com/lockarhythm/query/deadline/DeadlineResponder.java b/src/main/java/com/lockarhythm/query/deadline/DeadlineResponder.java new file mode 100644 index 00000000..8e6cc1bd --- /dev/null +++ b/src/main/java/com/lockarhythm/query/deadline/DeadlineResponder.java @@ -0,0 +1,29 @@ +package com.lockarhythm.query.deadline; + +import com.lockarhythm.query.AddedTaskResult; +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskList; + +public class DeadlineResponder extends RegexQueryInterpreter { + private TaskList list; + + public DeadlineResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^deadline (.+) \\/by (.+)"; + } + + public Result onMatch(String[] groups) { + try { + Task task = list.addDeadlineTask(groups[1], groups[2]); + return new AddedTaskResult(task, list.size()); + } catch (java.time.format.DateTimeParseException e) { + return new Result( + String.format("Please give me a valid LocalDate pattern: %s", e.getMessage())); + } + } +} diff --git a/src/main/java/com/lockarhythm/query/delete/DeleteResponder.java b/src/main/java/com/lockarhythm/query/delete/DeleteResponder.java new file mode 100644 index 00000000..7dfa9b10 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/delete/DeleteResponder.java @@ -0,0 +1,29 @@ +package com.lockarhythm.query.delete; + +import com.lockarhythm.query.DeletedTaskResult; +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskList; + +public class DeleteResponder extends RegexQueryInterpreter { + private TaskList list; + + public DeleteResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^delete (\\d+)"; + } + + public Result onMatch(String[] groups) { + int index = Integer.parseInt(groups[1]); + try { + Task task = list.deleteTask(index - 1); + return new DeletedTaskResult(task, list.size()); + } catch (IndexOutOfBoundsException e) { + return new Result(String.format("Item %d is not on the list. I cannot delete it!", index)); + } + } +} diff --git a/src/main/java/com/lockarhythm/query/echo/EchoResponder.java b/src/main/java/com/lockarhythm/query/echo/EchoResponder.java new file mode 100644 index 00000000..7b6f8a32 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/echo/EchoResponder.java @@ -0,0 +1,10 @@ +package com.lockarhythm.query.echo; + +import com.lockarhythm.query.QueryInterpreter; +import com.lockarhythm.query.Result; + +public class EchoResponder implements QueryInterpreter { + public Result interpret(String query) { + return new Result(query); + } +} diff --git a/src/main/java/com/lockarhythm/query/event/EventResponder.java b/src/main/java/com/lockarhythm/query/event/EventResponder.java new file mode 100644 index 00000000..2a8a088d --- /dev/null +++ b/src/main/java/com/lockarhythm/query/event/EventResponder.java @@ -0,0 +1,29 @@ +package com.lockarhythm.query.event; + +import com.lockarhythm.query.AddedTaskResult; +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskList; + +public class EventResponder extends RegexQueryInterpreter { + private TaskList list; + + public EventResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^event (.+) \\/at (.+)"; + } + + public Result onMatch(String[] groups) { + try { + Task task = list.addEventTask(groups[1], groups[2]); + return new AddedTaskResult(task, list.size()); + } catch (java.time.format.DateTimeParseException e) { + return new Result( + String.format("Please give me a valid LocalDate pattern: %s", e.getMessage())); + } + } +} diff --git a/src/main/java/com/lockarhythm/query/exit/ExitResponder.java b/src/main/java/com/lockarhythm/query/exit/ExitResponder.java new file mode 100644 index 00000000..452edcd3 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/exit/ExitResponder.java @@ -0,0 +1,13 @@ +package com.lockarhythm.query.exit; + +import com.lockarhythm.query.QueryInterpreter; +import com.lockarhythm.query.Result; + +public class ExitResponder implements QueryInterpreter { + public Result interpret(String query) { + if (query.equals("bye")) { + return new Result("Bye. Hope to see you again soon!", true); + } + return null; + } +} diff --git a/src/main/java/com/lockarhythm/query/find/FindResponder.java b/src/main/java/com/lockarhythm/query/find/FindResponder.java new file mode 100644 index 00000000..0ef60507 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/find/FindResponder.java @@ -0,0 +1,31 @@ +package com.lockarhythm.query.find; + +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.TaskList; + +public class FindResponder extends RegexQueryInterpreter { + private TaskList list; + + public FindResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^find (.*)$"; + } + + public Result onMatch(String[] groups) { + String query = groups[1]; + + TaskList filteredList = list.find(query); + + if (filteredList.size() == 0) { + return new Result( + String.format("I could not find any tasks matching your query '%s'", query)); + } + return new Result( + String.format( + "Ok! I have found these tasks for your query '%s'!\n%s", query, filteredList)); + } +} diff --git a/src/main/java/com/lockarhythm/query/list/ListResponder.java b/src/main/java/com/lockarhythm/query/list/ListResponder.java new file mode 100644 index 00000000..ba52a90c --- /dev/null +++ b/src/main/java/com/lockarhythm/query/list/ListResponder.java @@ -0,0 +1,20 @@ +package com.lockarhythm.query.list; + +import com.lockarhythm.query.QueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.TaskList; + +public final class ListResponder implements QueryInterpreter { + private TaskList list; + + public ListResponder(TaskList list) { + this.list = list; + } + + public Result interpret(String query) { + if (query.equals("list")) { + return new Result("Here are the tasks in your list:\n" + list.toString()); + } + return null; + } +} diff --git a/src/main/java/com/lockarhythm/query/markasdone/MarkAsDoneResponder.java b/src/main/java/com/lockarhythm/query/markasdone/MarkAsDoneResponder.java new file mode 100644 index 00000000..bcb629c2 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/markasdone/MarkAsDoneResponder.java @@ -0,0 +1,28 @@ +package com.lockarhythm.query.markasdone; + +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskList; + +public class MarkAsDoneResponder extends RegexQueryInterpreter { + private TaskList list; + + public MarkAsDoneResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^done (\\d+)"; + } + + public Result onMatch(String[] groups) { + int i = Integer.parseInt(groups[1]); + try { + Task task = list.markAsDone(i - 1); + return new Result("Nice! I've marked this task as done:\n\t" + task.toString()); + } catch (IndexOutOfBoundsException e) { + return new Result(String.format("Item %d is not on the list. I cannot mark it as done!", i)); + } + } +} diff --git a/src/main/java/com/lockarhythm/query/sort/SortResponder.java b/src/main/java/com/lockarhythm/query/sort/SortResponder.java new file mode 100644 index 00000000..52ca5e65 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/sort/SortResponder.java @@ -0,0 +1,59 @@ +package com.lockarhythm.query.sort; + +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.TaskList; + +public class SortResponder extends RegexQueryInterpreter { + private static final boolean IS_ASCENDING_DEFAULT = true; + + private TaskList list; + + public SortResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^sort by (date|done)( desc| asc)?$"; + } + + public Result onMatch(String[] groups) { + String sortKey = groups[1]; + boolean isAscending = parseIsAscending(groups[2]); + + TaskList sortedList = sortBy(sortKey, isAscending); + + return new Result( + String.format( + "Ok! I have sorted the tasks by %s in %s order!\n%s", + sortKey, isAscending ? "ascending" : "descending", sortedList)); + } + + private boolean parseIsAscending(String value) { + if (value == null) { + return IS_ASCENDING_DEFAULT; + } + String s = value.trim(); + if (s.equals("desc")) { + return false; + } else if (s.equals("asc")) { + return true; + } else { + return IS_ASCENDING_DEFAULT; + } + } + + /** Sorts by the `sortKey` in the order given by `isAscending`. */ + private TaskList sortBy(String sortKey, boolean isAscending) { + switch (sortKey) { + case "done": + return list.sortByDone(isAscending); + case "date": + return list.sortByTaskDate(isAscending); + default: + assert false : "sortKey is not a valid value. Check command regex."; + } + ; + return list.sortByTaskDate(isAscending); + } +} diff --git a/src/main/java/com/lockarhythm/query/todo/TodoResponder.java b/src/main/java/com/lockarhythm/query/todo/TodoResponder.java new file mode 100644 index 00000000..9e144e27 --- /dev/null +++ b/src/main/java/com/lockarhythm/query/todo/TodoResponder.java @@ -0,0 +1,24 @@ +package com.lockarhythm.query.todo; + +import com.lockarhythm.query.AddedTaskResult; +import com.lockarhythm.query.RegexQueryInterpreter; +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskList; + +public class TodoResponder extends RegexQueryInterpreter { + private TaskList list; + + public TodoResponder(TaskList list) { + this.list = list; + } + + protected String commandRegex() { + return "^todo (.+)"; + } + + public Result onMatch(String[] groups) { + Task task = list.addTodoTask(groups[1]); + return new AddedTaskResult(task, list.size()); + } +} diff --git a/src/main/java/com/lockarhythm/storage/GsonLocalDate.java b/src/main/java/com/lockarhythm/storage/GsonLocalDate.java new file mode 100644 index 00000000..e2fe70bb --- /dev/null +++ b/src/main/java/com/lockarhythm/storage/GsonLocalDate.java @@ -0,0 +1,23 @@ +package com.lockarhythm.storage; + +import com.google.gson.*; +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class GsonLocalDate implements JsonSerializer, JsonDeserializer { + + @Override + public LocalDate deserialize( + JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) + throws JsonParseException { + String ldtString = jsonElement.getAsString(); + return LocalDate.parse(ldtString, DateTimeFormatter.ISO_LOCAL_DATE); + } + + @Override + public JsonElement serialize( + LocalDate localDate, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(localDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + } +} diff --git a/src/main/java/com/lockarhythm/storage/GsonLocalDateTime.java b/src/main/java/com/lockarhythm/storage/GsonLocalDateTime.java new file mode 100644 index 00000000..1fea45ef --- /dev/null +++ b/src/main/java/com/lockarhythm/storage/GsonLocalDateTime.java @@ -0,0 +1,24 @@ +package com.lockarhythm.storage; + +import com.google.gson.*; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class GsonLocalDateTime + implements JsonSerializer, JsonDeserializer { + + @Override + public LocalDateTime deserialize( + JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) + throws JsonParseException { + String ldtString = jsonElement.getAsString(); + return LocalDateTime.parse(ldtString, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + + @Override + public JsonElement serialize( + LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + } +} diff --git a/src/main/java/com/lockarhythm/storage/Storage.java b/src/main/java/com/lockarhythm/storage/Storage.java new file mode 100644 index 00000000..c770d632 --- /dev/null +++ b/src/main/java/com/lockarhythm/storage/Storage.java @@ -0,0 +1,85 @@ +package com.lockarhythm.storage; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.lockarhythm.tasks.Task; +import com.lockarhythm.tasks.TaskDeserializer; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; + +/** + * Storage handles all concerns relating to persisting tasks to disk. + */ +public class Storage { + private String filePath; + + private ArrayList list; + + private Gson gson; + + /** + * Storage constructor takes in filePath as the destination path to write bytes to. + * + * @param filePath a valid location path to persist the tasks to. + */ + public Storage(String filePath) { + this.filePath = filePath; + + gson = + new GsonBuilder() + .registerTypeAdapter(Task.class, new TaskDeserializer("_type")) + .registerTypeAdapter(LocalDateTime.class, new GsonLocalDateTime()) + .registerTypeAdapter(LocalDate.class, new GsonLocalDate()) + .setPrettyPrinting() + .create(); + } + + /** + * registerList is meant to allow top-level Application to pass a reference a the Task List so that Storage can save a reference. + * + * @param list an array list of tasks to be persisted. + */ + public void registerList(ArrayList list) { + this.list = list; + } + + /** + * load reads the array list of tasks from disk. In order to handle persisting of abstract classes like Task with GSON, we need to pass its class in as parameter. + * + * @param type to pass the abstract type that is expected. + * @return an array list of tasks from disk. + */ + public ArrayList load(Class type) { + String content; + try { + content = Files.readString(Path.of(filePath), StandardCharsets.UTF_8); + + Type typeOfT = TypeToken.getParameterized(ArrayList.class, type).getType(); + ArrayList deserialized = + gson.fromJson(content, new TypeToken>() {}.getType()); + if (deserialized == null) { + return new ArrayList(); + } + return deserialized; + } catch (IOException e) { + return new ArrayList(); + } + } + + /** + * overwrite writes the current array list to disk. + */ + public void overwrite() throws IOException { + FileOutputStream fo = new FileOutputStream(filePath); + String js = gson.toJson(list); + fo.write(js.getBytes()); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/DeadlineTask.java b/src/main/java/com/lockarhythm/tasks/DeadlineTask.java new file mode 100644 index 00000000..d6239988 --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/DeadlineTask.java @@ -0,0 +1,26 @@ +package com.lockarhythm.tasks; + +final class DeadlineTask extends Task { + private TaskDate by; + private String _type = "DEADLINE"; + + public DeadlineTask(String description, String by) { + super(description); + this.by = new TaskDate(by); + } + + @Override + public TaskDate getTaskDate() { + return by; + } + + @Override + protected String getTaskTypeIcon() { + return "D"; + } + + @Override + public String toString() { + return String.format("%s (by: %s)", super.toString(), by); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/EventTask.java b/src/main/java/com/lockarhythm/tasks/EventTask.java new file mode 100644 index 00000000..60b8c5b2 --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/EventTask.java @@ -0,0 +1,26 @@ +package com.lockarhythm.tasks; + +final class EventTask extends Task { + private TaskDate at; + private String _type = "EVENT"; + + public EventTask(String description, String at) { + super(description); + this.at = new TaskDate(at); + } + + @Override + public TaskDate getTaskDate() { + return at; + } + + @Override + protected String getTaskTypeIcon() { + return "E"; + } + + @Override + public String toString() { + return String.format("%s (at: %s)", super.toString(), at); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/Task.java b/src/main/java/com/lockarhythm/tasks/Task.java new file mode 100644 index 00000000..4a739823 --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/Task.java @@ -0,0 +1,47 @@ +package com.lockarhythm.tasks; + +import java.io.Serializable; + +public abstract class Task implements Serializable { + private static transient String DEFAULT_TASK_DATE_VALUE = "3000-12-31T23:59:59"; + private static transient TaskDate DEFAULT_TASK_DATE = new TaskDate(DEFAULT_TASK_DATE_VALUE); + + private String description; + private boolean isDone; + + public Task(String description) { + this.description = description; + this.isDone = false; + } + + public void toggleDone() { + this.isDone = !this.isDone; + } + + public void setDone(boolean isDone) { + this.isDone = isDone; + } + + public String getDescription() { + return description; + } + + public boolean isDone() { + return isDone; + } + + public TaskDate getTaskDate() { + return DEFAULT_TASK_DATE; + } + + private String getDoneIcon() { + return isDone ? "X" : " "; + } + + protected abstract String getTaskTypeIcon(); + + @Override + public String toString() { + return String.format("[%s][%s] %s", getTaskTypeIcon(), getDoneIcon(), description); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/TaskDate.java b/src/main/java/com/lockarhythm/tasks/TaskDate.java new file mode 100644 index 00000000..46552183 --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/TaskDate.java @@ -0,0 +1,35 @@ +package com.lockarhythm.tasks; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.List; +import org.ocpsoft.prettytime.nlp.PrettyTimeParser; + +class TaskDate implements Serializable { + private LocalDateTime d; + + public TaskDate(String s) { + List dates = new PrettyTimeParser().parse(s); + if (dates.size() > 0) { + d = toLocalDateTime(dates.get(0)); + return; + } + d = LocalDateTime.parse(s); + } + + public static int compare(TaskDate x, TaskDate y) { + return x.d.compareTo(y.d); + } + + @Override + public String toString() { + return d.format(DateTimeFormatter.ofPattern("MMM d yyyy, HH:mm")); + } + + private LocalDateTime toLocalDateTime(Date dateToConvert) { + return LocalDateTime.ofInstant(dateToConvert.toInstant(), ZoneId.systemDefault()); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/TaskDeserializer.java b/src/main/java/com/lockarhythm/tasks/TaskDeserializer.java new file mode 100644 index 00000000..d157e563 --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/TaskDeserializer.java @@ -0,0 +1,44 @@ +package com.lockarhythm.tasks; + +import com.google.gson.*; +import com.lockarhythm.storage.GsonLocalDate; +import com.lockarhythm.storage.GsonLocalDateTime; +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** Technique credit to https://www.baeldung.com/gson-list */ +public class TaskDeserializer implements JsonDeserializer { + private String taskTypeElementName; + private Gson gson; + private Map> taskTypeRegistry; + + public TaskDeserializer(String taskTypeElementName) { + this.gson = + new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new GsonLocalDateTime()) + .registerTypeAdapter(LocalDate.class, new GsonLocalDate()) + .create(); + + this.taskTypeElementName = taskTypeElementName; + this.taskTypeRegistry = new HashMap<>(); + + this.registerTaskType("DEADLINE", DeadlineTask.class); + this.registerTaskType("TODO", TodoTask.class); + this.registerTaskType("EVENT", EventTask.class); + } + + public void registerTaskType(String taskTypeName, Class taskType) { + taskTypeRegistry.put(taskTypeName, taskType); + } + + public Task deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + JsonObject taskObject = json.getAsJsonObject(); + JsonElement taskTypeElement = taskObject.get(taskTypeElementName); + + Class taskType = taskTypeRegistry.get(taskTypeElement.getAsString()); + return gson.fromJson(taskObject, taskType); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/TaskList.java b/src/main/java/com/lockarhythm/tasks/TaskList.java new file mode 100644 index 00000000..1709accc --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/TaskList.java @@ -0,0 +1,100 @@ +package com.lockarhythm.tasks; + +import java.util.ArrayList; +import java.util.Collections; + +public class TaskList { + private ArrayList list; + + public TaskList() { + this.list = new ArrayList(); + } + + public TaskList(ArrayList list) { + this.list = list; + } + + public int size() { + return list.size(); + } + + public TodoTask addTodoTask(String description) { + TodoTask task = new TodoTask(description); + list.add(task); + return task; + } + + public DeadlineTask addDeadlineTask(String description, String by) { + DeadlineTask task = new DeadlineTask(description, by); + list.add(task); + return task; + } + + public EventTask addEventTask(String description, String at) { + EventTask task = new EventTask(description, at); + list.add(task); + return task; + } + + public Task markAsDone(int index) { + Task t = list.get(index); + t.setDone(true); + return t; + } + + public Task deleteTask(int index) { + return list.remove(index); + } + + /** Returns a copy of TaskList sorted by the task date. */ + public TaskList sortByTaskDate(boolean isAscending) { + ArrayList copy = new ArrayList(list); + Collections.sort( + copy, + (a, b) -> { + if (isAscending) { + return TaskDate.compare(a.getTaskDate(), b.getTaskDate()); + } + return TaskDate.compare(b.getTaskDate(), a.getTaskDate()); + }); + return new TaskList(copy); + } + + /** Returns a copy of TaskList sorted by the "done" field. */ + public TaskList sortByDone(boolean isAscending) { + ArrayList copy = new ArrayList(list); + Collections.sort( + copy, + (a, b) -> { + if (isAscending) { + return Boolean.compare(b.isDone(), a.isDone()); + } + return Boolean.compare(a.isDone(), b.isDone()); + }); + return new TaskList(copy); + } + + /** Returns a new TaskList that matches the given "query" string. */ + public TaskList find(String query) { + ArrayList filtered = new ArrayList(); + for (Task task : list) { + if (task.getDescription().contains(query)) { + filtered.add(task); + } + } + return new TaskList(filtered); + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + int i = 1; + for (i = 0; i < list.size(); i++) { + s.append(i + 1); + s.append("."); + s.append(list.get(i)); + s.append("\n"); + } + return s.toString(); + } +} diff --git a/src/main/java/com/lockarhythm/tasks/TodoTask.java b/src/main/java/com/lockarhythm/tasks/TodoTask.java new file mode 100644 index 00000000..f7e2c263 --- /dev/null +++ b/src/main/java/com/lockarhythm/tasks/TodoTask.java @@ -0,0 +1,14 @@ +package com.lockarhythm.tasks; + +public final class TodoTask extends Task { + private String _type = "TODO"; + + public TodoTask(String description) { + super(description); + } + + @Override + protected String getTaskTypeIcon() { + return "T"; + } +} diff --git a/src/main/java/com/lockarhythm/ui/TerminalUI.java b/src/main/java/com/lockarhythm/ui/TerminalUI.java new file mode 100644 index 00000000..084eea25 --- /dev/null +++ b/src/main/java/com/lockarhythm/ui/TerminalUI.java @@ -0,0 +1,54 @@ +package com.lockarhythm.ui; + +import com.lockarhythm.query.Result; +import java.util.Arrays; +import java.util.Scanner; + +/** + * TerminalUI is an adapter of UI port. + */ +public final class TerminalUI implements UI { + private static Scanner in = new Scanner(System.in); + + /** + * nextLine reads the next line from standard input. + * + * @return the next line as a string + */ + public String nextLine() { + return in.nextLine(); + } + + /** + * hasNext returns true if another line is available in standard input. + * + * @return true if another line is available. False otherwise. + */ + public boolean hasNext() { + return in.hasNext(); + } + + /** + * print formats the given strings and prints to standard output. + * + * @param strings the list of strings to be printed + */ + public void print(String... strings) { + System.out.println("\t____________________________________________________________"); + for (String s : strings) { + s = Arrays.stream(s.split("\n")).map(x -> "\t" + x).reduce("", (x, y) -> x + y + "\n"); + System.out.println(s); + } + System.out.println("\t____________________________________________________________\n"); + } + + /** + * print formats the given Result and prints to standard output. This is a helper function to handle the common operation of printing results from query interpreters. + * + * @param res the task Result to print. + */ + public void print(Result res) { + assert res != null; + print(res.getText()); + } +} diff --git a/src/main/java/com/lockarhythm/ui/UI.java b/src/main/java/com/lockarhythm/ui/UI.java new file mode 100644 index 00000000..33c625f3 --- /dev/null +++ b/src/main/java/com/lockarhythm/ui/UI.java @@ -0,0 +1,36 @@ +package com.lockarhythm.ui; + +import com.lockarhythm.query.Result; + +/** + * UI is a port. An example adapter is the TerminalUI. + */ +public interface UI { + /** + * nextLine returns the next line from the input. + * + * @return the next line as a string. + */ + public String nextLine(); + + /** + * hasNext returns true if there are more line(s) to be consumed in the next call to nextLine. Returns false otherwise. + * + * @return true if a new line exists. False otherwise. + */ + public boolean hasNext(); + + /** + * print should print the given strings to the output. + * + * @param strings a list of strings to be printed + */ + public void print(String... strings); + + /** + * print is a helper function to print Results to output. + * + * @param res a task result to print. + */ + public void print(Result res); +} diff --git a/src/test/java/com/lockarhythm/application/TestTerminalDuke.java b/src/test/java/com/lockarhythm/application/TestTerminalDuke.java new file mode 100644 index 00000000..11d2f9b5 --- /dev/null +++ b/src/test/java/com/lockarhythm/application/TestTerminalDuke.java @@ -0,0 +1,36 @@ +package com.lockarhythm.application; + +import static org.junit.Assert.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import org.junit.Test; + +public class TestTerminalDuke { + private final PrintStream standardOut = System.out; + private final InputStream standardIn = System.in; + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final ByteArrayInputStream in = + new ByteArrayInputStream("todo read book\ntodo return book\nlist\nbye".getBytes()); + + @Test + public void testMeetsLevel2() throws Exception { + System.setIn(in); + System.setOut(new PrintStream(out)); + + TerminalDuke.main(null); + String output = out.toString().trim(); + assertFalse(output.isEmpty()); + + assertTrue("it adds the 1st item", output.contains("Now you have 1 tasks in the list")); + assertTrue("it adds the 2nd item", output.contains("Now you have 2 tasks in the list")); + assertTrue("it lists the items", output.contains("1.[T][ ] read book")); + assertTrue("it lists the items", output.contains("2.[T][ ] return book")); + assertTrue("it exits", output.contains("Bye. Hope to see you again soon!")); + + System.setOut(standardOut); + System.setIn(standardIn); + } +} diff --git a/src/test/java/com/lockarhythm/query/echo/TestEchoResponder.java b/src/test/java/com/lockarhythm/query/echo/TestEchoResponder.java new file mode 100644 index 00000000..a528908a --- /dev/null +++ b/src/test/java/com/lockarhythm/query/echo/TestEchoResponder.java @@ -0,0 +1,17 @@ +package com.lockarhythm.query.echo; + +import static org.junit.Assert.*; + +import com.lockarhythm.query.Result; +import org.junit.Test; + +public class TestEchoResponder { + @Test + public void testEchosBackText() throws Exception { + EchoResponder responder = new EchoResponder(); + Result res = responder.interpret("hello"); + + assertEquals("hello", res.getText()); + assertFalse(res.shouldExit()); + } +} diff --git a/src/test/java/com/lockarhythm/query/exit/TestExitResponder.java b/src/test/java/com/lockarhythm/query/exit/TestExitResponder.java new file mode 100644 index 00000000..da25c270 --- /dev/null +++ b/src/test/java/com/lockarhythm/query/exit/TestExitResponder.java @@ -0,0 +1,24 @@ +package com.lockarhythm.query.exit; + +import static org.junit.Assert.*; + +import com.lockarhythm.query.Result; +import org.junit.Test; + +public class TestExitResponder { + @Test + public void testExitsOnKeyword() throws Exception { + ExitResponder responder = new ExitResponder(); + Result res = responder.interpret("bye"); + + assertTrue(res.shouldExit()); + } + + @Test + public void testNullOnNonkeyword() throws Exception { + ExitResponder responder = new ExitResponder(); + Result res = responder.interpret("hello"); + + assertNull(res); + } +} diff --git a/src/test/java/com/lockarhythm/query/list/TestListResponder.java b/src/test/java/com/lockarhythm/query/list/TestListResponder.java new file mode 100644 index 00000000..03f7c638 --- /dev/null +++ b/src/test/java/com/lockarhythm/query/list/TestListResponder.java @@ -0,0 +1,21 @@ +package com.lockarhythm.query.list; + +import static org.junit.Assert.*; + +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.TaskList; +import org.junit.Test; + +public class TestListResponder { + @Test + public void testAddsItemsAndLists() throws Exception { + TaskList list = new TaskList(); + ListResponder responder = new ListResponder(list); + + list.addTodoTask("read book"); + list.addTodoTask("return book"); + + Result res = responder.interpret("list"); + assertTrue(res.getText().contains("1.[T][ ] read book\n2.[T][ ] return book\n")); + } +} diff --git a/src/test/java/com/lockarhythm/query/markasdone/TestMarkAsDoneResponder.java b/src/test/java/com/lockarhythm/query/markasdone/TestMarkAsDoneResponder.java new file mode 100644 index 00000000..9655d533 --- /dev/null +++ b/src/test/java/com/lockarhythm/query/markasdone/TestMarkAsDoneResponder.java @@ -0,0 +1,49 @@ +package com.lockarhythm.query.markasdone; + +import static org.junit.Assert.*; + +import com.lockarhythm.query.Result; +import com.lockarhythm.tasks.TaskList; +import org.junit.Test; + +public class TestMarkAsDoneResponder { + @Test + public void testMarkAsDoneHappyPath() throws Exception { + TaskList list = new TaskList(); + MarkAsDoneResponder responder = new MarkAsDoneResponder(list); + + list.addTodoTask("read book"); + list.addTodoTask("return book"); + + Result res = responder.interpret("done 1"); + assertTrue(res.getText().contains("Nice! I've marked this task as done")); + assertTrue(res.getText().contains("read book")); + + assertTrue(list.toString().contains("1.[T][X] read book\n2.[T][ ] return book\n")); + } + + @Test + public void testMarkAsDoneOnNonExistentItem() throws Exception { + TaskList list = new TaskList(); + MarkAsDoneResponder responder = new MarkAsDoneResponder(list); + + list.addTodoTask("read book"); + list.addTodoTask("return book"); + + Result res = responder.interpret("done 10000"); + assertTrue(res.getText().contains("Item 10000 is not on the list. I cannot mark it as done!")); + + assertTrue(list.toString().contains("1.[T][ ] read book\n2.[T][ ] return book\n")); + } + + @Test + public void testMarkAsDoneParseCommand() throws Exception { + TaskList list = new TaskList(); + MarkAsDoneResponder responder = new MarkAsDoneResponder(list); + + list.addTodoTask("read book"); + + Result res = responder.interpret("all done 10000"); + assertNull(res); + } +} diff --git a/src/test/java/com/lockarhythm/tasks/TestTaskList.java b/src/test/java/com/lockarhythm/tasks/TestTaskList.java new file mode 100644 index 00000000..5c98bd5c --- /dev/null +++ b/src/test/java/com/lockarhythm/tasks/TestTaskList.java @@ -0,0 +1,23 @@ +package com.lockarhythm.tasks; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class TestTaskList { + @Test + public void testAddTask() throws Exception { + TaskList list = new TaskList(); + + list.addTodoTask("learn useful libraries in java"); + list.addTodoTask("learn bazel"); + assertEquals("has correct number of items", 2, list.size()); + + Task task = list.markAsDone(1); + assertEquals("can mark tasks as done", "learn bazel", task.getDescription()); + assertTrue("task is set to done", task.isDone()); + + task = list.markAsDone(1); + assertTrue("it's an idempotent operation", task.isDone()); + } +} diff --git a/src/test/java/com/lockarhythm/tasks/TestTodoTask.java b/src/test/java/com/lockarhythm/tasks/TestTodoTask.java new file mode 100644 index 00000000..a9fb2591 --- /dev/null +++ b/src/test/java/com/lockarhythm/tasks/TestTodoTask.java @@ -0,0 +1,15 @@ +package com.lockarhythm.tasks; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class TestTodoTask { + @Test + public void testToggleDone() throws Exception { + TodoTask task = new TodoTask("learn java well"); + + task.toggleDone(); + assertTrue("task should be done", task.isDone()); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 657e74f6..11540f9b 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,7 +1,231 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + ____________________________________________________________ + Hello I'm + ____ _ + | _ \ _ _| | _____ + | | | | | | | |/ / _ \ + | |_| | |_| | < __/ + |____/ \__,_|_|\_\___| + + What can I do for you? + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [T][ ] borrow book + Now you have 1 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [D][ ] return book (by: Oct 25 2021, 22:00) + Now you have 2 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [E][ ] buy bread (at: Oct 25 2021, 08:00) + Now you have 3 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [D][ ] return book (by: Dec 31 2021, 23:59) + Now you have 4 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [E][ ] buy bread (at: Nov 30 2021, 23:59) + Now you have 5 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Nice! I've marked this task as done: + [T][X] borrow book + + ____________________________________________________________ + + ____________________________________________________________ + Here are the tasks in your list: + 1.[T][X] borrow book + 2.[D][ ] return book (by: Oct 25 2021, 22:00) + 3.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 4.[D][ ] return book (by: Dec 31 2021, 23:59) + 5.[E][ ] buy bread (at: Nov 30 2021, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Nice! I've marked this task as done: + [D][X] return book (by: Oct 25 2021, 22:00) + + ____________________________________________________________ + + ____________________________________________________________ + Nice! I've marked this task as done: + [D][X] return book (by: Oct 25 2021, 22:00) + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [T][ ] done 1 + Now you have 6 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Here are the tasks in your list: + 1.[T][X] borrow book + 2.[D][X] return book (by: Oct 25 2021, 22:00) + 3.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 4.[D][ ] return book (by: Dec 31 2021, 23:59) + 5.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 6.[T][ ] done 1 + + ____________________________________________________________ + + ____________________________________________________________ + Sorry, I don't understand that yet! + + ____________________________________________________________ + + ____________________________________________________________ + Item 0 is not on the list. I cannot delete it! + + ____________________________________________________________ + + ____________________________________________________________ + Noted. I've removed this task: + [D][X] return book (by: Oct 25 2021, 22:00) + Now you have 5 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + Now you have 6 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Got it. I've added this task: + [E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + Now you have 7 tasks in the list. + + ____________________________________________________________ + + ____________________________________________________________ + Ok! I have sorted the tasks by date in ascending order! + 1.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + 2.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 3.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 4.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 5.[D][ ] return book (by: Dec 31 2021, 23:59) + 6.[T][X] borrow book + 7.[T][ ] done 1 + + ____________________________________________________________ + + ____________________________________________________________ + Ok! I have sorted the tasks by date in descending order! + 1.[T][X] borrow book + 2.[T][ ] done 1 + 3.[D][ ] return book (by: Dec 31 2021, 23:59) + 4.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 5.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Here are the tasks in your list: + 1.[T][X] borrow book + 2.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 3.[D][ ] return book (by: Dec 31 2021, 23:59) + 4.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 5.[T][ ] done 1 + 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Nice! I've marked this task as done: + [D][X] return book (by: Dec 31 2021, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Ok! I have sorted the tasks by done in ascending order! + 1.[T][X] borrow book + 2.[D][X] return book (by: Dec 31 2021, 23:59) + 3.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 4.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 5.[T][ ] done 1 + 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Ok! I have sorted the tasks by done in ascending order! + 1.[T][X] borrow book + 2.[D][X] return book (by: Dec 31 2021, 23:59) + 3.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 4.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 5.[T][ ] done 1 + 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Ok! I have sorted the tasks by done in descending order! + 1.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 2.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 3.[T][ ] done 1 + 4.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 5.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + 6.[T][X] borrow book + 7.[D][X] return book (by: Dec 31 2021, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Here are the tasks in your list: + 1.[T][X] borrow book + 2.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 3.[D][X] return book (by: Dec 31 2021, 23:59) + 4.[E][ ] buy bread (at: Nov 30 2021, 23:59) + 5.[T][ ] done 1 + 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59) + 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + Ok! I have found these tasks for your query 'bread'! + 1.[E][ ] buy bread (at: Oct 25 2021, 08:00) + 2.[E][ ] buy bread (at: Nov 30 2021, 23:59) + + ____________________________________________________________ + + ____________________________________________________________ + I could not find any tasks matching your query 'answer2life' + + ____________________________________________________________ + + ____________________________________________________________ + Bye. Hope to see you again soon! + + ____________________________________________________________ diff --git a/text-ui-test/EXPECTED.json b/text-ui-test/EXPECTED.json new file mode 100644 index 00000000..86afcdee --- /dev/null +++ b/text-ui-test/EXPECTED.json @@ -0,0 +1,52 @@ +[ + { + "_type": "TODO", + "description": "borrow book", + "isDone": true + }, + { + "at": { + "d": "2021-10-25T08:00:00" + }, + "_type": "EVENT", + "description": "buy bread", + "isDone": false + }, + { + "by": { + "d": "2021-12-31T23:59:59" + }, + "_type": "DEADLINE", + "description": "return book", + "isDone": true + }, + { + "at": { + "d": "2021-11-30T23:59:59" + }, + "_type": "EVENT", + "description": "buy bread", + "isDone": false + }, + { + "_type": "TODO", + "description": "done 1", + "isDone": false + }, + { + "at": { + "d": "2008-10-31T23:59:59" + }, + "_type": "EVENT", + "description": "this task is so 2008", + "isDone": false + }, + { + "at": { + "d": "2000-12-31T23:59:59" + }, + "_type": "EVENT", + "description": "this task is so 2000-and-late", + "isDone": false + } +] \ No newline at end of file diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29b..1d8120d4 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1,27 @@ +todo borrow book +deadline return book /by Oct 25 10pm +event buy bread /at Oct 25 morning +deadline return book /by 2021-12-31T23:59:59 +event buy bread /at 2021-11-30T23:59:59 +done 1 +list +done 2 +done 2 +todo done 1 +list +a-command-that-duke-doesnt-know +delete 0 +delete 2 +event this task is so 2008 /at 2008-10-31T23:59:59 +event this task is so 2000-and-late /at 2000-12-31T23:59:59 +sort by date +sort by date desc +list +done 3 +sort by done +sort by done asc +sort by done desc +list +find bread +find answer2life +bye diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh old mode 100644 new mode 100755 index c9ec8700..bbc3283f --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -12,15 +12,21 @@ then rm ACTUAL.TXT fi +# delete tasks.json from previous run +if [ -e "./tasks.json" ] +then + rm tasks.json +fi + # 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 +if ! USE_BAZEL_VERSION=ac9353fab161efae4af72e73fbb657a762b3620d bazelisk build //:TerminalDukeQA then echo "********** BUILD FAILURE **********" 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 +../bazel-bin/TerminalDukeQA < input.txt > ACTUAL.TXT # convert to UNIX format cp EXPECTED.TXT EXPECTED-UNIX.TXT @@ -30,9 +36,19 @@ dos2unix ACTUAL.TXT EXPECTED-UNIX.TXT diff ACTUAL.TXT EXPECTED-UNIX.TXT if [ $? -eq 0 ] then - echo "Test result: PASSED" + echo "Test result: PASSED [UI TESTS]" +else + echo "Test result: FAILED [UI TESTS]" + exit 1 +fi + +# compare the output to the expected output +diff tasks.json EXPECTED.json +if [ $? -eq 0 ] +then + echo "Test result: PASSED [SERIALIZATION TESTS]" exit 0 else - echo "Test result: FAILED" + echo "Test result: FAILED [SERIALIZATION TESTS]" exit 1 -fi \ No newline at end of file +fi diff --git a/tools/format-code.sh b/tools/format-code.sh new file mode 100644 index 00000000..da44ba37 --- /dev/null +++ b/tools/format-code.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh +mkdir -p .cache +cd .cache +if [ ! -f google-java-format-1.7-all-deps.jar ] +then + curl -LJO "https://github.com/google/google-java-format/releases/download/google-java-format-1.7/google-java-format-1.7-all-deps.jar" + chmod 755 google-java-format-1.7-all-deps.jar +fi +cd .. + +changed_java_files=$(git diff --cached --name-only --diff-filter=ACMR | grep ".*java$" ) +echo $changed_java_files +java -jar .cache/google-java-format-1.7-all-deps.jar --replace $changed_java_files