diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000000..109efdf7bbb
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,25 @@
+name: MarkBind Action
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Graphviz
+ run: sudo apt-get install graphviz
+ - name: Install Java
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+ - name: Build & Deploy MarkBind site
+ uses: MarkBind/markbind-action@v2
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ rootDirectory: './docs'
+ baseUrl: '/tp' # assuming your repo name is tp
+ version: '^5.5.2'
diff --git a/.gitignore b/.gitignore
index 284c4ca7cd9..eab4c7db6a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,4 @@ src/test/data/sandbox/
# MacOS custom attributes files created by Finder
.DS_Store
docs/_site/
+docs/_markbind/logs/
diff --git a/README.md b/README.md
index 16208adb9b6..35d8c1ae960 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,13 @@
-[](https://github.com/se-edu/addressbook-level3/actions)
+[](https://github.com/AY2425S2-CS2103-F10-4/tp/actions/workflows/gradle.yml)
+[](https://codecov.io/github/AY2425S2-CS2103-F10-4/tp)

-* This is **a sample project for Software Engineering (SE) students**.
- Example usages:
- * as a starting point of a course project (as opposed to writing everything from scratch)
- * as a case study
* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details.
* It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big.
* It comes with a **reasonable level of user and developer documentation**.
-* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...).
-* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**.
+* It is named `TutorProMax` because it was created for tutors with weekly tuition arrangements. This address book can help tutors keep track of students' contact details.
+* For the detailed documentation of this project, see the **[TutorProMax Website](https://ay2425s2-cs2103-f10-4.github.io/tp/AboutUs.html)**.
* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info.
+* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org)
+
diff --git a/build.gradle b/build.gradle
index 0db3743584e..33ee873c313 100644
--- a/build.gradle
+++ b/build.gradle
@@ -31,7 +31,11 @@ task coverage(type: JacocoReport) {
executionData.from files(jacocoTestReport.executionData)
afterEvaluate {
classDirectories.from files(classDirectories.files.collect {
- fileTree(dir: it, exclude: ['**/*.jar'])
+ fileTree(dir: it, exclude: [
+ '**/*.jar',
+ 'seedu/address/ui/HelpWindow.class',
+ 'seedu/address/ui/WrappedTextTableCellFactory.class'
+ ])
})
}
reports {
@@ -59,6 +63,7 @@ dependencies {
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0'
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.10'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion
@@ -66,7 +71,11 @@ dependencies {
}
shadowJar {
- archiveFileName = 'addressbook.jar'
+ archiveFileName = 'tutorpromax.jar'
+}
+
+run {
+ enableAssertions = true
}
defaultTasks 'clean', 'test'
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000000..b518bd9ef4a
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,2 @@
+ignore:
+ - "src/main/java/seedu/address/ui"
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000000..1748e487fbd
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+_markbind/logs/
+
+# Dependency directories
+node_modules/
+
+# Production build files (change if you output the build to a different directory)
+_site/
+
+# Env
+.env
+.env.local
+
+# IDE configs
+.vscode/
+.idea/*
+*.iml
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index ff3f04abd02..864fde20d44 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -1,59 +1,87 @@
----
-layout: page
-title: About Us
----
-We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg).
-You can reach us at the email `seer[at]comp.nus.edu.sg`
-## Project team
-### John Doe
-
-[[homepage](http://www.comp.nus.edu.sg/~damithch)]
-[[github](https://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
-* Role: Project Advisor
-### Jane Doe
-
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
-* Role: Team Lead
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+ layout: default.md
+ title: "About Us"
+---
+
+# About Us
+
+We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg).
+
+You can reach us at the email `seer[at]comp.nus.edu.sg`
+
+## Project team
+
+### Brandon Koh
+
+
+
+[[github](http://github.com/brvndonkoh)]
+[[portfolio](team/brvndonkoh.md)]
+
+* Role: Developers
* Responsibilities: UI
-### Johnny Doe
+### Huang Tian
-
+
-[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)]
+[[github](http://github.com/huangtian666)] [[portfolio](team/huangtian666.md)]
* Role: Developer
* Responsibilities: Data
-### Jean Doe
+### David Chong
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](http://github.com/davidchongg)]
+[[portfolio](team/davidchongg.md)]
* Role: Developer
-* Responsibilities: Dev Ops + Threading
+* Responsibilities: Backend
-### James Doe
+### Jeron Kor
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](http://github.com/jeronkk)]
+[[portfolio](team/jeronkk.md)]
* Role: Developer
* Responsibilities: UI
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 13cf0faea16..32f6255f3b9 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,6 +1,8 @@
---
-layout: page
-title: Configuration guide
+ layout: default.md
+ title: "Configuration guide"
---
+# Configuration guide
+
Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`).
diff --git a/docs/DevOps.md b/docs/DevOps.md
index d2fd91a6001..8228c845e86 100644
--- a/docs/DevOps.md
+++ b/docs/DevOps.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: DevOps guide
+ layout: default.md
+ title: "DevOps guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# DevOps guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
## Build automation
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 743c65a49d2..a82a04b60f6 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -1,15 +1,19 @@
---
-layout: page
-title: Developer Guide
+ layout: default.md
+ title: "Developer Guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+
+# TutorProMax Developer Guide
+
+
+
--------------------------------------------------------------------------------------------------------------------
## **Acknowledgements**
-* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well}
+ TutorProxMax is adapted from AddressBook Level 3: https://github.com/nus-cs2103-AY2425S2/tp
--------------------------------------------------------------------------------------------------------------------
@@ -21,14 +25,9 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
## **Design**
-
-
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams.
-
-
### Architecture
-
+
The ***Architecture Diagram*** given above explains the high-level design of the App.
@@ -53,7 +52,7 @@ The bulk of the app's work is done by the following four components:
The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`.
-
+
Each of the four main components (also shown in the diagram above),
@@ -62,7 +61,7 @@ Each of the four main components (also shown in the diagram above),
For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below.
-
+
The sections below give more details of each component.
@@ -70,7 +69,7 @@ The sections below give more details of each component.
The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java)
-
+
The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI.
@@ -89,14 +88,16 @@ The `UI` component,
Here's a (partial) class diagram of the `Logic` component:
-
+
The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example.
-
+
-
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
-
+
+
+**Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
+
How the `Logic` component works:
@@ -108,7 +109,7 @@ How the `Logic` component works:
Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command:
-
+
How the parsing works:
* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object.
@@ -117,7 +118,7 @@ How the parsing works:
### Model component
**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java)
-
+
The `Model` component,
@@ -127,18 +128,20 @@ The `Model` component,
* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects.
* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components)
-
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
+
+**Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
-
+
-
+
### Storage component
**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java)
-
+
The `Storage` component,
* can save both address book data and user preference data in JSON format, and read them back into corresponding objects.
@@ -155,228 +158,427 @@ Classes used by multiple components are in the `seedu.address.commons` package.
This section describes some noteworthy details on how certain features are implemented.
-### \[Proposed\] Undo/redo feature
+### List Command
-#### Proposed Implementation
+The sequence diagram below illustrates how the list command is processed when the user enters a command such as `list`, `list t/Math`, `list tt/Monday`, `list tt/mon` .
+This command allows users to either
+- list all student via `list`
+- list all students with a specific subject specified after `t/`.
+ - example: `list t/math`, `list t/Science`.
+- list all students with a tuition time on a specific day specified after prefix `tt/`.
+ - example: `list tt/Monday`, `list tt/mon`, `list tt/Monday, 1000-1200`
-The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations:
+The process involves the following key objects:
+- LogicManager parses the user input into a ListCommand.
-* `VersionedAddressBook#commit()` — Saves the current address book state in its history.
-* `VersionedAddressBook#undo()` — Restores the previous address book state from its history.
-* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history.
+- ListCommand is executed via the Model.
-These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively.
+- The model updates its filtered list using the predicate provided by ListCommand.
-Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.
+- The UI is automatically refreshed based on the updated filtered list.
+
+
-Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state.
-
+### Sort Command
-Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state.
+The `sort` feature allows tutors to organize the student list based on specific fields such as name, address, email, phone, subject, or tuition timing. This improves usability by enabling tutors to view student data in a desired order, which is especially useful when managing many students.
-
+The `sort` command is implemented using the `SortCommand` class, which accepts a field keyword (e.g., `name`, `phone`) and applies the corresponding comparator to the address book list. The command then updates the filtered person list with the sorted result.
-Step 3. The user executes `add n/David …` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`.
+The sorting logic is encapsulated in the `PersonComparators` utility class, which contains static comparators for each sortable field. This separation of concerns makes it easier to maintain and extend.
-
+When the `sort` command is executed:
-
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`.
+1. The user input is parsed into a `SortCommand` with a specific comparator.
+2. The model’s `sortPersonList` method is called with this comparator.
+3. The list displayed to the user is updated to reflect the new sorted order.
-
+If an invalid or unsupported field is provided, the command will return an error message explaining valid options.
-Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state.
+
-
+--------------------------------------------------------------------------------------------------------------------
-
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather
-than attempting to perform the undo.
+## **Documentation, logging, testing, configuration, dev-ops**
-
+* [Documentation guide](Documentation.md)
+* [Testing guide](Testing.md)
+* [Logging guide](Logging.md)
+* [Configuration guide](Configuration.md)
+* [DevOps guide](DevOps.md)
-The following sequence diagram shows how an undo operation goes through the `Logic` component:
+--------------------------------------------------------------------------------------------------------------------
-
+## **Appendix: Requirements**
-
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
+### Product scope
-
+**Target user profile**:
-Similarly, how an undo operation goes through the `Model` component is shown below:
+* private tutors who need to manage a significant number of contacts and lesson details
+* private tutors who conduct weekly lessons to each student in the day
+* prefer desktop apps over other types
+* can type fast
+* prefers typing to mouse interactions
+* is reasonably comfortable using CLI apps
+* the context is **Singapore** where the form of communication is mainly in English
-
+**Value proposition**:
+- **TutorProMax** is an all-in-one desktop assistant built specifically for tutors.
+Whether they are juggling multiple students, managing lesson time, or students' subjects, TutorProMax keeps everything organised in one place.
+With intuitive features like scheduling, reminders, and offline support, it empowers tutors to focus more on teaching and less on administrative work. Say goodbye to cluttered spreadsheets and scattered notes — TutorProMax helps tutors run their tutoring business like pros.
+- It is especially suited for tutors who can type fast and prefer a **Command Line Interface (CLI)** over graphical interfaces, allowing for faster, more efficient workflows.
-The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state.
-
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
+### User stories
-
+Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*`
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged.
+| Priority | As a … | I want to … | So that I can… |
+|----------|----------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------|
+| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App |
+| `* * *` | user | add a student | keep track of all students and can refer to their contact details when I need |
+| `* * *` | user | delete a student | remove outdated or irrelevant student contact details |
+| `* * *` | user | view all students' contact details | easily access students’ information and retrieve their contact details efficiently |
+| `* * *` | user | find a student by his/her name | locate details of students without having to go through the entire list |
+| `* *` | user | edit a contact details | update new information without having to go through the process of delete and add |
+| `* *` | user | list lessons by date | view all lessons scheduled on a particular day |
+| `* *` | user | list students by subject | view all students with a filtered subject |
+| `* *` | user | sort students according to a field I specified | view all student a in particular order for to find them more easily |
+| `* *` | user | Press `↑` and `↓` keys after entering commands | navigate the previous commands more easily especially if i want to add, delete, edit multiple students |
-
-Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …` command. This is the behavior that most modern desktop applications follow.
+### Use cases
-
+(For all use cases below, the **System** is the `AddressBook (TutorProMax)` and the **Actor** is the `user (tutor)`, unless specified otherwise)
-The following activity diagram summarizes what happens when a user executes a new command:
+**Use case: Add a student**
-
+**MSS**
+1. User requests to add a specific person in the list
+2. TutorProMax adds the student
-#### Design considerations:
+ Use case ends.
-**Aspect: How undo & redo executes:**
+**Extensions**
-* **Alternative 1 (current choice):** Saves the entire address book.
- * Pros: Easy to implement.
- * Cons: May have performance issues in terms of memory usage.
+* 1a. The student already exists in the list
-* **Alternative 2:** Individual command knows how to undo/redo by
- itself.
- * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted).
- * Cons: We must ensure that the implementation of each individual command are correct.
+* 1a1. TutorProMax shows an error message
-_{more aspects and alternatives to be added}_
+ * Use case resumes at step 2
-### \[Proposed\] Data archiving
-_{Explain here how the data archiving feature will be implemented}_
+**Use case: Delete a person**
+**MSS**
---------------------------------------------------------------------------------------------------------------------
+1. User requests to list persons
+2. TutorProMax shows a list of persons
+3. User requests to delete a specific person in the list
+4. TutorProMax deletes the person
-## **Documentation, logging, testing, configuration, dev-ops**
+ Use case ends.
-* [Documentation guide](Documentation.md)
-* [Testing guide](Testing.md)
-* [Logging guide](Logging.md)
-* [Configuration guide](Configuration.md)
-* [DevOps guide](DevOps.md)
+**Extensions**
---------------------------------------------------------------------------------------------------------------------
+* 2a. The list is empty.
-## **Appendix: Requirements**
+ Use case ends.
-### Product scope
+* 3a. The given index is invalid.
-**Target user profile**:
+ * 3a1. TutorProMax shows an error message.
-* has a need to manage a significant number of contacts
-* prefer desktop apps over other types
-* can type fast
-* prefers typing to mouse interactions
-* is reasonably comfortable using CLI apps
+ Use case resumes at step 2.
-**Value proposition**: manage contacts faster than a typical mouse/GUI driven app
+**Use case: Find a student by his/her name**
-### User stories
+**MSS**
-Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*`
+1. User requests to search for a student by name
+2. TutorProMax searches for the student in the list
+3. TutorProMax displays the matching student(s)
-| Priority | As a … | I want to … | So that I can… |
-| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- |
-| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App |
-| `* * *` | user | add a new person | |
-| `* * *` | user | delete a person | remove entries that I no longer need |
-| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list |
-| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident |
-| `*` | user with many persons in the address book | sort persons by name | locate a person easily |
+ Use case ends.
-*{More to be added}*
+**Extensions**
-### Use cases
+* 2a. No matching student found
-(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise)
+ * 2a1. TutorProMax displays a message indicating that no student matches the given name
-**Use case: Delete a person**
+ Use case ends
+
+**Use case: List persons**
**MSS**
1. User requests to list persons
-2. AddressBook shows a list of persons
-3. User requests to delete a specific person in the list
-4. AddressBook deletes the person
+2. TutorProMax lists all students in the address book.
+
+ Use case ends.
+**Extensions**
+* 1a. User specifies a tag keyword (e.g. `list t/math`).
+ * 1a1. TutorProMax lists students whose tags contain the specified keyword.
+ Use case ends.
+* 1b. User specifies a tuition day keyword (e.g. `list tt/monday`).
+ * 1b1. TutorProMax lists students whose tuition time contains the specified day.
+ Use case ends.
+* 1c. User specifies both tag and tuition time (e.g. `list tt/monday t/math`).
+ * 1c1. TutorProMax lists students who match both the tuition day and tag.
Use case ends.
+* 1d. No students match the given filter(s)
+ * 1d1. TutorProMax shows a message indicating no matching students found.
+ Use case ends.
+* 1e. User inputs an invalid format (e.g. `list tt/mondae`)
+ * 1e1. TutorProMax shows an error message indicating invalid input.
+ Use case ends.
+
+**Use Case: Sort students**
+
+**MSS**
+
+1. User requests to sort students by a specified field and order.
+2. TutorProMax sorts and displays the student list accordingly.
+
+ Use case ends.
**Extensions**
+* 1a. User specifies a valid field and order (e.g. `sort name asc`)
+ * 1a1. TutorProMax sorts the address book using the given field and order.
+ Use case ends.
-* 2a. The list is empty.
+* 1b. User specifies an invalid field (e.g. `sort grade asc`)
+ * 1b1. TutorProMax shows an error message:
+ `Invalid sort field. Valid fields: name, phone, email, address, tuition, tag`
+ Use case ends.
- Use case ends.
+* 1c. User omits sort order (e.g. `sort name`)
+ * 1c1. TutorProMax shows an error message indicating invalid command format.
+ Use case ends.
-* 3a. The given index is invalid.
+* 1d. User inputs an invalid sort order (e.g. `sort name upwards`)
+ * 1d1. TutorProMax shows an error message indicating sort order must be `asc` or `desc`.
+ Use case ends.
- * 3a1. AddressBook shows an error message.
+**Use Case: Edit a student**
- Use case resumes at step 2.
+**MSS**
+
+1. User requests to edit a specific student by specifying their index in the list.
+2. TutorProMax updates the student’s details with the provided fields.
+
+ Use case ends.
+
+**Extensions**
+* 1a. The specified index is invalid (e.g. index does not exist in the current list)
+ * 1a1. TutorProMax shows an error message indicating the index is invalid.
+ Use case ends.
+
+* 1b. No fields are specified for editing (e.g. `edit 2`)
+ * 1b1. TutorProMax shows an error message indicating that at least one field must be provided.
+ Use case ends.
+
+* 1c. User clears all tags by specifying `t/` without any tags
+ * 1c1. TutorProMax removes all tags from the student.
+ Use case ends.
+
+* 1d. User specifies invalid values (e.g. email without '@', invalid phone number, etc.)
+ * 1d1. TutorProMax shows an appropriate validation error message.
+ Use case ends.
-*{More to be added}*
### Non-Functional Requirements
1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed.
2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage.
3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse.
+4. The system should not require additional dependencies beyond standard Java libraries for core functionalities.
-*{More to be added}*
### Glossary
* **Mainstream OS**: Windows, Linux, Unix, MacOS
-* **Private contact detail**: A contact detail that is not meant to be shared with others
+* **Tutor**: Private tutor who conducts weekly one to one, face-to-face lessons with his/her students.
+* **Tag**: Tags are used to classify students according to **subjects**.
--------------------------------------------------------------------------------------------------------------------
+## Appendix: Instructions for Manual Testing
+
+Below are the manual test cases for verifying each feature of **TutorProMax**.
+
+> **Note:** These are starting points. Testers are encouraged to perform exploratory testing for more thorough coverage.
+
+---
+
+### Launch and Shutdown
-## **Appendix: Instructions for manual testing**
+#### 1. Initial Launch
+- Download the `.jar` file and copy it into an empty folder.
+- Open a terminal and `cd` into the folder.
+- Run: `java -jar tutorpromax.jar`
+- **Expected:** GUI opens with sample data.
-Given below are instructions to test the app manually.
+#### 2. Saving Window Preferences
+- Resize and move the window. Close the app.
+- Re-launch it using the same command.
+- **Expected:** Previous window size and position is retained.
+
+---
-
:information_source: **Note:** These instructions only provide a starting point for testers to work on;
-testers are expected to do more *exploratory* testing.
+### Adding a Person
-
+**Command:** `add n/John Doe p/98765432 e/john@example.com a/123 Clementi Rd tt/Monday, 1000-1200 t/Math t/Science`
-### Launch and shutdown
+- **Expected:** John Doe is added to the list with all the details correctly saved and displayed.
-1. Initial launch
+#### Invalid Cases to Test:
+- Missing mandatory fields:
+ - `add p/98765432`
+ - Error Message: `Invalid command format!
+ add: Adds a person to the address book. Parameters: n/NAME p/PHONE e/EMAIL a/ADDRESS tt/TUITION_TIME [t/TAG]...
+ Example: add n/John Doe p/98765432 e/johnd@example.com a/311, Clementi Ave 2, #02-25 tt/Monday, 1400-1600 t/Math t/Science`.
+- Invalid phone number: `add n/Jane p/abc123` → Same error message as above.
- 1. Download the jar file and copy into an empty folder
+---
- 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
+### Listing All Persons
+
+**Commands to Test:**
+- `list`
+- `list t/Math`
+- `list tt/Monday`
+
+- **Expected:**
+ - `list`: Shows all contacts.
+ - `list t/Math`: Filters only those with Math tag (case-sensitive).
+ - `list tt/Monday`: Filters only those with tuition on Monday.
+
+#### Invalid Cases to Test:
+
+- Invalid day format: `list tt/mondayyy`
+ - Error Message: `Invalid day entered. Please use a valid day like 'monday' or 'mon' (case-insensitive).
+ list with no prefix behind list all contacts.
+ list with prefix list all contacts with the keyword specified in rhe prefix.
+ Parameters: [field, keyword]
+ There are 2 fields available: t/[subject] or tt/[tuition time (day)].
+ Example: list t/Math
+ Example: list tt/Monday`
+- Empty field with prefix: `list t/`
+ - Error Message: `Please enter field and keyword!
+ list with no prefix behind list all contacts.
+ list with prefix list all contacts with the keyword specified in rhe prefix.
+ Parameters: [field, keyword]
+ There are 2 fields available: t/[subject] or tt/[tuition time (day)].
+ Example: list t/Math
+ Example: list tt/Monday`
-1. Saving window preferences
+---
- 1. Resize the window to an optimum size. Move the window to a different location. Close the window.
+### Finding Persons by Name
- 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained.
+**Command:** `find John`
-1. _{ more test cases … }_
+- **Expected:** All persons with "John" in their name (case-insensitive) are listed.
-### Deleting a person
+#### Additional Cases:
+- `find John Alex`
+- `find jOhN`
+- `find` (no input) → Should show error.
+ - `Invalid command format!
+ find: Finds all persons whose names contain any of the specified keywords (case-insensitive) and displays them as a list with index numbers.
+ Parameters: KEYWORD [MORE_KEYWORDS]...
+ Example: find alice bob charlie`
-1. Deleting a person while all persons are being shown
+---
- 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list.
+### Editing a Person
- 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated.
+**Command:** `edit 1 p/91234567 e/new@example.com`
- 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same.
+- **Expected:** Updates the phone and email of person at index 1.
- 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous.
+#### Additional Cases:
+- `edit 2 n/Alice t/` → Clears tags.
+- `edit 5` (no fields) → Error message.
+- `edit 0` or index out of bounds → Error message.
-1. _{ more test cases … }_
+---
-### Saving data
+### Deleting a Person
+
+**Command:** `delete 2`
+
+- **Expected:** The 2nd person in the current list is removed.
+
+#### Additional Cases:
+- `delete` → Error.
+- `delete abc` → Error.
+- `delete 100` → Error if out of bounds.
+
+---
+
+### Sorting
+
+**Commands to Test:**
+- `sort name asc`
+- `sort tuition desc`
+- `sort by email asc`
+
+- **Expected:** List is sorted as specified.
+
+#### Invalid Case:
+- `sort by banana desc` → Error: Invalid sort field.
+
+---
+
+### Clearing All Entries
+
+**Command:** `clear`
+
+- **Expected:** All contacts are removed.
+
+#### Edge Case:
+- `clear 123` → Should still work and clear everything.
+
+---
+
+### Exiting the Program
+
+**Command:** `exit`
+
+- **Expected:** Application closes.
+
+---
+
+### Help Command
+
+**Command:** `help`
+
+- **Expected:** Help window opens with list of commands.
+
+---
+
+### Command History
+
+- Press `↑` and `↓` keys after entering commands.
+- **Expected:** Cycles through previously entered commands.
+
+---
-1. Dealing with missing/corrupted data files
+### Saving and Editing Data
- 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_
+#### 1. Data Persistence
+- Add/edit/delete a person.
+- Close and reopen the app.
+- **Expected:** All data changes are saved.
-1. _{ more test cases … }_
+#### 2. Editing the JSON File
+- Navigate to `[home]/data/addressbook.json`
+- Manually modify a valid entry.
+- Restart the app.
+- **Expected:** Updated values are reflected.
diff --git a/docs/Documentation.md b/docs/Documentation.md
index 3e68ea364e7..082e652d947 100644
--- a/docs/Documentation.md
+++ b/docs/Documentation.md
@@ -1,29 +1,21 @@
---
-layout: page
-title: Documentation guide
+ layout: default.md
+ title: "Documentation guide"
+ pageNav: 3
---
-**Setting up and maintaining the project website:**
-
-* We use [**Jekyll**](https://jekyllrb.com/) to manage documentation.
-* The `docs/` folder is used for documentation.
-* To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html).
-* Note these points when adapting the documentation to a different project/product:
- * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar.
- * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format).
-* If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping)
+# Documentation Guide
+* We use [**MarkBind**](https://markbind.org/) to manage documentation.
+* The `docs/` folder contains the source files for the documentation website.
+* To learn how set it up and maintain the project website, follow the guide [[se-edu/guides] Working with Forked MarkBind sites](https://se-education.org/guides/tutorials/markbind-forked-sites.html) for project documentation.
**Style guidance:**
* Follow the [**_Google developer documentation style guide_**](https://developers.google.com/style).
+* Also relevant is the [_se-edu/guides **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html).
-* Also relevant is the [_[se-edu/guides] **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html)
-
-**Diagrams:**
-
-* See the [_[se-edu/guides] **Using PlantUML**_](https://se-education.org/guides/tutorials/plantUml.html)
-**Converting a document to the PDF format:**
+**Converting to PDF**
-* See the guide [_[se-edu/guides] **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html)
+* See the guide [_se-edu/guides **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html).
diff --git a/docs/Gemfile b/docs/Gemfile
deleted file mode 100644
index c8385d85874..00000000000
--- a/docs/Gemfile
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-source "https://rubygems.org"
-
-git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
-
-gem 'jekyll'
-gem 'github-pages', group: :jekyll_plugins
-gem 'wdm', '~> 0.1.0' if Gem.win_platform?
-gem 'webrick'
diff --git a/docs/Logging.md b/docs/Logging.md
index 5e4fb9bc217..589644ad5c6 100644
--- a/docs/Logging.md
+++ b/docs/Logging.md
@@ -1,8 +1,10 @@
---
-layout: page
-title: Logging guide
+ layout: default.md
+ title: "Logging guide"
---
+# Logging guide
+
* We are using `java.util.logging` package for logging.
* The `LogsCenter` class is used to manage the logging levels and logging destinations.
* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level.
diff --git a/docs/SettingUp.md b/docs/SettingUp.md
index aef33ec72fd..5e8b9d11991 100644
--- a/docs/SettingUp.md
+++ b/docs/SettingUp.md
@@ -1,27 +1,33 @@
---
-layout: page
-title: Setting up and getting started
+ layout: default.md
+ title: "Setting up and getting started"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# Setting up and getting started
+
+
--------------------------------------------------------------------------------------------------------------------
## Setting up the project in your computer
-
:exclamation: **Caution:**
+
+**Caution:**
Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps.
-
+
First, **fork** this repo, and **clone** the fork into your computer.
If you plan to use Intellij IDEA (highly recommended):
+
1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to ensure Intellij is configured to use **JDK 17**.
-1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
- :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project.
+1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
+
+ Note: Importing a Gradle project is slightly different from importing a normal Java project.
+
1. **Verify the setup**:
1. Run the `seedu.address.Main` and try a few commands.
1. [Run the tests](Testing.md) to ensure they all pass.
@@ -34,10 +40,11 @@ If you plan to use Intellij IDEA (highly recommended):
If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours.
-
:bulb: **Tip:**
+
+ **Tip:**
Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code.
-
+
1. **Set up CI**
diff --git a/docs/Testing.md b/docs/Testing.md
index 8a99e82438a..78ddc57e670 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: Testing guide
+ layout: default.md
+ title: "Testing guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# Testing guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
## Running tests
@@ -19,8 +22,10 @@ There are two ways to run tests.
* **Method 2: Using Gradle**
* Open a console and run the command `gradlew clean test` (Mac/Linux: `./gradlew clean test`)
-
:link: **Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle.
-
+
+
+**Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle.
+
--------------------------------------------------------------------------------------------------------------------
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 27c2d1cf16c..0bfa271d3ac 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -1,12 +1,34 @@
---
-layout: page
-title: User Guide
+ layout: default.md
+ title: "User Guide"
+ pageNav: 3
---
-AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps.
+# 🧑🏫 TutorProMax
+
+TutorProMax is a **desktop app for managing contacts, built specifically for private tutors in Singapore optimised for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, TutorProMax can get your contact management tasks more efficiently than traditional GUI apps.
+
+
+
+
+## Why Choose TutorProMax?
+
+TutorProMax isn't just another task management app — it's your ultimate companion for managing tutoring sessions and staying organized. Here's why you'll love it:
+
+- 📚 **Tailored for Tutors**
+ Manage students, track lessons, set subject preferences, and manage tuition time with ease.
+
+- ⏱️ **Fast workflow**
+ CLI commands let you manage your data quickly — perfect for fast typists.
+
+- 🔒 **Offline and Secure**
+ No internet required. Your data stays private and local.
+
+- 🖥️ **Portable**
+ Simple .jar file — no installation required.
+
+> 🚀 **TutorProMax helps you save time and stay organised!**
-* Table of Contents
-{:toc}
--------------------------------------------------------------------------------------------------------------------
@@ -15,20 +37,25 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
1. Ensure you have Java `17` or above installed in your Computer.
**Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html).
-1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases).
+1. Download the latest `.jar` file from [here](https://github.com/AY2425S2-CS2103-F10-4/tp/releases).
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook.
+1. Copy the file to the folder you want to use as the _home folder_ for TutorProMax.
-1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- 
+1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar tutorpromax.jar` command to run the application.
+ - ```bash
+ cd path/to/your/folder
+ java -jar tutorpromax.jar
+ - Alternatively, for Mac users, double-click on the folder where the jar file is in, and select `open terminal at the filder`
+
+3. A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
+ 
1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
Some example commands you can try:
* `list` : Lists all contacts.
- * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book.
+ * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 t/Math t/Science tt/Friday, 1000-1200` : Adds a contact named `John Doe` to TutorProMax.
* `delete 3` : Deletes the 3rd contact shown in the current list.
@@ -36,24 +63,81 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
* `exit` : Exits the app.
+ * `help` : Check for all available commands and their format.
+
1. Refer to the [Features](#features) below for details of each command.
--------------------------------------------------------------------------------------------------------------------
+## Parameters and Their Constraints
+
+This section outlines the validation rules for each parameter used in TutorProMax commands.
+
+### **Name**
+
+- **Allowed**:
+ - **Unicode letters** from various scripts (e.g., Latin, Chinese, Vietnamese).
+ - **Spaces**, as long as the entire name is not just spaces.
+ - **Digits**, but only when combined with letters (e.g., “peter the 2nd”). Purely numeric names are disallowed.
+ - **Standard punctuation** like apostrophes (`'`), hyphens (`-`), slashes (`/`), dots (`.`), etc.
+- **Disallowed**:
+ - **Empty** (e.g., `""`) or **spaces only** (e.g., `" "`).
+ - **Purely numeric** (e.g., `"12345"`).
+ - **Emojis** (e.g., `"🚀 Rocket"`).
+ - **Certain special symbols** on their own (e.g., `"^"`, `"peter*"`).
+- **Examples**:
+ `Nguyễn Văn A`, `张伟`, `Jean-Luc Picard`, `Vignesh S/O Muniyandi`, `Mary J. Blige`, `Dr. Tan`
+
+### **Phone**
+
+- **Format**: Must be recognized as a valid phone number.
+- **No letters**: Letters (`[A-Za-z]`) are disallowed.
+- **No consecutive spaces**: Cannot contain multiple spaces in a row.
+- **Must pass** [Google’s PhoneNumberUtil checks](https://github.com/google/libphonenumber).
+- **Examples**: `+65 9123 4567`, `91234567`, `+1 408-555-1234`
+
+### **Email**
+
+- **Format**: Must be in the form `local@domain`, with a valid domain name.
+- Allows alphanumeric and certain special characters for the local part, maximum of 64 characters.
+- Case-insensitive for the domain portion, maximum of 255 characters.
+- **Examples**: `alice@example.com`, `bob.smith@university.edu`, `JOHN_DOE@my-school.org`
+
+### **Address**
+
+- **Not Blank**: The address cannot be empty or consist only of spaces.
+- **Examples**:`John street, block 123, #01-01`, `12345 Maple Drive`
+
+### **Tuition Time**
+
+- **Required Format**: `, -`
+ - **DAY** can be full (`Monday`, `Tuesday`, …) or short (`Mon`, `Tue`, …). Case‐insensitive.
+ - **START_TIME** and **END_TIME** in 24‐hour format (`HHMM`), e.g., `1000-1200`.
+- Crossing midnight is **not** supported (e.g., `2300-0000`).
+- **Examples**: `Friday, 1400-1600`, `Mon, 0900-1000`
+
+### **Tags**
+
+- **Optional**: A person can have zero or more tags.
+- Alphanumeric
+- **Examples**: `Math`, `Science`, `English`
+
+--------------------------------------------------------------------------------------------------------------------
+
## Features
-
+
-**:information_source: Notes about the command format:**
+**Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`.
* Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`.
+ e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/Math` or as `n/John Doe`.
* Items with `…` after them can be used multiple times including zero times.
- e.g. `[t/TAG]…` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc.
+ e.g. `[t/TAG]…` can be used as ` ` (i.e. 0 times), `t/Math`, `t/Science t/English` etc.
* Parameters can be in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable.
@@ -61,12 +145,18 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`.
+* However, there are 2 extensions from the `list` command.
+ * `list` without any command behind will be treated as the basic list command where all contacts will be listed.
+ * `list t/` will list all students with the particular tag (subject).
+ * `list tt/` will list all students with tuition on a particular day.
+
* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
-
+
### Viewing help : `help`
-Shows a message explaning how to access the help page.
+Shows a list of commands available.
+In addition, it shows a message explaining how to access the detailed help page.

@@ -77,21 +167,47 @@ Format: `help`
Adds a person to the address book.
-Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…`
+Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS tt/TUITION_TIME [t/TAG]…`
+
+
-
:bulb: **Tip:**
-A person can have any number of tags (including 0)
-
+**Tip:** A person can have any number of tags (including 0)
+
Examples:
-* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`
-* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal`
+* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 tt/Monday, 1000-1200`
+* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/91234567 tt/Friday, 1400-1600 t/criminal`
+
+See [Parameters and Their Constraints](#parameters-and-their-constraints) for the exact rules each field must follow.
### Listing all persons : `list`
-Shows a list of all persons in the address book.
+Shows a list of all persons in the address book. Specify a subject or tuition time with keyword to
+list only persons with matching subject or tuition time. If both subject and tuition time are specified,
+both filter will be applied and only persons that fulfil both criteria will be shown.
-Format: `list`
+<<<<<<< HEAD
+Format: `list [t/SUBJECT] [tt/TUITIONTIME]`
+=======
+Format:
+* `list` - Lists all students
+* `list t/[keyword_tag]` - Lists by tag
+* `list tt/[keyword_tuition]` - Lists by tuition day (e.g. `Monday`, `mon`, etc.)
+
+**Accepted day formats:** You can use **full day names** (`Monday`, `Tuesday`, ...) or **short forms** (`mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`). The search is **case-insensitive**.
+>>>>>>> master
+
+Examples:
+* `list` Lists all persons in the address book.
+* `list t/math` Lists all persons with keyword math in their tags (subjects).
+<<<<<<< HEAD
+* `list tt/monday` Lists all persons with keyword monday in their tuition times.
+* `list t/math tt/Monday, 1000-1200` Lists all persons with keyword math in their
+tags (subjects) and tuition time of Monday, 1000-1200.
+=======
+* `list tt/monday` Lists all persons with tuition on Monday
+* `list tt/mon` – Also lists persons with tuition on Monday (short form).
+>>>>>>> master
### Editing a person : `edit`
@@ -110,6 +226,8 @@ Examples:
* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively.
* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags.
+See [Parameters and Their Constraints](#parameters-and-their-constraints) for the exact rules each field must follow.
+
### Locating persons by name: `find`
Finds persons whose names contain any of the given keywords.
@@ -128,6 +246,28 @@ Examples:
* `find alex david` returns `Alex Yeoh`, `David Li`

+### Sorting : `sort`
+Sort the address book based on field specified.
+
+Valid fields: `name`, `phone`, `email`, `address`, `tuition`, `tag`
+
+Format: `sort field asc/desc`
+
+Examples:
+* `sort name asc` Sorts address book using ascending order of names.
+* `sort tuition desc` Sorts address book using descending order of tuition times.
+* `sort email asc` Sorts address book using ascending order of email addresses.
+
+
+
+**Possible Errors:**
+
+❌ If you try to sort by an invalid field, TutorProMax will return:
+
+`Invalid sort field. Valid fields: name, phone, email, address, tuition, tag`
+
+
+
### Deleting a person : `delete`
Deletes the specified person from the address book.
@@ -162,10 +302,22 @@ AddressBook data are saved in the hard disk automatically after any command that
AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
-
:exclamation: **Caution:**
-If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
-
+
+
+**Caution:**
+If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
+
+
+### Command History Navigation : arrow keys
+
+Use the up (↑) and down (↓) arrow keys to navigate your previously entered commands.
+
+* Pressing up (↑) cycles backward through your command history.
+
+* Pressing down (↓) cycles forward to more recent commands.
+
+* This helps quickly re-enter or modify past commands without retyping.
### Archiving data files `[coming in v2.0]`
@@ -176,7 +328,23 @@ _Details coming soon ..._
## FAQ
**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder.
+**A**: Install **TutorProMax** on the new computer.
+
+- Run the app once to generate the necessary data folder.
+
+- On your original computer, navigate to the folder:
+`[TutorProMax]/data/addressbook.json`
+
+- Copy the `addressbook.json` file (this contains your data) to a USB drive or cloud storage.
+
+- On the new computer, go to the same directory:
+`[TutorProMax]/data/`
+
+- Replace the newly created `addressbook.json` with your copied version.
+
+- Launch TutorProMax. Your previously saved contacts should now be visible.
+
+ **Tip:** If you're unsure where the data folder is located, it is usually in the installation directory where **TutorProMax** was unzipped or installed. You can also check `preferences.json` for the exact path.
--------------------------------------------------------------------------------------------------------------------
@@ -189,12 +357,13 @@ _Details coming soon ..._
## Command summary
-Action | Format, Examples
---------|------------------
-**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…` e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague`
-**Clear** | `clear`
+Action | Format, Examples
+-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------
+**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS tt/TUITION_TIME [t/TAG]…` e.g., `add n/James Ho p/92224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 tt/Monday, 1000-1200 t/friend t/colleague`
+**Clear** | `clear`
**Delete** | `delete INDEX` e.g., `delete 3`
-**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…` e.g.,`edit 2 n/James Lee e/jameslee@example.com`
-**Find** | `find KEYWORD [MORE_KEYWORDS]` e.g., `find James Jake`
-**List** | `list`
-**Help** | `help`
+**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…` e.g.,`edit 2 n/James Lee e/jameslee@example.com`
+**Find** | `find KEYWORD [MORE_KEYWORDS]` e.g., `find James Jake`
+**Sort** | `sort [field] asc/desc` e.g. `sort name asc`
+**List** | `list [t/SUBJECT] [tt/TUITIONTIME]` e.g. `list` e.g. `list t/Math` e.g. `list tt/Monday` or `list tt/mon`
+**Help** | `help`
diff --git a/docs/_config.yml b/docs/_config.yml
deleted file mode 100644
index 6bd245d8f4e..00000000000
--- a/docs/_config.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-title: "AB-3"
-theme: minima
-
-header_pages:
- - UserGuide.md
- - DeveloperGuide.md
- - AboutUs.md
-
-markdown: kramdown
-
-repository: "se-edu/addressbook-level3"
-github_icon: "images/github-icon.png"
-
-plugins:
- - jemoji
diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml
deleted file mode 100644
index 8f3e50cb601..00000000000
--- a/docs/_data/projects.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-- name: "AB-1"
- url: https://se-edu.github.io/addressbook-level1
-
-- name: "AB-2"
- url: https://se-edu.github.io/addressbook-level2
-
-- name: "AB-3"
- url: https://se-edu.github.io/addressbook-level3
-
-- name: "AB-4"
- url: https://se-edu.github.io/addressbook-level4
-
-- name: "Duke"
- url: https://se-edu.github.io/duke
-
-- name: "Collate"
- url: https://se-edu.github.io/collate
-
-- name: "Book"
- url: https://se-edu.github.io/se-book
-
-- name: "Resources"
- url: https://se-edu.github.io/resources
diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html
deleted file mode 100644
index 8559a67ffad..00000000000
--- a/docs/_includes/custom-head.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% comment %}
- Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons:
-
- 1. Head over to https://realfavicongenerator.net/ to add your own favicons.
- 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet.
-{% endcomment %}
diff --git a/docs/_includes/head.html b/docs/_includes/head.html
deleted file mode 100644
index 83ac5326933..00000000000
--- a/docs/_includes/head.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
- {%- include custom-head.html -%}
-
- {{page.title}}
-
-
diff --git a/docs/_includes/header.html b/docs/_includes/header.html
deleted file mode 100644
index 33badcd4f99..00000000000
--- a/docs/_includes/header.html
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
This method performs the following operations in sequence:
+ *
+ *
Sets up cell value factories for each column to extract the appropriate data
+ * from {@link CommandUsageInfo} objects
+ *
Applies the {@link WrappedTextTableCellFactory} to all columns to enable
+ * text wrapping and consistent styling
+ *
Loads command data from {@link CommandConfig} into the table's items
+ *
Refreshes the table view to ensure all updates are visible
+ *
+ *
+ *
The resulting table will:
+ *
+ *
Display white text on dark background
+ *
Automatically wrap long text in cells
+ *
Show all commands from the configuration
+ *
+ *
+ * @implNote The method must be called after the FXML fields are injected but before
+ * the window is shown. The table columns ({@code commandColumn},
+ * {@code formatColumn}, {@code exampleColumn}) and the table view
+ * ({@code commandsTable}) must be properly initialized before calling this method.
+ */
+ public void initializeTable() {
+ // 1. First set up the cell value factories
+ commandColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().command()));
+ formatColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().format()));
+ exampleColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().example()));
+
+ // 2. Apply the wrapped text factory to ALL columns
+ commandColumn.setCellFactory(new WrappedTextTableCellFactory<>());
+ formatColumn.setCellFactory(new WrappedTextTableCellFactory<>());
+ exampleColumn.setCellFactory(new WrappedTextTableCellFactory<>());
+
+ // 3. ACTUALLY LOAD THE DATA (This was missing)
+ ObservableList commands = FXCollections.observableArrayList(
+ CommandConfig.getAllCommands()
+ );
+ commandsTable.setItems(commands);
+
+ // 4. Refresh the table to ensure updates are visible
+ commandsTable.refresh();
+ }
+
/**
* Shows the help window.
* @throws IllegalStateException
*
- *
+ *
* if this method is called on a thread other than the JavaFX Application Thread.
*
*
@@ -93,7 +157,7 @@ public void focus() {
* Copies the URL to the user guide to the clipboard.
*/
@FXML
- private void copyUrl() {
+ public void copyUrl() {
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent url = new ClipboardContent();
url.putString(USERGUIDE_URL);
diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java
index 79e74ef37c0..5b15a456420 100644
--- a/src/main/java/seedu/address/ui/MainWindow.java
+++ b/src/main/java/seedu/address/ui/MainWindow.java
@@ -66,6 +66,7 @@ public MainWindow(Stage primaryStage, Logic logic) {
setAccelerators();
helpWindow = new HelpWindow();
+ this.primaryStage.setTitle("TutorProMax");
}
public Stage getPrimaryStage() {
diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java
index 094c42cda82..62b8a563299 100644
--- a/src/main/java/seedu/address/ui/PersonCard.java
+++ b/src/main/java/seedu/address/ui/PersonCard.java
@@ -39,6 +39,8 @@ public class PersonCard extends UiPart {
@FXML
private Label email;
@FXML
+ private Label tuitionTime;
+ @FXML
private FlowPane tags;
/**
@@ -52,6 +54,7 @@ public PersonCard(Person person, int displayedIndex) {
phone.setText(person.getPhone().value);
address.setText(person.getAddress().value);
email.setText(person.getEmail().value);
+ tuitionTime.setText("Tuition Time: " + person.getTuitionTime().value);
person.getTags().stream()
.sorted(Comparator.comparing(tag -> tag.tagName))
.forEach(tag -> tags.getChildren().add(new Label(tag.tagName)));
diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/address/ui/Ui.java
index 17aa0b494fe..c5c4d07ea04 100644
--- a/src/main/java/seedu/address/ui/Ui.java
+++ b/src/main/java/seedu/address/ui/Ui.java
@@ -3,7 +3,7 @@
import javafx.stage.Stage;
/**
- * API of UI component
+ * API of the UI component
*/
public interface Ui {
diff --git a/src/main/java/seedu/address/ui/WrappedTextTableCellFactory.java b/src/main/java/seedu/address/ui/WrappedTextTableCellFactory.java
new file mode 100644
index 00000000000..1f08ba9ba5b
--- /dev/null
+++ b/src/main/java/seedu/address/ui/WrappedTextTableCellFactory.java
@@ -0,0 +1,65 @@
+package seedu.address.ui;
+
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Text;
+import javafx.util.Callback;
+
+/**
+ * A factory class that creates {@link TableCell} instances with text wrapping and white text color
+ * for use in JavaFX {@link TableColumn} controls. The generated cells will:
+ *
+ *
Automatically wrap text to fit within the column width
+ *
Display text in white color
+ *
Adjust row height dynamically based on content
+ *
+ *
+ * @param The type of the TableView generic type (typically the data model class)
+ * @param The type of the item contained within the cell (typically String)
+ */
+public class WrappedTextTableCellFactory implements Callback, TableCell> {
+
+ /**
+ * Creates and returns a new {@link TableCell} instance configured with text wrapping
+ * and white text color for the specified table column.
+ *
+ * @param param The table column that will use this cell factory
+ * @return A new TableCell instance with:
+ * - White text color
+ * - Automatic text wrapping
+ * - Dynamic row height adjustment
+ */
+ @Override
+ public TableCell call(TableColumn param) {
+ return new TableCell() {
+ private Text text = new Text();
+
+ {
+ text.setFill(Color.WHITE);
+ text.wrappingWidthProperty().bind(widthProperty().subtract(10));
+ setGraphic(text);
+ setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+ setStyle("-fx-text-fill: white;");
+ }
+
+ /**
+ * Updates the item displayed in the cell.
+ *
+ * @param item The new item for the cell, or null if empty
+ * @param empty true if the cell represents a null item, false otherwise
+ */
+ @Override
+ public void updateItem(T item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ text.setText(null);
+ } else {
+ text.setText(item.toString());
+ setStyle("-fx-text-fill: white;");
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml
index 124283a392e..64ac89ec0f5 100644
--- a/src/main/resources/view/CommandBox.fxml
+++ b/src/main/resources/view/CommandBox.fxml
@@ -6,4 +6,3 @@
-
diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css
index 17e8a8722cd..edc18e7cbe4 100644
--- a/src/main/resources/view/HelpWindow.css
+++ b/src/main/resources/view/HelpWindow.css
@@ -1,9 +1,79 @@
-#copyButton, #helpMessage {
+/* Table styles */
+.root {
+ -fx-background-color: #2d2d2d;
+}
+.table-view {
+ -fx-base: #2d2d2d;
+ -fx-control-inner-background: #2d2d2d;
+ -fx-background-color: #2d2d2d;
+ -fx-table-cell-border-color: transparent;
+ -fx-border-color: #4d4d4d;
+ -fx-border-width: 1.5;
+}
+
+.table-view .column-header-background {
+ -fx-background-color: linear-gradient(#3d3d3d 0%, #2d2d2d 100%);
+}
+
+.table-view .column-header, .table-view .filler {
+ -fx-background-color: transparent;
+ -fx-border-width: 0 0 1 0;
+ -fx-border-color: derive(-fx-base, 10%);
+}
+
+.table-view .column-header .label {
-fx-text-fill: white;
+ -fx-font-weight: bold;
}
+.table-row-cell {
+ -fx-text-fill: white;
+ -fx-background-color: #2d2d2d;
+}
+
+.table-row-cell:odd {
+ -fx-text-fill: white;
+ -fx-background-color: #3d3d3d;
+}
+
+.table-row-cell:selected {
+ -fx-background-color: #4d4d4d;
+}
+
+.table-cell {
+ -fx-cell-size: 35px;
+ -fx-wrap-text: true;
+ -fx-padding: 5px;
+ -fx-text-fill: white
+}
+
+.table-column {
+ -fx-alignment: CENTER-LEFT;
+}
+
+.table-view .cell {
+ -fx-wrap-text: true;
+ -fx-cell-size: -1; /* Auto-size */
+ -fx-padding: 5px;
+}
+
+.scroll-bar {
+ -fx-background-color: #2d2d2d;
+}
+
+.scroll-bar .thumb {
+ -fx-background-color: #555555;
+ -fx-background-insets: 2;
+ -fx-background-radius: 5;
+}
+
+/* Button styles */
#copyButton {
-fx-background-color: dimgray;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-padding: 5 15;
+ -fx-background-radius: 5;
}
#copyButton:hover {
@@ -14,6 +84,7 @@
-fx-background-color: darkgray;
}
-#helpMessageContainer {
- -fx-background-color: derive(#1d1d1d, 20%);
+/* Label styles */
+#helpMessage {
+ -fx-text-fill: white;
}
diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml
index e01f330de33..8d2f5f229ed 100644
--- a/src/main/resources/view/HelpWindow.fxml
+++ b/src/main/resources/view/HelpWindow.fxml
@@ -9,7 +9,12 @@
-
+
+
+
+
+
@@ -19,6 +24,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml
index 84e09833a87..50ac558fd94 100644
--- a/src/main/resources/view/PersonListCard.fxml
+++ b/src/main/resources/view/PersonListCard.fxml
@@ -25,12 +25,13 @@
-
+
-
-
-
+
+
+
+
diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml
index 01b691792a9..6044fe878ee 100644
--- a/src/main/resources/view/ResultDisplay.fxml
+++ b/src/main/resources/view/ResultDisplay.fxml
@@ -5,5 +5,5 @@
-
+
diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json
index a7427fe7aa2..8c898d2b987 100644
--- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json
+++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json
@@ -1,14 +1,20 @@
{
- "persons": [ {
- "name": "Alice Pauline",
- "phone": "94351253",
- "email": "alice@example.com",
- "address": "123, Jurong West Ave 6, #08-111",
- "tags": [ "friends" ]
- }, {
- "name": "Alice Pauline",
- "phone": "94351253",
- "email": "pauline@example.com",
- "address": "4th street"
- } ]
+ "persons": [
+ {
+ "name": "Alice Pauline",
+ "phone": "94351253",
+ "email": "alice@example.com",
+ "address": "123, Jurong West Ave 6, #08-111",
+ "tuitionTime": "Monday, 1400-1600",
+ "tags": ["Math"]
+ },
+ {
+ "name": "Alice Pauline",
+ "phone": "94351253",
+ "email": "pauline@example.com",
+ "address": "4th street",
+ "tuitionTime": "Tuesday, 1000-1200",
+ "tags": []
+ }
+ ]
}
diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json
index ad3f135ae42..2bfacf148a5 100644
--- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json
+++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json
@@ -1,8 +1,11 @@
{
- "persons": [ {
- "name": "Hans Muster",
- "phone": "9482424",
- "email": "invalid@email!3e",
- "address": "4th street"
- } ]
+ "persons": [
+ {
+ "name": "Hans Muster",
+ "phone": "9482424",
+ "email": "invalid@email!3e",
+ "address": "4th street",
+ "tuitionTime": "Not a valid time format"
+ }
+ ]
}
diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json
index 72262099d35..0123c28e034 100644
--- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json
+++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json
@@ -1,46 +1,60 @@
{
- "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()",
- "persons" : [ {
- "name" : "Alice Pauline",
- "phone" : "94351253",
- "email" : "alice@example.com",
- "address" : "123, Jurong West Ave 6, #08-111",
- "tags" : [ "friends" ]
- }, {
- "name" : "Benson Meier",
- "phone" : "98765432",
- "email" : "johnd@example.com",
- "address" : "311, Clementi Ave 2, #02-25",
- "tags" : [ "owesMoney", "friends" ]
- }, {
- "name" : "Carl Kurz",
- "phone" : "95352563",
- "email" : "heinz@example.com",
- "address" : "wall street",
- "tags" : [ ]
- }, {
- "name" : "Daniel Meier",
- "phone" : "87652533",
- "email" : "cornelia@example.com",
- "address" : "10th street",
- "tags" : [ "friends" ]
- }, {
- "name" : "Elle Meyer",
- "phone" : "9482224",
- "email" : "werner@example.com",
- "address" : "michegan ave",
- "tags" : [ ]
- }, {
- "name" : "Fiona Kunz",
- "phone" : "9482427",
- "email" : "lydia@example.com",
- "address" : "little tokyo",
- "tags" : [ ]
- }, {
- "name" : "George Best",
- "phone" : "9482442",
- "email" : "anna@example.com",
- "address" : "4th street",
- "tags" : [ ]
- } ]
+ "persons": [
+ {
+ "name": "Alice Pauline",
+ "phone": "94351253",
+ "email": "alice@example.com",
+ "address": "123, Jurong West Ave 6, #08-111",
+ "tuitionTime": "Monday, 1400-1600",
+ "tags": ["Math"]
+ },
+ {
+ "name": "Benson Meier",
+ "phone": "98765432",
+ "email": "johnd@example.com",
+ "address": "311, Clementi Ave 2, #02-25",
+ "tuitionTime": "Tuesday, 1000-1200",
+ "tags": ["Math", "Science"]
+ },
+ {
+ "name": "Carl Kurz",
+ "phone": "95352563",
+ "email": "heinz@example.com",
+ "address": "wall street",
+ "tuitionTime": "Wednesday, 0930-1130",
+ "tags": []
+ },
+ {
+ "name": "Daniel Meier",
+ "phone": "87652533",
+ "email": "cornelia@example.com",
+ "address": "10th street",
+ "tuitionTime": "Thursday, 1500-1700",
+ "tags": ["English"]
+ },
+ {
+ "name": "Elle Meyer",
+ "phone": "+6591234567",
+ "email": "werner@example.com",
+ "address": "michegan ave",
+ "tuitionTime": "Friday, 0800-1000",
+ "tags": []
+ },
+ {
+ "name": "Fiona Kunz",
+ "phone": "+14085551234",
+ "email": "lydia@example.com",
+ "address": "little tokyo",
+ "tuitionTime": "Saturday, 1300-1500",
+ "tags": []
+ },
+ {
+ "name": "George Best",
+ "phone": "+4915123456789",
+ "email": "anna@example.com",
+ "address": "4th street",
+ "tuitionTime": "Sunday, 1400-1600",
+ "tags": []
+ }
+ ]
}
diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java
index baf8ce336a2..a0b0cdd5b34 100644
--- a/src/test/java/seedu/address/logic/LogicManagerTest.java
+++ b/src/test/java/seedu/address/logic/LogicManagerTest.java
@@ -1,14 +1,13 @@
package seedu.address.logic;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static seedu.address.logic.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX;
import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND;
import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY;
import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY;
import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY;
import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY;
+import static seedu.address.logic.commands.CommandTestUtil.TUITION_TIME_DESC_AMY;
import static seedu.address.testutil.Assert.assertThrows;
-import static seedu.address.testutil.TypicalPersons.AMY;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
@@ -27,11 +26,9 @@
import seedu.address.model.ModelManager;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.UserPrefs;
-import seedu.address.model.person.Person;
import seedu.address.storage.JsonAddressBookStorage;
import seedu.address.storage.JsonUserPrefsStorage;
import seedu.address.storage.StorageManager;
-import seedu.address.testutil.PersonBuilder;
public class LogicManagerTest {
private static final IOException DUMMY_IO_EXCEPTION = new IOException("dummy IO exception");
@@ -49,9 +46,13 @@ public void setUp() {
new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json"));
JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json"));
StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage);
+
+ // ✅ Initialize model with sample persons
+ model = new ModelManager(seedu.address.testutil.TypicalPersons.getTypicalAddressBook(), new UserPrefs());
logic = new LogicManager(model, storage);
}
+
@Test
public void execute_invalidCommandFormat_throwsParseException() {
String invalidCommand = "uicfhmowqewca";
@@ -61,7 +62,10 @@ public void execute_invalidCommandFormat_throwsParseException() {
@Test
public void execute_commandExecutionError_throwsCommandException() {
String deleteCommand = "delete 9";
- assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX
+ + "Please provide a number between 1 and " + model.getFilteredPersonList().size();
+
+ assertCommandException(deleteCommand, expectedMessage);
}
@Test
@@ -166,10 +170,9 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath)
// Triggers the saveAddressBook method by executing an add command
String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY
- + EMAIL_DESC_AMY + ADDRESS_DESC_AMY;
- Person expectedPerson = new PersonBuilder(AMY).withTags().build();
- ModelManager expectedModel = new ModelManager();
- expectedModel.addPerson(expectedPerson);
+ + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + TUITION_TIME_DESC_AMY;
+ //Person expectedPerson = new PersonBuilder(AMY).withTags().build();
+ ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs());
assertCommandFailure(addCommand, CommandException.class, expectedMessage, expectedModel);
}
}
diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java
index 90e8253f48e..735683a60c6 100644
--- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java
@@ -10,6 +10,7 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
@@ -157,6 +158,12 @@ public ObservableList getFilteredPersonList() {
public void updateFilteredPersonList(Predicate predicate) {
throw new AssertionError("This method should not be called.");
}
+
+ @Override
+ public void sortFilteredPersonList(Comparator comparator) {
+ throw new AssertionError("This method should not be called.");
+ }
+
}
/**
diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
index 80d9110c03a..d6fcb01411b 100644
--- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java
@@ -17,7 +17,7 @@ public void execute_emptyAddressBook_success() {
Model model = new ModelManager();
Model expectedModel = new ModelManager();
- assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel);
+ assertCommandSuccess(new ClearCommand(), model, ClearCommand.EMPTYLIST_MESSAGE, expectedModel);
}
@Test
diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
index 643a1d08069..5518b65acf3 100644
--- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
+++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java
@@ -7,6 +7,7 @@
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TUITION_TIME;
import static seedu.address.testutil.Assert.assertThrows;
import java.util.ArrayList;
@@ -28,14 +29,16 @@ public class CommandTestUtil {
public static final String VALID_NAME_AMY = "Amy Bee";
public static final String VALID_NAME_BOB = "Bob Choo";
- public static final String VALID_PHONE_AMY = "11111111";
- public static final String VALID_PHONE_BOB = "22222222";
+ public static final String VALID_PHONE_AMY = "91234567";
+ public static final String VALID_PHONE_BOB = "+65 8123 4567";
public static final String VALID_EMAIL_AMY = "amy@example.com";
public static final String VALID_EMAIL_BOB = "bob@example.com";
public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1";
public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3";
- public static final String VALID_TAG_HUSBAND = "husband";
- public static final String VALID_TAG_FRIEND = "friend";
+ public static final String VALID_TUITION_TIME_AMY = "Monday, 1400-1600";
+ public static final String VALID_TUITION_TIME_BOB = "Tuesday, 1000-1200";
+ public static final String VALID_TAG_MATH = "Math";
+ public static final String VALID_TAG_SCIENCE = "Science";
public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY;
public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB;
@@ -45,8 +48,10 @@ public class CommandTestUtil {
public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB;
public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY;
public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB;
- public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND;
- public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND;
+ public static final String TUITION_TIME_DESC_AMY = " " + PREFIX_TUITION_TIME + VALID_TUITION_TIME_AMY;
+ public static final String TUITION_TIME_DESC_BOB = " " + PREFIX_TUITION_TIME + VALID_TUITION_TIME_BOB;
+ public static final String TAG_DESC_MATH = " " + PREFIX_TAG + VALID_TAG_MATH;
+ public static final String TAG_DESC_SCIENCE = " " + PREFIX_TAG + VALID_TAG_SCIENCE;
public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names
public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones
@@ -63,10 +68,10 @@ public class CommandTestUtil {
static {
DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY)
.withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY)
- .withTags(VALID_TAG_FRIEND).build();
+ .withTags(VALID_TAG_SCIENCE).build();
DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB)
.withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB)
- .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build();
+ .withTags(VALID_TAG_MATH, VALID_TAG_SCIENCE).build();
}
/**
diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
index b6f332eabca..5e265d646d8 100644
--- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java
@@ -46,7 +46,10 @@ public void execute_invalidIndexUnfilteredList_throwsCommandException() {
Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1);
DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex);
- assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX
+ + "Please provide a number between 1 and " + model.getFilteredPersonList().size();
+
+ assertCommandFailure(deleteCommand, model, expectedMessage);
}
@Test
@@ -76,7 +79,11 @@ public void execute_invalidIndexFilteredList_throwsCommandException() {
DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex);
- assertCommandFailure(deleteCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX
+ + "Please provide a number between 1 and " + model.getFilteredPersonList().size();
+
+
+ assertCommandFailure(deleteCommand, model, expectedMessage);
}
@Test
diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java
index 469dd97daa7..5e3964921fc 100644
--- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java
@@ -7,7 +7,7 @@
import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure;
import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex;
@@ -56,10 +56,10 @@ public void execute_someFieldsSpecifiedUnfilteredList_success() {
PersonBuilder personInList = new PersonBuilder(lastPerson);
Person editedPerson = personInList.withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB)
- .withTags(VALID_TAG_HUSBAND).build();
+ .withTags(VALID_TAG_SCIENCE).build();
EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB)
- .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build();
+ .withPhone(VALID_PHONE_BOB).withTags(VALID_TAG_SCIENCE).build();
EditCommand editCommand = new EditCommand(indexLastPerson, descriptor);
String expectedMessage = String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson));
diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java
index b17c1f3d5c2..2bec863ad33 100644
--- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java
+++ b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java
@@ -9,7 +9,8 @@
import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_MATH;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TUITION_TIME_BOB;
import org.junit.jupiter.api.Test;
@@ -53,8 +54,15 @@ public void equals() {
assertFalse(DESC_AMY.equals(editedAmy));
// different tags -> returns false
- editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_HUSBAND).build();
+ editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTags(VALID_TAG_MATH).build();
+ System.out.println("DESC_AMY tags: " + DESC_AMY.getTags());
+ System.out.println("editedAmy tags: " + editedAmy.getTags());
assertFalse(DESC_AMY.equals(editedAmy));
+
+ // different tuition time -> returns false
+ editedAmy = new EditPersonDescriptorBuilder(DESC_AMY).withTuitionTime(VALID_TUITION_TIME_BOB).build();
+ assertFalse(DESC_AMY.equals(editedAmy));
+
}
@Test
@@ -64,7 +72,8 @@ public void toStringMethod() {
+ editPersonDescriptor.getName().orElse(null) + ", phone="
+ editPersonDescriptor.getPhone().orElse(null) + ", email="
+ editPersonDescriptor.getEmail().orElse(null) + ", address="
- + editPersonDescriptor.getAddress().orElse(null) + ", tags="
+ + editPersonDescriptor.getAddress().orElse(null) + ", tuitionTime="
+ + editPersonDescriptor.getTuitionTime().orElse(null) + ", tags="
+ editPersonDescriptor.getTags().orElse(null) + "}";
assertEquals(expected, editPersonDescriptor.toString());
}
diff --git a/src/test/java/seedu/address/logic/commands/ListCommandTest.java b/src/test/java/seedu/address/logic/commands/ListCommandTest.java
index 435ff1f7275..48753e6dee2 100644
--- a/src/test/java/seedu/address/logic/commands/ListCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/ListCommandTest.java
@@ -5,12 +5,17 @@
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;
import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
+import java.util.function.Predicate;
+
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.TagsContainKeywordPredicate;
+import seedu.address.model.person.TuitionTimeContainsKeywordPredicate;
/**
* Contains integration tests (interaction with the Model) and unit tests for ListCommand.
@@ -36,4 +41,33 @@ public void execute_listIsFiltered_showsEverything() {
showPersonAtIndex(model, INDEX_FIRST_PERSON);
assertCommandSuccess(new ListCommand(), model, ListCommand.MESSAGE_SUCCESS, expectedModel);
}
+
+ @Test
+ public void execute_filterByTuitionTime_showsMatchingPersons() {
+ String keyword = "Monday";
+ Predicate predicate = new TuitionTimeContainsKeywordPredicate(keyword);
+ ListCommand command = new ListCommand(predicate, "Showing persons with tuition time on: ", keyword);
+
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, "Showing persons with tuition time on: " + keyword, expectedModel);
+ }
+
+ @Test
+ public void execute_filterByTag_showsMatchingPersons() {
+ String keyword = "student";
+ Predicate predicate = new TagsContainKeywordPredicate(keyword);
+ ListCommand command = new ListCommand(predicate, "Showing persons with tag: ", keyword);
+
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, "No student found.", expectedModel);
+ }
+
+ @Test
+ public void execute_validPredicate_triggersAssertions() throws Exception {
+ Predicate predicate = person -> true; // Matches all
+ ListCommand command = new ListCommand(predicate, "Showing everyone", "");
+
+ expectedModel.updateFilteredPersonList(predicate);
+ assertCommandSuccess(command, model, "Showing everyone", expectedModel);
+ }
}
diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java
new file mode 100644
index 00000000000..81af0934956
--- /dev/null
+++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java
@@ -0,0 +1,278 @@
+package seedu.address.logic.commands;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+
+import org.junit.jupiter.api.Test;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.person.Person;
+import seedu.address.testutil.PersonBuilder;
+
+public class SortCommandTest {
+
+ @Test
+ public void execute_validField_doesNotThrow() {
+ Model model = new SimpleModelStub();
+ SortCommand command = new SortCommand("name", true);
+
+ assertDoesNotThrow(() -> command.execute(model));
+ }
+
+ @Test
+ public void execute_invalidField_throwsCommandException() {
+ Model model = new SimpleModelStub();
+ SortCommand command = new SortCommand("banana", true);
+
+ assertThrows(CommandException.class, () -> command.execute(model));
+ }
+
+ @Test
+ public void execute_nameFieldAscending_sortsCorrectly() throws Exception {
+ Person alice = new PersonBuilder().withName("Alice").build();
+ Person bob = new PersonBuilder().withName("Bob").build();
+ Person charlie = new PersonBuilder().withName("Charlie").build();
+
+ ObservableList list = FXCollections.observableArrayList(charlie, bob, alice);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("name", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(alice, bob, charlie);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_nameFieldDescending_sortsCorrectly() throws Exception {
+ Person alice = new PersonBuilder().withName("Alice").build();
+ Person bob = new PersonBuilder().withName("Bob").build();
+ Person charlie = new PersonBuilder().withName("Charlie").build();
+
+ ObservableList list = FXCollections.observableArrayList(alice, bob, charlie);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("name", false);
+ command.execute(model);
+
+ List expected = Arrays.asList(charlie, bob, alice);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_addressFieldAscending_sortsCorrectly() throws Exception {
+ Person alice = new PersonBuilder().withAddress("Admiralty").build();
+ Person bob = new PersonBuilder().withAddress("Bishan").build();
+ Person charlie = new PersonBuilder().withAddress("Tampines").build();
+
+ ObservableList list = FXCollections.observableArrayList(alice, bob, charlie);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("address", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(alice, bob, charlie);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_phoneFieldAscending_sortsCorrectly() throws Exception {
+ Person alice = new PersonBuilder().withName("Alice").withPhone("91234567").build();
+ Person bob = new PersonBuilder().withName("Bob").withPhone("92234567").build();
+ Person charlie = new PersonBuilder().withName("Charlie").withPhone("93234567").build();
+
+ ObservableList list = FXCollections.observableArrayList(charlie, bob, alice);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("phone", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(alice, bob, charlie);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_emailFieldAscending_sortsCorrectly() throws Exception {
+ Person alice = new PersonBuilder().withName("Alice").withEmail("a@example.com").build();
+ Person bob = new PersonBuilder().withName("Bob").withEmail("b@example.com").build();
+ Person charlie = new PersonBuilder().withName("Charlie").withEmail("c@example.com").build();
+
+ ObservableList list = FXCollections.observableArrayList(charlie, bob, alice);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("email", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(alice, bob, charlie);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_tuitionFieldAscending_sortsCorrectly() throws Exception {
+ Person monday = new PersonBuilder().withName("Monday").withTuitionTime("Monday, 0900-1100").build();
+ Person friday = new PersonBuilder().withName("Friday").withTuitionTime("Friday, 1200-1300").build();
+ Person saturday = new PersonBuilder().withName("Saturday").withTuitionTime("Saturday, 1200-1300").build();
+ Person sunday = new PersonBuilder().withName("Sunday").withTuitionTime("Sunday, 1400-1500").build();
+
+ ObservableList list = FXCollections.observableArrayList(sunday, friday, monday, saturday);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("tuition", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(monday, friday, saturday, sunday);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_tagsFieldAscending_sortsCorrectly() throws Exception {
+ Person oneTag = new PersonBuilder().withName("One").withTags("Math").build();
+ Person twoTags = new PersonBuilder().withName("Two").withTags("Science", "Math").build();
+ Person threeTags = new PersonBuilder().withName("Three").withTags("Math", "English", "Science").build();
+
+ // initial order: One, Two, Three
+ ObservableList list = FXCollections.observableArrayList(oneTag, twoTags, threeTags);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("tags", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(oneTag, twoTags, threeTags);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_tagsFieldDescending_sortsCorrectly() throws Exception {
+ Person oneTag = new PersonBuilder().withName("One").withTags("Math").build();
+ Person twoTags = new PersonBuilder().withName("Two").withTags("Science", "Math").build();
+ Person threeTags = new PersonBuilder().withName("Three").withTags("Math", "English", "Science").build();
+
+ ObservableList list = FXCollections.observableArrayList(threeTags, twoTags, oneTag);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("tags", false);
+ command.execute(model);
+
+ List expected = Arrays.asList(threeTags, twoTags, oneTag);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void execute_personWithNoTags_sortsCorrectly() throws Exception {
+ Person noTags = new PersonBuilder().withName("Tagless").withTags().build();
+ Person oneTag = new PersonBuilder().withName("Tagged").withTags("Math").build();
+
+ ObservableList list = FXCollections.observableArrayList(noTags, oneTag);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("tags", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(noTags, oneTag);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+ @Test
+ public void getSortedTags_emptyTags_returnsEmptyString() throws Exception {
+ Person p = new PersonBuilder().withName("Empty").withTags().build();
+ ObservableList list = FXCollections.observableArrayList(p);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ new SortCommand("tags", true).execute(model);
+ assertEquals(List.of(p), model.getFilteredPersonList()); // still sorted, nothing to compare
+ }
+
+ @Test
+ public void getSortedTags_singleTag_returnsTagLowercased() throws Exception {
+ Person p = new PersonBuilder().withName("Solo").withTags("Math").build();
+ ObservableList list = FXCollections.observableArrayList(p);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ new SortCommand("tags", true).execute(model);
+ assertEquals(List.of(p), model.getFilteredPersonList()); // No reordering needed
+ }
+
+ @Test
+ public void getSortedTags_multipleSortedTags_returnsSameOrder() throws Exception {
+ Person p = new PersonBuilder().withName("Sorted").withTags("English", "Math", "Science").build();
+ ObservableList list = FXCollections.observableArrayList(p);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ new SortCommand("tags", true).execute(model);
+ assertEquals(List.of(p), model.getFilteredPersonList()); // still sorted, but logic ran
+ }
+
+ @Test
+ public void execute_tagsSameCountDifferentOrder_triggersTagStringComparator() throws Exception {
+ Person alphaFirst = new PersonBuilder().withName("A").withTags("Alpha", "Beta").build();
+ Person betaFirst = new PersonBuilder().withName("B").withTags("Charlie", "Alpha").build();
+
+ ObservableList list = FXCollections.observableArrayList(betaFirst, alphaFirst);
+ SimpleModelStub model = new SimpleModelStub(list);
+
+ SortCommand command = new SortCommand("tags", true);
+ command.execute(model);
+
+ List expected = Arrays.asList(alphaFirst, betaFirst);
+ assertEquals(expected, model.getFilteredPersonList());
+ }
+
+
+ private static class SimpleModelStub implements Model {
+ private final ObservableList list;
+
+ SimpleModelStub() {
+ this.list = FXCollections.observableArrayList();
+ }
+
+ SimpleModelStub(ObservableList list) {
+ this.list = list;
+ }
+
+ @Override
+ public ObservableList getFilteredPersonList() {
+ return list;
+ }
+
+ @Override
+ public void sortFilteredPersonList(Comparator comparator) {
+ FXCollections.sort(list, comparator);
+ }
+
+ @Override
+ public void updateFilteredPersonList(Predicate predicate) {}
+
+ // Unused methods
+ @Override public void setUserPrefs(seedu.address.model.ReadOnlyUserPrefs userPrefs) {}
+ @Override public seedu.address.model.ReadOnlyUserPrefs getUserPrefs() {
+ return null;
+ }
+ @Override public seedu.address.commons.core.GuiSettings getGuiSettings() {
+ return null;
+ }
+ @Override public void setGuiSettings(seedu.address.commons.core.GuiSettings guiSettings) {}
+ @Override public java.nio.file.Path getAddressBookFilePath() {
+ return null;
+ }
+ @Override public void setAddressBookFilePath(java.nio.file.Path path) {}
+ @Override public void setAddressBook(seedu.address.model.ReadOnlyAddressBook ab) {}
+ @Override public seedu.address.model.ReadOnlyAddressBook getAddressBook() {
+ return null;
+ }
+ @Override public boolean hasPerson(Person person) {
+ return false;
+ }
+ @Override public void deletePerson(Person person) {}
+ @Override public void addPerson(Person person) {}
+ @Override public void setPerson(Person target, Person editedPerson) {}
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java
index 5bc11d3cdaa..bbede79375a 100644
--- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java
@@ -16,14 +16,18 @@
import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB;
import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_NON_EMPTY;
import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE;
-import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND;
-import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_MATH;
+import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_SCIENCE;
+import static seedu.address.logic.commands.CommandTestUtil.TUITION_TIME_DESC_AMY;
+import static seedu.address.logic.commands.CommandTestUtil.TUITION_TIME_DESC_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_MATH;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TUITION_TIME_AMY;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TUITION_TIME_BOB;
import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
@@ -50,25 +54,30 @@ public class AddCommandParserTest {
@Test
public void parse_allFieldsPresent_success() {
- Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND).build();
+ Person expectedPerson = new PersonBuilder(BOB)
+ .withTuitionTime(VALID_TUITION_TIME_BOB)
+ .withTags(VALID_TAG_MATH)
+ .build();
// whitespace only preamble
assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB
- + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson));
+ + ADDRESS_DESC_BOB + TUITION_TIME_DESC_BOB + TAG_DESC_MATH, new AddCommand(expectedPerson));
// multiple tags - all accepted
- Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND)
+ Person expectedPersonMultipleTags = new PersonBuilder(BOB)
+ .withTags(VALID_TAG_SCIENCE, VALID_TAG_MATH)
+ .withTuitionTime(VALID_TUITION_TIME_BOB)
.build();
- assertParseSuccess(parser,
- NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND,
+ assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
+ + TUITION_TIME_DESC_BOB + TAG_DESC_SCIENCE + TAG_DESC_MATH,
new AddCommand(expectedPersonMultipleTags));
}
@Test
public void parse_repeatedNonTagValue_failure() {
String validExpectedPersonString = NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB
- + ADDRESS_DESC_BOB + TAG_DESC_FRIEND;
+ + ADDRESS_DESC_BOB + TUITION_TIME_DESC_BOB + TAG_DESC_MATH;
// multiple names
assertParseFailure(parser, NAME_DESC_AMY + validExpectedPersonString,
@@ -87,9 +96,8 @@ public void parse_repeatedNonTagValue_failure() {
Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS));
// multiple fields repeated
- assertParseFailure(parser,
- validExpectedPersonString + PHONE_DESC_AMY + EMAIL_DESC_AMY + NAME_DESC_AMY + ADDRESS_DESC_AMY
- + validExpectedPersonString,
+ assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
+ + TUITION_TIME_DESC_BOB + PHONE_DESC_AMY + EMAIL_DESC_AMY + NAME_DESC_AMY + ADDRESS_DESC_AMY,
Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, PREFIX_EMAIL, PREFIX_PHONE));
// invalid value followed by valid value
@@ -132,9 +140,11 @@ public void parse_repeatedNonTagValue_failure() {
@Test
public void parse_optionalFieldsMissing_success() {
// zero tags
- Person expectedPerson = new PersonBuilder(AMY).withTags().build();
- assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY,
- new AddCommand(expectedPerson));
+ Person expectedPerson = new PersonBuilder(AMY)
+ .withTuitionTime(VALID_TUITION_TIME_AMY)
+ .withTags().build();
+ assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY
+ + TUITION_TIME_DESC_AMY, new AddCommand(expectedPerson));
}
@Test
@@ -142,55 +152,59 @@ public void parse_compulsoryFieldMissing_failure() {
String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE);
// missing name prefix
- assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB,
- expectedMessage);
+ assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
+ + TUITION_TIME_DESC_BOB, expectedMessage);
// missing phone prefix
- assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB,
- expectedMessage);
+ assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
+ + TUITION_TIME_DESC_BOB, expectedMessage);
// missing email prefix
- assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB,
- expectedMessage);
+ assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB
+ + TUITION_TIME_DESC_BOB, expectedMessage);
// missing address prefix
- assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB,
- expectedMessage);
+ assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB
+ + TUITION_TIME_DESC_BOB, expectedMessage);
+
+ // missing tuition time prefix
+ assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
+ + VALID_TUITION_TIME_BOB, expectedMessage);
// all prefixes missing
- assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB,
- expectedMessage);
+ assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB
+ + VALID_TUITION_TIME_BOB, expectedMessage);
}
@Test
public void parse_invalidValue_failure() {
// invalid name
assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
- + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS);
+ + TUITION_TIME_DESC_BOB + TAG_DESC_SCIENCE + TAG_DESC_MATH, Name.MESSAGE_CONSTRAINTS);
// invalid phone
assertParseFailure(parser, NAME_DESC_BOB + INVALID_PHONE_DESC + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
- + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS);
+ + TUITION_TIME_DESC_BOB + TAG_DESC_SCIENCE + TAG_DESC_MATH, Phone.MESSAGE_CONSTRAINTS);
// invalid email
assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + INVALID_EMAIL_DESC + ADDRESS_DESC_BOB
- + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS);
+ + TUITION_TIME_DESC_BOB + TAG_DESC_SCIENCE + TAG_DESC_MATH, Email.MESSAGE_CONSTRAINTS);
// invalid address
assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC
- + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS);
+ + TUITION_TIME_DESC_BOB + TAG_DESC_SCIENCE + TAG_DESC_MATH, Address.MESSAGE_CONSTRAINTS);
// invalid tag
assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB
- + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS);
+ + TUITION_TIME_DESC_BOB + INVALID_TAG_DESC + VALID_TAG_SCIENCE, Tag.MESSAGE_CONSTRAINTS);
// two invalid values, only first invalid value reported
- assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC,
- Name.MESSAGE_CONSTRAINTS);
+ assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC
+ + TUITION_TIME_DESC_BOB, Name.MESSAGE_CONSTRAINTS);
// non-empty preamble
assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB
- + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND,
+ + ADDRESS_DESC_BOB + TUITION_TIME_DESC_BOB + TAG_DESC_SCIENCE + TAG_DESC_MATH,
String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE));
}
}
diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
index 5a1ab3dbc0c..7e518f04d21 100644
--- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java
@@ -85,7 +85,8 @@ public void parseCommand_help() throws Exception {
@Test
public void parseCommand_list() throws Exception {
assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand);
- assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand);
+ assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " t/math") instanceof ListCommand);
+ assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " tt/monday") instanceof ListCommand);
}
@Test
diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java
index cc7175172d4..fc384704df0 100644
--- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java
@@ -13,15 +13,15 @@
import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY;
import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY;
import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND;
-import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_MATH;
+import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_SCIENCE;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_MATH;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
@@ -94,24 +94,24 @@ public void parse_invalidValue_failure() {
// while parsing {@code PREFIX_TAG} alone will reset the tags of the {@code Person} being edited,
// parsing it together with a valid tag results in error
- assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS);
- assertParseFailure(parser, "1" + TAG_DESC_FRIEND + TAG_EMPTY + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS);
- assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS);
+ assertParseFailure(parser, "1" + TAG_DESC_MATH + TAG_DESC_SCIENCE + TAG_EMPTY, Tag.MESSAGE_CONSTRAINTS);
+ assertParseFailure(parser, "1" + TAG_DESC_MATH + TAG_EMPTY + TAG_DESC_SCIENCE, Tag.MESSAGE_CONSTRAINTS);
+ assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_MATH + TAG_DESC_SCIENCE, Tag.MESSAGE_CONSTRAINTS);
// multiple invalid values, but only the first invalid value is captured
- assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY,
- Name.MESSAGE_CONSTRAINTS);
+ assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY
+ + VALID_PHONE_AMY, Name.MESSAGE_CONSTRAINTS);
}
@Test
public void parse_allFieldsSpecified_success() {
Index targetIndex = INDEX_SECOND_PERSON;
- String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND
- + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND;
+ String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_SCIENCE
+ + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_MATH;
EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY)
.withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY)
- .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build();
+ .withTags(VALID_TAG_SCIENCE, VALID_TAG_MATH).build();
EditCommand expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
@@ -157,8 +157,8 @@ public void parse_oneFieldSpecified_success() {
assertParseSuccess(parser, userInput, expectedCommand);
// tags
- userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND;
- descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build();
+ userInput = targetIndex.getOneBased() + TAG_DESC_MATH;
+ descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_MATH).build();
expectedCommand = new EditCommand(targetIndex, descriptor);
assertParseSuccess(parser, userInput, expectedCommand);
}
@@ -181,8 +181,8 @@ public void parse_multipleRepeatedFields_failure() {
// mulltiple valid fields repeated
userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY
- + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND
- + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND;
+ + TAG_DESC_MATH + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_MATH
+ + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_SCIENCE;
assertParseFailure(parser, userInput,
Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS));
diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java
index d92e64d12f9..3fc0d8ff06b 100644
--- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java
@@ -20,6 +20,12 @@ public void parse_emptyArg_throwsParseException() {
assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}
+ @Test
+ public void parse_withFieldKeyword_throwsParseException() {
+ assertParseFailure(parser, " n/Alice", String.format(
+ "Field keyword n/ is not required. \n%1$s", FindCommand.MESSAGE_USAGE));
+ }
+
@Test
public void parse_validArgs_returnsFindCommand() {
// no leading and trailing whitespaces
diff --git a/src/test/java/seedu/address/logic/parser/ListCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ListCommandParserTest.java
new file mode 100644
index 00000000000..57763594b2c
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/ListCommandParserTest.java
@@ -0,0 +1,85 @@
+package seedu.address.logic.parser;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_TT_FORMAT;
+import static seedu.address.logic.Messages.MESSAGE_MISSING_KEYWORD;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.ListCommand;
+import seedu.address.model.person.TagsContainKeywordPredicate;
+import seedu.address.model.person.TuitionTimeContainsKeywordPredicate;
+
+public class ListCommandParserTest {
+
+ private static final String INVALID_TT_FORMAT =
+ "Invalid day entered. Please use a valid day like 'monday' or 'mon' (case-insensitive).";
+ private final ListCommandParser parser = new ListCommandParser();
+
+ @Test
+ public void parse_emptyArg_returnsListAllCommand() {
+ assertParseSuccess(parser, " ", new ListCommand());
+ }
+
+ @Test
+ public void parse_validTagArg_returnsTagFilteredListCommand() {
+ String input = " t/math";
+ ListCommand expectedCommand = new ListCommand(
+ new TagsContainKeywordPredicate("math"),
+ "Listed all persons with subject ",
+ "math"
+ );
+ assertParseSuccess(parser, input, expectedCommand);
+ }
+
+ @Test
+ public void parse_validTuitionTime_returnsTuitionTimeFilteredListCommand() {
+ String input = " tt/Monday";
+ ListCommand expectedCommand = new ListCommand(
+ new TuitionTimeContainsKeywordPredicate("Monday"),
+ "Listed all persons with tuition time on ",
+ "Monday"
+ );
+ assertParseSuccess(parser, input, expectedCommand);
+ }
+
+ @Test
+ public void parse_validFullTuitionTime_noThrow() {
+ String input = " tt/Monday, 1000-1200";
+ assertDoesNotThrow(() -> parser.parse(input));
+ }
+
+ @Test
+ public void parse_validTuitionTimeAndTag_noThrow() {
+ String input = " tt/Monday t/Math";
+ assertDoesNotThrow(() -> parser.parse(input));
+ }
+
+ @Test
+ public void parse_invalidTuitionTimeArg_throwsParseException() {
+ String input = " tt/Funday";
+ String expectedMessage = String.format(MESSAGE_INVALID_TT_FORMAT, ListCommand.MESSAGE_USAGE);
+ assertParseFailure(parser, input, expectedMessage);
+ }
+
+ @Test
+ public void parse_missingFieldPrefix_throwsParseException() {
+ String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE);
+ assertParseFailure(parser, " math", expectedMessage);
+ }
+
+ @Test
+ public void parse_emptyTagField_throwsParseException() {
+ String expectedMessage = String.format(MESSAGE_MISSING_KEYWORD, ListCommand.MESSAGE_USAGE);
+ assertParseFailure(parser, " t/ ", expectedMessage);
+ }
+
+ @Test
+ public void parse_emptyTuitionTimeField_throwsParseException() {
+ String expectedMessage = String.format(MESSAGE_MISSING_KEYWORD, ListCommand.MESSAGE_USAGE);
+ assertParseFailure(parser, " tt/ ", expectedMessage);
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
index 4256788b1a7..541c266d3c5 100644
--- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
+++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
@@ -28,7 +28,7 @@ public class ParserUtilTest {
private static final String INVALID_TAG = "#friend";
private static final String VALID_NAME = "Rachel Walker";
- private static final String VALID_PHONE = "123456";
+ private static final String VALID_PHONE = "91234567";
private static final String VALID_ADDRESS = "123 Main Street #0505";
private static final String VALID_EMAIL = "rachel@example.com";
private static final String VALID_TAG_1 = "friend";
diff --git a/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java
new file mode 100644
index 00000000000..595ac525f2e
--- /dev/null
+++ b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java
@@ -0,0 +1,70 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure;
+import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.logic.commands.SortCommand;
+
+public class SortCommandParserTest {
+
+ private final SortCommandParser parser = new SortCommandParser();
+
+ @Test
+ public void parse_validInputNameAsc_returnsSortCommand() throws Exception {
+ SortCommand expected = new SortCommand("name", true);
+ assertParseSuccess(parser, "name asc", expected);
+ }
+
+
+ @Test
+ public void parse_validInputEmailDesc_returnsSortCommand() throws Exception {
+ SortCommand expected = new SortCommand("email", false);
+ assertParseSuccess(parser, "email desc", expected);
+ }
+
+ @Test
+ public void parse_validInputWithExtraSpaces_returnsSortCommand() throws Exception {
+ SortCommand expected = new SortCommand("address", true);
+ assertParseSuccess(parser, " address asc ", expected);
+ }
+
+ @Test
+ public void parse_missingField_throwsParseException() {
+ assertParseFailure(parser, " ", "Invalid sort field. Valid fields: name, phone, "
+ + "email, address, tuition, tag");
+ }
+
+ @Test
+ public void parse_missingDirection_throwsParseException() {
+ assertParseSuccess(parser, "name", new SortCommand("name", true));
+ }
+
+ @Test
+ public void parse_invalidDirection_throwsParseException() {
+ assertParseFailure(parser, "name upwards", "Invalid sort order. Use 'asc' or 'desc'.");
+ }
+
+ @Test
+ public void parse_caseInsensitiveInput_returnsSortCommand() throws Exception {
+ SortCommand expected = new SortCommand("email", false);
+ assertParseSuccess(parser, "By EMAIL Desc", expected);
+ }
+
+ @Test
+ public void parse_validInputWithBy_returnsSortCommand() throws Exception {
+ SortCommand expected = new SortCommand("phone", true);
+ assertParseSuccess(parser, "by phone", expected);
+ }
+
+ @Test
+ public void parse_invalidDirectionWithBy_throwsParseException() {
+ assertParseFailure(parser, "by name up", "Invalid sort order. Use 'asc' or 'desc'.");
+ }
+
+ @Test
+ public void parse_invalidFieldFormat_throwsParseException() {
+ assertParseFailure(parser, "sort name descending fast", "Invalid sort format. Try: sort by name desc");
+ }
+}
diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java
index 68c8c5ba4d5..adac3bf820e 100644
--- a/src/test/java/seedu/address/model/AddressBookTest.java
+++ b/src/test/java/seedu/address/model/AddressBookTest.java
@@ -4,7 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalPersons.ALICE;
import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;
@@ -46,7 +46,7 @@ public void resetData_withValidReadOnlyAddressBook_replacesData() {
@Test
public void resetData_withDuplicatePersons_throwsDuplicatePersonException() {
// Two persons with the same identity fields
- Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND)
+ Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_SCIENCE)
.build();
List newPersons = Arrays.asList(ALICE, editedAlice);
AddressBookStub newData = new AddressBookStub(newPersons);
@@ -73,7 +73,7 @@ public void hasPerson_personInAddressBook_returnsTrue() {
@Test
public void hasPerson_personWithSameIdentityFieldsInAddressBook_returnsTrue() {
addressBook.addPerson(ALICE);
- Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND)
+ Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_SCIENCE)
.build();
assertTrue(addressBook.hasPerson(editedAlice));
}
diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/person/EmailTest.java
index f08cdff0a64..5ade03ba1b1 100644
--- a/src/test/java/seedu/address/model/person/EmailTest.java
+++ b/src/test/java/seedu/address/model/person/EmailTest.java
@@ -51,27 +51,42 @@ public void isValidEmail() {
assertFalse(Email.isValidEmail("peterjack@-example.com")); // domain name starts with a hyphen
assertFalse(Email.isValidEmail("peterjack@example.com-")); // domain name ends with a hyphen
assertFalse(Email.isValidEmail("peterjack@example.c")); // top level domain has less than two chars
+ assertFalse(Email.isValidEmail("a@bc")); // minimal
+ assertFalse(Email.isValidEmail("test@localhost")); // alphabets only
+ assertFalse(Email.isValidEmail("123@145")); // numeric local part and domain name
// valid email
assertTrue(Email.isValidEmail("PeterJack_1190@example.com")); // underscore in local part
assertTrue(Email.isValidEmail("PeterJack.1190@example.com")); // period in local part
assertTrue(Email.isValidEmail("PeterJack+1190@example.com")); // '+' symbol in local part
assertTrue(Email.isValidEmail("PeterJack-1190@example.com")); // hyphen in local part
- assertTrue(Email.isValidEmail("a@bc")); // minimal
- assertTrue(Email.isValidEmail("test@localhost")); // alphabets only
- assertTrue(Email.isValidEmail("123@145")); // numeric local part and domain name
assertTrue(Email.isValidEmail("a1+be.d@example1.com")); // mixture of alphanumeric and special characters
assertTrue(Email.isValidEmail("peter_jack@very-very-very-long-example.com")); // long domain name
assertTrue(Email.isValidEmail("if.you.dream.it_you.can.do.it@example.com")); // long local part
assertTrue(Email.isValidEmail("e1234567@u.nus.edu")); // more than one period in domain
}
+ @Test
+ public void emailLengthChecks() {
+ // 1. local part > 64
+ String localTooLong = "a".repeat(65) + "@domain.com";
+ assertThrows(IllegalArgumentException.class, () -> new Email(localTooLong));
+
+ // 2. domain part > 255
+ String domainTooLong = "local@" + "b".repeat(256);
+ assertThrows(IllegalArgumentException.class, () -> new Email(domainTooLong));
+
+ // 3. total > 320
+ String totalTooLong = "a".repeat(320) + "@x"; // Actually 321+ chars if you do the math
+ assertThrows(IllegalArgumentException.class, () -> new Email(totalTooLong));
+ }
+
@Test
public void equals() {
- Email email = new Email("valid@email");
+ Email email = new Email("valid@email.com");
// same values -> returns true
- assertTrue(email.equals(new Email("valid@email")));
+ assertTrue(email.equals(new Email("valid@email.com")));
// same object -> returns true
assertTrue(email.equals(email));
@@ -83,6 +98,6 @@ public void equals() {
assertFalse(email.equals(5.0f));
// different values -> returns false
- assertFalse(email.equals(new Email("other.valid@email")));
+ assertFalse(email.equals(new Email("other@email.com")));
}
}
diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java
index 6b3fd90ade7..c39cb99e746 100644
--- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java
+++ b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java
@@ -70,7 +70,7 @@ public void test_nameDoesNotContainKeywords_returnsFalse() {
// Keywords match phone, email and address, but does not match name
predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street"));
- assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345")
+ assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("91234567")
.withEmail("alice@email.com").withAddress("Main Street").build()));
}
diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/person/NameTest.java
index 94e3dd726bd..49943064765 100644
--- a/src/test/java/seedu/address/model/person/NameTest.java
+++ b/src/test/java/seedu/address/model/person/NameTest.java
@@ -19,25 +19,42 @@ public void constructor_invalidName_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> new Name(invalidName));
}
+ @Test
+ public void constructor_tooLongName_throwsIllegalArgumentException() {
+ // 81 characters
+ String longName = "a".repeat(81);
+ assertThrows(IllegalArgumentException.class, () -> new Name(longName));
+ }
+
@Test
public void isValidName() {
// null name
assertThrows(NullPointerException.class, () -> Name.isValidName(null));
- // invalid name
+ // invalid names
assertFalse(Name.isValidName("")); // empty string
assertFalse(Name.isValidName(" ")); // spaces only
- assertFalse(Name.isValidName("^")); // only non-alphanumeric characters
- assertFalse(Name.isValidName("peter*")); // contains non-alphanumeric characters
+ assertFalse(Name.isValidName("^")); // only symbols not allowed
+ assertFalse(Name.isValidName("peter*")); // contains invalid symbol '*'
+ assertFalse(Name.isValidName("🚀 Rocket")); // emoji not allowed
+ assertFalse(Name.isValidName("12345")); //numbers only not allowed
- // valid name
+ // valid names (Unicode and culturally relevant)
assertTrue(Name.isValidName("peter jack")); // alphabets only
- assertTrue(Name.isValidName("12345")); // numbers only
- assertTrue(Name.isValidName("peter the 2nd")); // alphanumeric characters
- assertTrue(Name.isValidName("Capital Tan")); // with capital letters
+ assertTrue(Name.isValidName("peter the 2nd")); // alphanumeric
+ assertTrue(Name.isValidName("Capital Tan")); // with capitals
assertTrue(Name.isValidName("David Roger Jackson Ray Jr 2nd")); // long names
+ assertTrue(Name.isValidName("O'Connor")); // apostrophe
+ assertTrue(Name.isValidName("Nguyễn Văn A")); // Vietnamese
+ assertTrue(Name.isValidName("张伟")); // Chinese characters
+ assertTrue(Name.isValidName("Jean-Luc Picard")); // hyphenated
+ assertTrue(Name.isValidName("Vignesh S/O Muniyandi")); // with slashes
+ assertTrue(Name.isValidName("Mary J. Blige")); // with dot
+ assertTrue(Name.isValidName("Dr. Tan")); // title with dot
+ assertTrue(Name.isValidName("A. B. C.")); // initials
}
+
@Test
public void equals() {
Name name = new Name("Valid Name");
diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java
index 31a10d156c9..9d232bdde49 100644
--- a/src/test/java/seedu/address/model/person/PersonTest.java
+++ b/src/test/java/seedu/address/model/person/PersonTest.java
@@ -7,7 +7,8 @@
import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TUITION_TIME_BOB;
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalPersons.ALICE;
import static seedu.address.testutil.TypicalPersons.BOB;
@@ -32,10 +33,12 @@ public void isSamePerson() {
// null -> returns false
assertFalse(ALICE.isSamePerson(null));
- // same name, all other attributes different -> returns true
+ // same name, all other attributes different -> returns false as people can have same name but different number
+ // phone number can uniquely identify a person
Person editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB)
- .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build();
- assertTrue(ALICE.isSamePerson(editedAlice));
+ .withAddress(VALID_ADDRESS_BOB).withTuitionTime(VALID_TUITION_TIME_BOB)
+ .withTags(VALID_TAG_SCIENCE).build();
+ assertFalse(ALICE.isSamePerson(editedAlice));
// different name, all other attributes same -> returns false
editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build();
@@ -85,15 +88,20 @@ public void equals() {
editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build();
assertFalse(ALICE.equals(editedAlice));
+ // different tuition time -> returns false
+ editedAlice = new PersonBuilder(ALICE).withTuitionTime(VALID_TUITION_TIME_BOB).build();
+ assertFalse(ALICE.equals(editedAlice));
+
// different tags -> returns false
- editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build();
+ editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_SCIENCE).build();
assertFalse(ALICE.equals(editedAlice));
}
@Test
public void toStringMethod() {
String expected = Person.class.getCanonicalName() + "{name=" + ALICE.getName() + ", phone=" + ALICE.getPhone()
- + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags() + "}";
+ + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tuitionTime="
+ + ALICE.getTuitionTime() + ", tags=" + ALICE.getTags() + "}";
assertEquals(expected, ALICE.toString());
}
}
diff --git a/src/test/java/seedu/address/model/person/PhoneTest.java b/src/test/java/seedu/address/model/person/PhoneTest.java
index deaaa5ba190..f10a57e6fdf 100644
--- a/src/test/java/seedu/address/model/person/PhoneTest.java
+++ b/src/test/java/seedu/address/model/person/PhoneTest.java
@@ -1,5 +1,6 @@
package seedu.address.model.person;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.testutil.Assert.assertThrows;
@@ -19,31 +20,51 @@ public void constructor_invalidPhone_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () -> new Phone(invalidPhone));
}
+ @Test
+ public void constructor_standardizesValidPhone() {
+ Phone phone = new Phone("+65 9123-4567");
+ assertEquals("+65 9123 4567", phone.value);
+ }
+
+ @Test
+ public void constructor_rejectsInvalidPhone() {
+ assertThrows(IllegalArgumentException.class, () -> new Phone("abc-123"));
+ }
+
+ @Test
+ public void standardizePhone_invalidNumber_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> Phone.standardizePhone("+++++"));
+ }
+
@Test
public void isValidPhone() {
// null phone number
- assertThrows(NullPointerException.class, () -> Phone.isValidPhone(null));
+ assertFalse(Phone.isValidPhone(null)); // null
// invalid phone numbers
assertFalse(Phone.isValidPhone("")); // empty string
assertFalse(Phone.isValidPhone(" ")); // spaces only
- assertFalse(Phone.isValidPhone("91")); // less than 3 numbers
+ assertFalse(Phone.isValidPhone("91")); // too short (even for SG)
assertFalse(Phone.isValidPhone("phone")); // non-numeric
- assertFalse(Phone.isValidPhone("9011p041")); // alphabets within digits
- assertFalse(Phone.isValidPhone("9312 1534")); // spaces within digits
+ assertFalse(Phone.isValidPhone("9381e9173")); // alphabet in digits
+ assertFalse(Phone.isValidPhone("+1 123-4567")); // US number too short (missing area code)
+ assertFalse(Phone.isValidPhone("+999 12345678")); // invalid country code
+ assertFalse(Phone.isValidPhone("9123 4567")); // multiple consecutive spaces
// valid phone numbers
- assertTrue(Phone.isValidPhone("911")); // exactly 3 numbers
- assertTrue(Phone.isValidPhone("93121534"));
- assertTrue(Phone.isValidPhone("124293842033123")); // long phone numbers
+ assertTrue(Phone.isValidPhone("91234567")); // valid SG local
+ assertTrue(Phone.isValidPhone("+6591234567")); // valid SG international
+ assertTrue(Phone.isValidPhone("+14085551234")); // valid US number
+ assertTrue(Phone.isValidPhone("93121534")); // another valid SG number
+ assertTrue(Phone.isValidPhone("+442079460958")); // valid UK number
}
@Test
public void equals() {
- Phone phone = new Phone("999");
+ Phone phone = new Phone("91234567");
// same values -> returns true
- assertTrue(phone.equals(new Phone("999")));
+ assertTrue(phone.equals(new Phone("91234567")));
// same object -> returns true
assertTrue(phone.equals(phone));
@@ -55,6 +76,6 @@ public void equals() {
assertFalse(phone.equals(5.0f));
// different values -> returns false
- assertFalse(phone.equals(new Phone("995")));
+ assertFalse(phone.equals(new Phone("92234567")));
}
}
diff --git a/src/test/java/seedu/address/model/person/TagsContainKeywordPredicateTest.java b/src/test/java/seedu/address/model/person/TagsContainKeywordPredicateTest.java
new file mode 100644
index 00000000000..3d6291f2c69
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/TagsContainKeywordPredicateTest.java
@@ -0,0 +1,76 @@
+package seedu.address.model.person;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.testutil.PersonBuilder;
+
+public class TagsContainKeywordPredicateTest {
+ @Test
+ public void equals() {
+ String firstPredicateKeyword = "first";
+ String secondPredicateKeyword = "second";
+
+ TagsContainKeywordPredicate firstPredicate = new TagsContainKeywordPredicate(firstPredicateKeyword);
+ TagsContainKeywordPredicate secondPredicate = new TagsContainKeywordPredicate(secondPredicateKeyword);
+
+ // same object -> returns true
+ assertTrue(firstPredicate.equals(firstPredicate));
+
+ // same values -> returns true
+ TagsContainKeywordPredicate firstPredicateCopy = new TagsContainKeywordPredicate(firstPredicateKeyword);
+ assertTrue(firstPredicate.equals(firstPredicateCopy));
+
+ // different types -> returns false
+ assertFalse(firstPredicate.equals(1));
+
+ // null -> returns false
+ assertFalse(firstPredicate.equals(null));
+
+ // different keyword -> returns false
+ assertFalse(firstPredicate.equals(secondPredicate));
+ }
+
+ @Test
+ public void test_tagsContainKeyword_returnsTrue() {
+ // One tag
+ TagsContainKeywordPredicate predicate = new TagsContainKeywordPredicate("chemistry");
+ assertTrue(predicate.test(new PersonBuilder().withTags("chemistry").build()));
+
+ // Multiple tags
+ predicate = new TagsContainKeywordPredicate("chemistry");
+ assertTrue(predicate.test(new PersonBuilder().withTags("math", "chemistry").build()));
+
+ // Mixed-case keyword
+ predicate = new TagsContainKeywordPredicate("chEmiStRy");
+ assertTrue(predicate.test(new PersonBuilder().withTags("math", "chemistry").build()));
+
+ // Mixed-case tags
+ predicate = new TagsContainKeywordPredicate("chemistry");
+ assertTrue(predicate.test(new PersonBuilder().withTags("math", "chEmIsTry").build()));
+ }
+
+ @Test
+ public void test_tagsDoesNotContainKeyword_returnsFalse() {
+ // Non-matching keyword
+ TagsContainKeywordPredicate predicate = new TagsContainKeywordPredicate("chemistry");
+ assertFalse(predicate.test(new PersonBuilder().withTags("math", "biology").build()));
+
+ // Keyword match name and address, but does not match name
+ predicate = new TagsContainKeywordPredicate("Math");
+ assertFalse(predicate.test(new PersonBuilder().withName("Alice Main").withPhone("91234567")
+ .withAddress("main").withTags("chemistry", "biology").build()));
+ }
+
+ @Test
+ public void toStringMethod() {
+ String keyword = "chemistry";
+ TagsContainKeywordPredicate predicate = new TagsContainKeywordPredicate(keyword);
+
+ String expected = TagsContainKeywordPredicate.class.getCanonicalName() + "{keyword=" + keyword + "}";
+ assertEquals(expected, predicate.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/model/person/TuitionTimeContainsKeywordPredicateTest.java b/src/test/java/seedu/address/model/person/TuitionTimeContainsKeywordPredicateTest.java
new file mode 100644
index 00000000000..1d6001cea98
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/TuitionTimeContainsKeywordPredicateTest.java
@@ -0,0 +1,72 @@
+package seedu.address.model.person;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.testutil.PersonBuilder;
+
+public class TuitionTimeContainsKeywordPredicateTest {
+
+ @Test
+ public void equals() {
+ String firstKeyword = "monday";
+ String secondKeyword = "friday";
+
+ TuitionTimeContainsKeywordPredicate firstPredicate = new TuitionTimeContainsKeywordPredicate(firstKeyword);
+ TuitionTimeContainsKeywordPredicate secondPredicate = new TuitionTimeContainsKeywordPredicate(secondKeyword);
+
+ // same object -> returns true
+ assertTrue(firstPredicate.equals(firstPredicate));
+
+ // same values -> returns true
+ TuitionTimeContainsKeywordPredicate firstPredicateCopy = new TuitionTimeContainsKeywordPredicate(firstKeyword);
+ assertTrue(firstPredicate.equals(firstPredicateCopy));
+
+ // different types -> returns false
+ assertFalse(firstPredicate.equals(1));
+
+ // null -> returns false
+ assertFalse(firstPredicate.equals(null));
+
+ // different keyword -> returns false
+ assertFalse(firstPredicate.equals(secondPredicate));
+ }
+
+ @Test
+ public void test_tuitionTimeContainsKeyword_returnsTrue() {
+ // Exact match
+ TuitionTimeContainsKeywordPredicate predicate = new TuitionTimeContainsKeywordPredicate("Monday");
+ assertTrue(predicate.test(new PersonBuilder().withTuitionTime("Monday, 1000-1200").build()));
+
+ // Case-insensitive match
+ predicate = new TuitionTimeContainsKeywordPredicate("monday");
+ assertTrue(predicate.test(new PersonBuilder().withTuitionTime("MONDAY, 1000-1200").build()));
+
+ // Partial match not allowed by predicate logic (assuming it checks full day string)
+ predicate = new TuitionTimeContainsKeywordPredicate("mon");
+ assertTrue(predicate.test(new PersonBuilder().withTuitionTime("Monday, 1000-1200").build()));
+ }
+
+ @Test
+ public void test_tuitionTimeDoesNotContainKeyword_returnsFalse() {
+ // Keyword does not match
+ TuitionTimeContainsKeywordPredicate predicate = new TuitionTimeContainsKeywordPredicate("Friday");
+ assertFalse(predicate.test(new PersonBuilder().withTuitionTime("Monday, 1000-1200").build()));
+
+ // Keyword matches part of the time but not the day
+ predicate = new TuitionTimeContainsKeywordPredicate("1000");
+ assertTrue(predicate.test(new PersonBuilder().withTuitionTime("Monday, 1000-1200").build()));
+ }
+
+ @Test
+ public void toStringMethod() {
+ String keyword = "Monday";
+ TuitionTimeContainsKeywordPredicate predicate = new TuitionTimeContainsKeywordPredicate(keyword);
+
+ String expected = TuitionTimeContainsKeywordPredicate.class.getCanonicalName() + "{keyword=" + keyword + "}";
+ assertEquals(expected, predicate.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/model/person/TuitionTimeTest.java b/src/test/java/seedu/address/model/person/TuitionTimeTest.java
new file mode 100644
index 00000000000..259381b7b47
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/TuitionTimeTest.java
@@ -0,0 +1,58 @@
+package seedu.address.model.person;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class TuitionTimeTest {
+
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new TuitionTime(null));
+ }
+
+ @Test
+ public void constructor_invalidTuitionTime_throwsIllegalArgumentException() {
+ String invalidTuitionTime = "";
+ assertThrows(IllegalArgumentException.class, () -> new TuitionTime(invalidTuitionTime));
+ }
+
+ @Test
+ public void isValidTuitionTime() {
+ // null tuitionTime
+ assertThrows(NullPointerException.class, () -> TuitionTime.isValidTuitionTime(null));
+
+ // invalid tuitionTime
+ assertFalse(TuitionTime.isValidTuitionTime("")); // empty string
+ assertFalse(TuitionTime.isValidTuitionTime(" ")); // spaces only
+ assertFalse(TuitionTime.isValidTuitionTime("^")); // only non-alphanumeric characters
+ assertFalse(TuitionTime.isValidTuitionTime("peter*")); // contains non-alphanumeric characters
+ assertFalse(TuitionTime.isValidTuitionTime("Monday, 2100-2500"));
+ assertFalse(TuitionTime.isValidTuitionTime("Monday, 2100-1800"));
+
+ // valid tuitionTime
+ assertTrue(TuitionTime.isValidTuitionTime("Monday, 1800-2100"));
+ }
+
+ @Test
+ public void equals() {
+ TuitionTime tuitionTime = new TuitionTime("Monday, 1800-2100");
+
+ // same values -> returns true
+ assertTrue(tuitionTime.equals(new TuitionTime("Monday, 1800-2100")));
+
+ // same object -> returns true
+ assertTrue(tuitionTime.equals(tuitionTime));
+
+ // null -> returns false
+ assertFalse(tuitionTime.equals(null));
+
+ // different types -> returns false
+ assertFalse(tuitionTime.equals(5.0f));
+
+ // different values -> returns false
+ assertFalse(tuitionTime.equals(new TuitionTime("Tuesday, 1800-2100")));
+ }
+}
diff --git a/src/test/java/seedu/address/model/person/TuitionTimeUtilTest.java b/src/test/java/seedu/address/model/person/TuitionTimeUtilTest.java
new file mode 100644
index 00000000000..d1b1a8dbc2c
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/TuitionTimeUtilTest.java
@@ -0,0 +1,40 @@
+package seedu.address.model.person;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class TuitionTimeUtilTest {
+
+ @Test
+ public void getSortKey_validInput_returnsCorrectSortKey() {
+ assertEquals("1_0900", TuitionTimeUtil.getSortKey("Monday, 0900-1100"));
+ assertEquals("2_1800", TuitionTimeUtil.getSortKey("Tuesday, 1800-2000"));
+ assertEquals("7_2200", TuitionTimeUtil.getSortKey("Sunday, 2200-2300"));
+ assertEquals("3_1000", TuitionTimeUtil.getSortKey("WEDNESDAY, 1000-1200")); // case-insensitive
+ }
+
+ @Test
+ public void getSortKey_invalidFormat_returnsLowercaseOriginal() {
+ assertEquals("invalidinput", TuitionTimeUtil.getSortKey("invalidinput"));
+ assertEquals("", TuitionTimeUtil.getSortKey("")); // edge case: empty input
+ assertEquals("justaday", TuitionTimeUtil.getSortKey("justaday"));
+ }
+
+ @Test
+ public void getSortKey_unknownDay_returnsWithDefaultOrder8() {
+ assertEquals("8_1234", TuitionTimeUtil.getSortKey("Blursday, 1234-5678")); // unknown day
+ assertEquals("8_9999", TuitionTimeUtil.getSortKey("Holiday, 9999-1111")); // another unknown day
+ }
+
+ @Test
+ public void getSortKey_extraSpaces_stillParsesCorrectly() {
+ assertEquals("4_1300", TuitionTimeUtil.getSortKey(" Thursday, 1300-1500 "));
+ }
+
+ @Test
+ public void getSortKey_malformedTimeRange_returnsPartialKey() {
+ assertEquals("5_1400", TuitionTimeUtil.getSortKey("Friday, 1400")); // no dash
+ assertEquals("5_1400", TuitionTimeUtil.getSortKey("Friday, 1400-")); // incomplete range
+ }
+}
diff --git a/src/test/java/seedu/address/model/person/UniquePersonListTest.java b/src/test/java/seedu/address/model/person/UniquePersonListTest.java
index 17ae501df08..e68a859d457 100644
--- a/src/test/java/seedu/address/model/person/UniquePersonListTest.java
+++ b/src/test/java/seedu/address/model/person/UniquePersonListTest.java
@@ -4,7 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalPersons.ALICE;
import static seedu.address.testutil.TypicalPersons.BOB;
@@ -42,7 +42,7 @@ public void contains_personInList_returnsTrue() {
@Test
public void contains_personWithSameIdentityFieldsInList_returnsTrue() {
uniquePersonList.add(ALICE);
- Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND)
+ Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_SCIENCE)
.build();
assertTrue(uniquePersonList.contains(editedAlice));
}
@@ -85,7 +85,7 @@ public void setPerson_editedPersonIsSamePerson_success() {
@Test
public void setPerson_editedPersonHasSameIdentity_success() {
uniquePersonList.add(ALICE);
- Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND)
+ Person editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_SCIENCE)
.build();
uniquePersonList.setPerson(ALICE, editedAlice);
UniquePersonList expectedUniquePersonList = new UniquePersonList();
diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java
index 64d07d79ee2..23cdc960c14 100644
--- a/src/test/java/seedu/address/model/tag/TagTest.java
+++ b/src/test/java/seedu/address/model/tag/TagTest.java
@@ -1,5 +1,7 @@
package seedu.address.model.tag;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static seedu.address.testutil.Assert.assertThrows;
import org.junit.jupiter.api.Test;
@@ -21,6 +23,28 @@ public void constructor_invalidTagName_throwsIllegalArgumentException() {
public void isValidTagName() {
// null tag name
assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null));
+ assertFalse(() -> Tag.isValidTagName("Science!"));
+ // valid
+ assertTrue(Tag.isValidTagName("Computer Science"));
+ }
+
+ @Test
+ public void equalsIgnoreCaseTest() {
+ // exactly equals
+ Tag tag = new Tag("chemistry");
+ assertTrue(tag.equalsIgnoreCase("chemistry"));
+
+ // mixed-case tag
+ tag = new Tag("cHemIsTry");
+ assertTrue(tag.equalsIgnoreCase("chemistry"));
+
+ // mixed-case keyword
+ tag = new Tag("chemistry");
+ assertTrue(tag.equalsIgnoreCase("chEmiSTry"));
+
+ // not equalsTag
+ tag = new Tag("chemistry");
+ assertFalse(tag.equalsIgnoreCase("math"));
}
}
diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java
index 83b11331cdb..38d82eaefb6 100644
--- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java
+++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java
@@ -16,18 +16,21 @@
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
import seedu.address.model.person.Phone;
+import seedu.address.model.person.TuitionTime;
public class JsonAdaptedPersonTest {
private static final String INVALID_NAME = "R@chel";
private static final String INVALID_PHONE = "+651234";
private static final String INVALID_ADDRESS = " ";
private static final String INVALID_EMAIL = "example.com";
+ private static final String INVALID_TUITION_TIME = " ";
private static final String INVALID_TAG = "#friend";
private static final String VALID_NAME = BENSON.getName().toString();
private static final String VALID_PHONE = BENSON.getPhone().toString();
private static final String VALID_EMAIL = BENSON.getEmail().toString();
private static final String VALID_ADDRESS = BENSON.getAddress().toString();
+ private static final String VALID_TUITION_TIME = BENSON.getTuitionTime().toString();
private static final List VALID_TAGS = BENSON.getTags().stream()
.map(JsonAdaptedTag::new)
.collect(Collectors.toList());
@@ -40,71 +43,90 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception {
@Test
public void toModelType_invalidName_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = Name.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullName_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_invalidPhone_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = Phone.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullPhone_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_invalidEmail_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = Email.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullEmail_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_invalidAddress_throwsIllegalValueException() {
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = Address.MESSAGE_CONSTRAINTS;
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
@Test
public void toModelType_nullAddress_throwsIllegalValueException() {
- JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TUITION_TIME, VALID_TAGS);
String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName());
assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
}
+ @Test
+ public void toModelType_invalidTuitionTime_throwsIllegalValueException() {
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, INVALID_TUITION_TIME, VALID_TAGS);
+ String expectedMessage = TuitionTime.MESSAGE_CONSTRAINTS;
+ assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
+ }
+
+ @Test
+ public void toModelType_nullTuitionTime_throwsIllegalValueException() {
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, null, VALID_TAGS);
+ String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, TuitionTime.class.getSimpleName());
+ assertThrows(IllegalValueException.class, expectedMessage, person::toModelType);
+ }
+
@Test
public void toModelType_invalidTags_throwsIllegalValueException() {
List invalidTags = new ArrayList<>(VALID_TAGS);
invalidTags.add(new JsonAdaptedTag(INVALID_TAG));
- JsonAdaptedPerson person =
- new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags);
+ JsonAdaptedPerson person = new JsonAdaptedPerson(
+ VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TUITION_TIME, invalidTags);
assertThrows(IllegalValueException.class, person::toModelType);
}
-
}
diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java
index 4584bd5044e..a5942d8c78f 100644
--- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java
+++ b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java
@@ -10,6 +10,7 @@
import seedu.address.model.person.Name;
import seedu.address.model.person.Person;
import seedu.address.model.person.Phone;
+import seedu.address.model.person.TuitionTime;
import seedu.address.model.tag.Tag;
/**
@@ -36,6 +37,7 @@ public EditPersonDescriptorBuilder(Person person) {
descriptor.setPhone(person.getPhone());
descriptor.setEmail(person.getEmail());
descriptor.setAddress(person.getAddress());
+ descriptor.setTuitionTime(person.getTuitionTime());
descriptor.setTags(person.getTags());
}
@@ -71,6 +73,14 @@ public EditPersonDescriptorBuilder withAddress(String address) {
return this;
}
+ /**
+ * Sets the {@code TuitionTime} of the {@code EditPersonDescriptor} that we are building.
+ */
+ public EditPersonDescriptorBuilder withTuitionTime(String tuitionTime) {
+ descriptor.setTuitionTime(new TuitionTime(tuitionTime));
+ return this;
+ }
+
/**
* Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor}
* that we are building.
diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java
index 6be381d39ba..3fc998b54ef 100644
--- a/src/test/java/seedu/address/testutil/PersonBuilder.java
+++ b/src/test/java/seedu/address/testutil/PersonBuilder.java
@@ -8,6 +8,7 @@
import seedu.address.model.person.Name;
import seedu.address.model.person.Person;
import seedu.address.model.person.Phone;
+import seedu.address.model.person.TuitionTime;
import seedu.address.model.tag.Tag;
import seedu.address.model.util.SampleDataUtil;
@@ -20,11 +21,13 @@ public class PersonBuilder {
public static final String DEFAULT_PHONE = "85355255";
public static final String DEFAULT_EMAIL = "amy@gmail.com";
public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111";
+ public static final String DEFAULT_TUITION_TIME = "Monday, 1400-1600";
private Name name;
private Phone phone;
private Email email;
private Address address;
+ private TuitionTime tuitionTime;
private Set tags;
/**
@@ -35,6 +38,7 @@ public PersonBuilder() {
phone = new Phone(DEFAULT_PHONE);
email = new Email(DEFAULT_EMAIL);
address = new Address(DEFAULT_ADDRESS);
+ tuitionTime = new TuitionTime(DEFAULT_TUITION_TIME);
tags = new HashSet<>();
}
@@ -46,6 +50,7 @@ public PersonBuilder(Person personToCopy) {
phone = personToCopy.getPhone();
email = personToCopy.getEmail();
address = personToCopy.getAddress();
+ tuitionTime = personToCopy.getTuitionTime();
tags = new HashSet<>(personToCopy.getTags());
}
@@ -58,7 +63,7 @@ public PersonBuilder withName(String name) {
}
/**
- * Parses the {@code tags} into a {@code Set} and set it to the {@code Person} that we are building.
+ * Parses the {@code tags} into a {@code Set} and sets them to the {@code Person} that we are building.
*/
public PersonBuilder withTags(String ... tags) {
this.tags = SampleDataUtil.getTagSet(tags);
@@ -89,8 +94,18 @@ public PersonBuilder withEmail(String email) {
return this;
}
- public Person build() {
- return new Person(name, phone, email, address, tags);
+ /**
+ * Sets the {@code TuitionTime} of the {@code Person} that we are building.
+ */
+ public PersonBuilder withTuitionTime(String tuitionTime) {
+ this.tuitionTime = new TuitionTime(tuitionTime);
+ return this;
}
+ /**
+ * Builds and returns a {@code Person} object with the current attributes.
+ */
+ public Person build() {
+ return new Person(name, phone, email, address, tuitionTime, tags);
+ }
}
diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java
index 90849945183..c8fa0a798b2 100644
--- a/src/test/java/seedu/address/testutil/PersonUtil.java
+++ b/src/test/java/seedu/address/testutil/PersonUtil.java
@@ -5,6 +5,7 @@
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TUITION_TIME;
import java.util.Set;
@@ -34,6 +35,7 @@ public static String getPersonDetails(Person person) {
sb.append(PREFIX_PHONE + person.getPhone().value + " ");
sb.append(PREFIX_EMAIL + person.getEmail().value + " ");
sb.append(PREFIX_ADDRESS + person.getAddress().value + " ");
+ sb.append(PREFIX_TUITION_TIME).append(person.getTuitionTime().value).append(" ");
person.getTags().stream().forEach(
s -> sb.append(PREFIX_TAG + s.tagName + " ")
);
@@ -49,6 +51,8 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip
descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" "));
descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" "));
descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" "));
+ descriptor.getTuitionTime().ifPresent(
+ tuitionTime -> sb.append(PREFIX_TUITION_TIME).append(tuitionTime.value).append(" "));
if (descriptor.getTags().isPresent()) {
Set tags = descriptor.getTags().get();
if (tags.isEmpty()) {
diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java
index fec76fb7129..52cf61fceb9 100644
--- a/src/test/java/seedu/address/testutil/TypicalPersons.java
+++ b/src/test/java/seedu/address/testutil/TypicalPersons.java
@@ -8,8 +8,8 @@
import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY;
import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND;
-import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_MATH;
+import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_SCIENCE;
import java.util.ArrayList;
import java.util.Arrays;
@@ -25,35 +25,43 @@ public class TypicalPersons {
public static final Person ALICE = new PersonBuilder().withName("Alice Pauline")
.withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com")
- .withPhone("94351253")
- .withTags("friends").build();
+ .withPhone("94351253").withTuitionTime("Monday, 1400-1600")
+ .withTags("Math").build();
public static final Person BENSON = new PersonBuilder().withName("Benson Meier")
- .withAddress("311, Clementi Ave 2, #02-25")
- .withEmail("johnd@example.com").withPhone("98765432")
- .withTags("owesMoney", "friends").build();
+ .withAddress("311, Clementi Ave 2, #02-25").withEmail("johnd@example.com")
+ .withPhone("98765432").withTuitionTime("Tuesday, 1000-1200")
+ .withTags("Math", "Science").build();
public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563")
- .withEmail("heinz@example.com").withAddress("wall street").build();
+ .withEmail("heinz@example.com").withAddress("wall street")
+ .withTuitionTime("Wednesday, 0930-1130").build();
public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533")
- .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build();
- public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224")
- .withEmail("werner@example.com").withAddress("michegan ave").build();
- public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427")
- .withEmail("lydia@example.com").withAddress("little tokyo").build();
- public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442")
- .withEmail("anna@example.com").withAddress("4th street").build();
+ .withEmail("cornelia@example.com").withAddress("10th street")
+ .withTuitionTime("Thursday, 1500-1700").withTags("English").build();
+ public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("+6591234567")
+ .withEmail("werner@example.com").withAddress("michegan ave")
+ .withTuitionTime("Friday, 0800-1000").build();
+ public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("+14085551234")
+ .withEmail("lydia@example.com").withAddress("little tokyo")
+ .withTuitionTime("Saturday, 1300-1500").build();
+ public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("+4915123456789")
+ .withEmail("anna@example.com").withAddress("4th street")
+ .withTuitionTime("Sunday, 1400-1600").build();
// Manually added
- public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424")
- .withEmail("stefan@example.com").withAddress("little india").build();
- public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131")
- .withEmail("hans@example.com").withAddress("chicago ave").build();
+ public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("+819012345678")
+ .withEmail("stefan@example.com").withAddress("little india")
+ .withTuitionTime("Monday, 1600-1800").build();
+ public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("+61412345678")
+ .withEmail("hans@example.com").withAddress("chicago ave")
+ .withTuitionTime("Tuesday, 1100-1300").build();
// Manually added - Person's details found in {@code CommandTestUtil}
public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY)
- .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build();
+ .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY)
+ .withTuitionTime("Wednesday, 1000-1200").withTags(VALID_TAG_MATH).build();
public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB)
- .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND)
- .build();
+ .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB)
+ .withTuitionTime("Thursday, 1400-1600").withTags(VALID_TAG_SCIENCE, VALID_TAG_MATH).build();
public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER
diff --git a/src/test/java/seedu/address/ui/CommandConfigTest.java b/src/test/java/seedu/address/ui/CommandConfigTest.java
new file mode 100644
index 00000000000..7839d43d333
--- /dev/null
+++ b/src/test/java/seedu/address/ui/CommandConfigTest.java
@@ -0,0 +1,44 @@
+package seedu.address.ui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+public class CommandConfigTest {
+
+ @Test
+ public void getAllCommands_returnsNonNullList() {
+ List commands = CommandConfig.getAllCommands();
+ assertNotNull(commands, "Command list should not be null");
+ }
+
+ @Test
+ public void getAllCommands_returnsCorrectNumberOfCommands() {
+ List commands = CommandConfig.getAllCommands();
+ // There are 10 commands defined in CommandConfig.getAllCommands()
+ assertEquals(10, commands.size(), "Expected 10 commands in the list");
+ }
+
+ @Test
+ public void getAllCommands_containsExpectedCommandInfo() {
+ List commands = CommandConfig.getAllCommands();
+
+ // Check the first command (the 'add' command)
+ CommandUsageInfo addCommand = commands.get(0);
+ String expectedCommand = "add - add a person to the address book";
+ String expectedFormat = "add n/NAME p/PHONE e/EMAIL a/ADDRESS [t/TAG] [tt/TUITION TIME]";
+ String expectedExample = "add n/John Doe p/98765432 e/johnd@example.com "
+ + "a/311 Clementi Ave 2 t/math t/science tt/Monday, 1000 - 1200";
+
+ assertEquals(expectedCommand, addCommand.command(), "Command name does not match");
+ assertEquals(expectedFormat, addCommand.format(), "Command format does not match");
+
+ // Normalize whitespace in the example to avoid minor spacing issues
+ String normalizedExample = addCommand.example().replaceAll("\\s+", " ").trim();
+ String normalizedExpected = expectedExample.replaceAll("\\s+", " ").trim();
+ assertEquals(normalizedExpected, normalizedExample, "Command example does not match");
+ }
+}
diff --git a/src/test/java/seedu/address/ui/CommandHistoryManagerTest.java b/src/test/java/seedu/address/ui/CommandHistoryManagerTest.java
new file mode 100644
index 00000000000..0bbcc6d2e5d
--- /dev/null
+++ b/src/test/java/seedu/address/ui/CommandHistoryManagerTest.java
@@ -0,0 +1,75 @@
+package seedu.address.ui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class CommandHistoryManagerTest {
+
+ private CommandHistoryManager historyManager;
+
+ @BeforeEach
+ public void setUp() {
+ historyManager = new CommandHistoryManager();
+ }
+
+ @Test
+ public void getPrevious_emptyHistory_returnsNull() {
+ String result = historyManager.getPrevious("typing something");
+ assertNull(result);
+ }
+
+ @Test
+ public void add_andNavigateHistory_correctOrder() {
+ historyManager.add("list");
+ historyManager.add("add John");
+ historyManager.add("delete 1");
+
+ // Simulate that user is typing something before pressing UP for the first time
+ String dummyCurrentInput = "some in-progress typing";
+
+ // Navigate up
+ assertEquals("delete 1", historyManager.getPrevious(dummyCurrentInput));
+ assertEquals("add John", historyManager.getPrevious(""));
+ assertEquals("list", historyManager.getPrevious(""));
+
+ // Cannot go further up
+ assertEquals("list", historyManager.getPrevious(""));
+
+ // Navigate down
+ assertEquals("add John", historyManager.getNext());
+ assertEquals("delete 1", historyManager.getNext());
+
+ // Reaches temp input (restores what was typed before)
+ assertEquals(dummyCurrentInput, historyManager.getNext());
+
+ // Cannot go further down
+ assertEquals(dummyCurrentInput, historyManager.getNext());
+ }
+
+ @Test
+ public void navigateHistory_preservesTempInput() {
+ historyManager.add("list");
+
+ // Start typing a new command
+ String inProgress = "add Alice";
+
+ // Navigate up to access history
+ assertEquals("list", historyManager.getPrevious(inProgress));
+
+ // Navigate down should return back to in-progress input
+ assertEquals(inProgress, historyManager.getNext());
+ }
+
+ @Test
+ public void addCommand_resetsTempInput() {
+ historyManager.getPrevious("temp input");
+ historyManager.add("new command");
+
+ // Should clear previous tempInput
+ assertEquals("new command", historyManager.getPrevious(""));
+ assertEquals("", historyManager.getNext());
+ }
+}
diff --git a/src/test/java/seedu/address/ui/CommandUsageInfoTest.java b/src/test/java/seedu/address/ui/CommandUsageInfoTest.java
new file mode 100644
index 00000000000..eea8dee796a
--- /dev/null
+++ b/src/test/java/seedu/address/ui/CommandUsageInfoTest.java
@@ -0,0 +1,35 @@
+package seedu.address.ui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class CommandUsageInfoTest {
+
+ @Test
+ public void constructor_createsRecordWithValues() {
+ // Given
+ String command = "add";
+ String format = "add n/NAME p/PHONE";
+ String example = "add n/John p/91234567";
+
+ // When
+ CommandUsageInfo info = new CommandUsageInfo(command, format, example);
+
+ // Then
+ assertEquals(command, info.command(), "Command field should match constructor arg");
+ assertEquals(format, info.format(), "Format field should match constructor arg");
+ assertEquals(example, info.example(), "Example field should match constructor arg");
+ }
+
+ @Test
+ public void record_immutableProperties() {
+ // Once a record is created, its fields cannot be changed (immutability).
+ CommandUsageInfo info = new CommandUsageInfo("delete", "delete INDEX", "delete 3");
+
+ // We can only read the values, not modify them
+ assertEquals("delete", info.command());
+ assertEquals("delete INDEX", info.format());
+ assertEquals("delete 3", info.example());
+ }
+}