From 3652153a7c99efb201c1447de0d73d4ce73b4637 Mon Sep 17 00:00:00 2001 From: marcus_pzj Date: Mon, 18 Mar 2019 03:46:20 +0800 Subject: [PATCH 1/4] Add User Stories --- docs/DeveloperGuide.adoc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index ac0ef41f6..f7cb944af 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -358,7 +358,7 @@ image::UndoRedoActivityDiagram.png[width="650"] === Data Encryption -The storage file "PlanMySem.txt" is encrypted to prevent easy access of the user's calendar. +The storage file "PlanMySem.txt" is encrypted to prevent easy access of the user's calendar. We are encrypting and decrypting the data using the Java Cypher class. This feature is implemented through creating a Encryptor that contains encrypt and decrypt methods. The encrypt method takes a String object as an argument and returns a encrypted String object. The decrypt method takes in a String object as an argument and returns the decrypted message as a String object. @@ -370,10 +370,10 @@ Similarly, decryption of the data is done automatically before it is loaded. In === Exporting .ics file -The user can export the current planner into a .ics file to use in external calendar applications. The .ics file will contain the names of the slots in the SUMMARY field and the descriptions in the DESCRIPTION field. This command automatically exports into the main directory and names the file “PlanMySem.ics”. Future updates can include user input to allow saving the file in another directory and naming the file. -We have chosen to use the iCalendar format due to its popularity and it’s use in applications such as Google Calendar, Microsoft Outlook and Nusmods. +The user can export the current planner into a .ics file to use in external calendar applications. The .ics file will contain the names of the slots in the SUMMARY field and the descriptions in the DESCRIPTION field. This command automatically exports into the main directory and names the file “PlanMySem.ics”. Future updates can include user input to allow saving the file in another directory and naming the file. +We have chosen to use the iCalendar format due to its popularity and it’s use in applications such as Google Calendar, Microsoft Outlook and Nusmods. -In our implementation, we have chosen not to export the tags into the .ics file. This is because iCalendar does not have in-built tag fields. This means that other other applications that import .ics will not be able to use the tags. +In our implementation, we have chosen not to export the tags into the .ics file. This is because iCalendar does not have in-built tag fields. This means that other other applications that import .ics will not be able to use the tags. ===== Aspect: Exporting tags into .ics file. @@ -906,6 +906,8 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un |`* * *` |user |view all slots on a certain week |conveniently view my planner for the week |`* * *` |user |view all slots on a certain month |conveniently view my planner for the month |`* * *` |user |add details to a slot |record information related to the slot +|`* * *` |user |undo a command |easily revert my changes and restore a previous state +|`* * *` |user |redo a command |easily revert my `undo` command in case I mistakenly undo too far |`* *` |user |view the planner in a graphic calendar format|easily view my schedule for the day/week/month/semester |`* *` |user |view a slot |view the details of a specific activity I am looking for |`* *` |user |remove tags on a time slot |remove unused/ unnecessary tags from an activity* @@ -918,6 +920,7 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un |`*` |user |export semester timetable (.ics files) |view my timetable on another platform |`*` |user |receive notifications of upcoming activities |be reminded of important upcoming activities |`*` |user |view recess week and exam week |view specifically the weeks to rest +|`*` |user |view vacations |plan my schedule on vacation days or special semesters |`*` |user |favourite an activity |prioritise important activities |`*` |user |view public holidays |be aware of upcoming public holidays |`*` |user |compare my timetable with someone else's |find a common time slot for a meeting From fa2720440d714d1eb9de2db09fde46236614faa0 Mon Sep 17 00:00:00 2001 From: Marcus Phua Zheng Jie <32394182+marcus-pzj@users.noreply.github.com> Date: Mon, 18 Mar 2019 19:50:19 +0800 Subject: [PATCH 2/4] Merge (#12) * added Encryptor class to encrypt and decrypt storage data * set encryption to false * added junit class * checkstyle edits * removed junit test * checkstyle edits * more checkstyle edits * Update documentation for encryption * export calendar as .ics file * checkstyle edits * added some documentation * Update developer guide for export * Add User Stories (#83) * Resolve checkstyle errors (#85) * Enhance FindCommand (#86) * Add view command for monthly calendar view (#87) * Planner: Initialize weekType for each Day * Planner: Fix Codacy issue * Planner: Update code for initialisation of semester * Planner: Fix Codacy and checkstyle issues * Planner: Fix build error * Planner: Dynamic generation of semester from date * PlannerTest: Add JUnit test for generateSemester * Update documentation for view command * Add view command for monthly calendar view * Fix Codacy issue * Add test cases for add function, Utils and other code enhancements (#88) * v1.1 * fix some errors and typos * Refactor entire project, remove all traces of Addressbook * Update developer guide for ExportCommandP (#80) * Update documentation for view command (#81) * Remove remaining Addressbook classes * Refactor some classes * fix missing files issue * Add test cases for add function, Utils and other code enhancements * fix codacy issues * fix codacy issues --- PlanMySem.txt | 1002 ----------------- docs/DeveloperGuide.adoc | 157 ++- docs/UserGuide.adoc | 6 +- src/planmysem/Main.java | 7 +- src/planmysem/commands/AddCommand.java | 18 +- src/planmysem/commands/DeleteCommand.java | 6 +- src/planmysem/commands/EditCommand.java | 12 +- src/planmysem/commands/ExportCommand.java | 1 - src/planmysem/commands/FindCommand.java | 86 +- src/planmysem/commands/ListCommand.java | 8 +- src/planmysem/commands/ViewCommand.java | 137 +++ src/planmysem/common/Messages.java | 11 + src/planmysem/common/Utils.java | 44 +- src/planmysem/data/Planner.java | 12 +- src/planmysem/data/semester/Day.java | 14 + src/planmysem/data/semester/ReadOnlyDay.java | 13 + src/planmysem/data/semester/Semester.java | 31 +- src/planmysem/data/slot/Location.java | 4 +- src/planmysem/data/slot/ReadOnlySlot.java | 10 +- src/planmysem/data/slot/Slot.java | 16 +- src/planmysem/data/tag/Tag.java | 30 +- src/planmysem/data/tag/TagP.java | 53 - src/planmysem/logic/Logic.java | 4 +- src/planmysem/parser/Parser.java | 117 +- .../storage/jaxb/AdaptedSemester.java | 5 + src/planmysem/storage/jaxb/AdaptedSlot.java | 6 +- src/planmysem/storage/jaxb/AdaptedTag.java | 8 +- src/planmysem/ui/DarkTheme.css | 2 +- src/planmysem/ui/MainWindow.java | 5 +- test/data/StorageFileTest/InvalidData.txt | 4 +- test/data/StorageFileTest/ValidData.txt | 971 +++++++++++++++- test/java/planmysem/common/UtilsTest.java | 76 +- test/java/planmysem/logic/LogicTest.java | 339 +++++- test/java/planmysem/parser/ParserTest.java | 274 +++++ .../planmysem/storage/StorageFileTest.java | 88 +- 35 files changed, 2245 insertions(+), 1332 deletions(-) delete mode 100644 PlanMySem.txt create mode 100644 src/planmysem/commands/ViewCommand.java delete mode 100644 src/planmysem/data/tag/TagP.java diff --git a/PlanMySem.txt b/PlanMySem.txt deleted file mode 100644 index 3623cfa84..000000000 --- a/PlanMySem.txt +++ /dev/null @@ -1,1002 +0,0 @@ - - - - Sem 2 - AY2018/2019 - 2019-01-14 - 2019-05-12 - 17 - - - 2019-02-09 - - SATURDAY - Week 4 - - - - 2019-02-08 - - FRIDAY - Week 4 - - - - 2019-02-07 - - THURSDAY - Week 4 - - - - 2019-02-06 - - WEDNESDAY - Week 4 - - - - 2019-02-05 - - TUESDAY - Week 4 - - - - 2019-02-04 - - MONDAY - Week 4 - - - - 2019-02-03 - - SUNDAY - Week 3 - - - - 2019-02-02 - - SATURDAY - Week 3 - - - - 2019-02-01 - - FRIDAY - Week 3 - - - - 2019-01-31 - - THURSDAY - Week 3 - - - - 2019-01-30 - - WEDNESDAY - Week 3 - - - - 2019-04-30 - - TUESDAY - Examination Week - - - - 2019-04-20 - - SATURDAY - Week 13 - - - - 2019-04-21 - - SUNDAY - Week 13 - - - - 2019-04-22 - - MONDAY - Reading Week - - - - 2019-04-23 - - TUESDAY - Reading Week - - - - 2019-04-24 - - WEDNESDAY - Reading Week - - - - 2019-04-25 - - THURSDAY - Reading Week - - - - 2019-04-26 - - FRIDAY - Reading Week - - - - 2019-04-27 - - SATURDAY - Reading Week - - - - 2019-04-28 - - SUNDAY - Reading Week - - - - 2019-04-29 - - MONDAY - Examination Week - - - - 2019-02-19 - - TUESDAY - Week 6 - - - - 2019-02-18 - - MONDAY - Week 6 - - - - 2019-02-17 - - SUNDAY - Week 5 - - - - 2019-02-16 - - SATURDAY - Week 5 - - - - 2019-02-15 - - FRIDAY - Week 5 - - - - 2019-02-14 - - THURSDAY - Week 5 - - - - 2019-02-13 - - WEDNESDAY - Week 5 - - - - 2019-02-12 - - TUESDAY - Week 5 - - - - 2019-02-11 - - MONDAY - Week 5 - - - - 2019-02-10 - - SUNDAY - Week 4 - - - - 2019-05-01 - - WEDNESDAY - Examination Week - - - - 2019-05-02 - - THURSDAY - Examination Week - - - - 2019-05-03 - - FRIDAY - Examination Week - - - - 2019-05-04 - - SATURDAY - Examination Week - - - - 2019-05-05 - - SUNDAY - Examination Week - - - - 2019-05-06 - - MONDAY - Examination Week - - - - 2019-05-07 - - TUESDAY - Examination Week - - - - 2019-05-08 - - WEDNESDAY - Examination Week - - - - 2019-05-09 - - THURSDAY - Examination Week - - - - 2019-02-28 - - THURSDAY - Recess Week - - - - 2019-02-27 - - WEDNESDAY - Recess Week - - - - 2019-02-26 - - TUESDAY - Recess Week - - - - 2019-02-25 - - MONDAY - Recess Week - - - - 2019-02-24 - - SUNDAY - Week 6 - - - - 2019-02-23 - - SATURDAY - Week 6 - - - - 2019-02-22 - - FRIDAY - Week 6 - - - - 2019-02-21 - - THURSDAY - Week 6 - - - - 2019-02-20 - - WEDNESDAY - Week 6 - - - - 2019-05-10 - - FRIDAY - Examination Week - - - - 2019-05-11 - - SATURDAY - Examination Week - - - - 2019-03-01 - - FRIDAY - Recess Week - - - - 2019-03-02 - - SATURDAY - Recess Week - - - - 2019-03-03 - - SUNDAY - Recess Week - - - - 2019-03-04 - - MONDAY - Week 7 - - - - 2019-03-05 - - TUESDAY - Week 7 - - - - 2019-03-06 - - WEDNESDAY - Week 7 - - - - 2019-03-07 - - THURSDAY - Week 7 - - - - 2019-03-08 - - FRIDAY - Week 7 - - - - 2019-03-09 - - SATURDAY - Week 7 - - - - 2019-03-10 - - SUNDAY - Week 7 - - - - 2019-03-11 - - MONDAY - Week 8 - - - - 2019-03-12 - - TUESDAY - Week 8 - - - - 2019-03-13 - - WEDNESDAY - Week 8 - - - - 2019-03-14 - - THURSDAY - Week 8 - - - - 2019-03-15 - - FRIDAY - Week 8 - - - - 2019-03-16 - - SATURDAY - Week 8 - - - - 2019-03-17 - - SUNDAY - Week 8 - - - - 2019-03-18 - - MONDAY - Week 9 - - CS1010 - 60 - 08:00 - lab - - - CS2030 - 60 - 12:00 - tutorial - - - CS2101 - 180 - 11:00 - lab - - - - - 2019-03-19 - - TUESDAY - Week 9 - - - - 2019-03-20 - - WEDNESDAY - Week 9 - - CS2113T - 60 - 11:00 - lab - - - - - 2019-03-21 - - THURSDAY - Week 9 - - - - 2019-03-22 - - FRIDAY - Week 9 - - CS2113T - 180 - 09:00 - lab - - - - - 2019-03-23 - - SATURDAY - Week 9 - - - - 2019-03-24 - - SUNDAY - Week 9 - - - - 2019-03-25 - - MONDAY - Week 10 - - CS2113T Tutorial - Topic: Sequence Diagram - 60 - 08:00 - Tutorial - CS2113T - - - CS2113T Tutorial - Topic: Sequence Diagram - 60 - 08:00 - Tutorial - CS2113T - - - - - 2019-03-26 - - TUESDAY - Week 10 - - - - 2019-03-27 - - WEDNESDAY - Week 10 - - - - 2019-03-28 - - THURSDAY - Week 10 - - - - 2019-03-29 - - FRIDAY - Week 10 - - - - 2019-01-19 - - SATURDAY - Week 1 - - - - 2019-01-18 - - FRIDAY - Week 1 - - - - 2019-01-17 - - THURSDAY - Week 1 - - - - 2019-01-16 - - WEDNESDAY - Week 1 - - - - 2019-01-15 - - TUESDAY - Week 1 - - - - 2019-01-14 - - MONDAY - Week 1 - - - - 2019-03-30 - - SATURDAY - Week 10 - - - - 2019-03-31 - - SUNDAY - Week 10 - - - - 2019-04-01 - - MONDAY - Week 11 - - - - 2019-04-02 - - TUESDAY - Week 11 - - - - 2019-04-03 - - WEDNESDAY - Week 11 - - - - 2019-04-04 - - THURSDAY - Week 11 - - - - 2019-04-05 - - FRIDAY - Week 11 - - - - 2019-04-06 - - SATURDAY - Week 11 - - - - 2019-04-07 - - SUNDAY - Week 11 - - - - 2019-04-08 - - MONDAY - Week 12 - - - - 2019-04-09 - - TUESDAY - Week 12 - - - - 2019-01-29 - - TUESDAY - Week 3 - - - - 2019-01-28 - - MONDAY - Week 3 - - - - 2019-01-27 - - SUNDAY - Week 2 - - - - 2019-01-26 - - SATURDAY - Week 2 - - - - 2019-01-25 - - FRIDAY - Week 2 - - - - 2019-01-24 - - THURSDAY - Week 2 - - - - 2019-01-23 - - WEDNESDAY - Week 2 - - - - 2019-01-22 - - TUESDAY - Week 2 - - - - 2019-01-21 - - MONDAY - Week 2 - - - - 2019-01-20 - - SUNDAY - Week 1 - - - - 2019-04-10 - - WEDNESDAY - Week 12 - - - - 2019-04-11 - - THURSDAY - Week 12 - - - - 2019-04-12 - - FRIDAY - Week 12 - - - - 2019-04-13 - - SATURDAY - Week 12 - - - - 2019-04-14 - - SUNDAY - Week 12 - - - - 2019-04-15 - - MONDAY - Week 13 - - - - 2019-04-16 - - TUESDAY - Week 13 - - - - 2019-04-17 - - WEDNESDAY - Week 13 - - - - 2019-04-18 - - THURSDAY - Week 13 - - - - 2019-04-19 - - FRIDAY - Week 13 - - - - 2019-03-01 - 2019-02-28 - 2019-03-02 - 2019-02-27 - 2019-03-03 - 2019-02-26 - 2019-02-25 - 2019-04-22 - 2019-04-23 - 2019-04-24 - 2019-04-25 - 2019-04-26 - 2019-04-27 - 2019-04-28 - 2019-02-09 - 2019-02-08 - 2019-02-07 - 2019-02-06 - 2019-02-05 - 2019-02-04 - 2019-02-03 - 2019-02-02 - 2019-02-01 - 2019-01-31 - 2019-01-30 - 2019-04-20 - 2019-04-21 - 2019-03-10 - 2019-03-11 - 2019-03-12 - 2019-03-13 - 2019-03-14 - 2019-03-15 - 2019-03-16 - 2019-03-17 - 2019-03-18 - 2019-03-19 - 2019-02-19 - 2019-02-18 - 2019-02-17 - 2019-02-16 - 2019-02-15 - 2019-02-14 - 2019-02-13 - 2019-02-12 - 2019-02-11 - 2019-02-10 - 2019-03-20 - 2019-03-21 - 2019-03-22 - 2019-03-23 - 2019-03-24 - 2019-03-25 - 2019-03-26 - 2019-03-27 - 2019-03-28 - 2019-03-29 - 2019-01-19 - 2019-01-18 - 2019-01-17 - 2019-01-16 - 2019-01-15 - 2019-01-14 - 2019-02-24 - 2019-02-23 - 2019-02-22 - 2019-02-21 - 2019-02-20 - 2019-03-30 - 2019-03-31 - 2019-04-01 - 2019-04-02 - 2019-04-03 - 2019-04-04 - 2019-04-05 - 2019-04-06 - 2019-04-07 - 2019-04-08 - 2019-04-09 - 2019-01-29 - 2019-01-28 - 2019-01-27 - 2019-01-26 - 2019-01-25 - 2019-01-24 - 2019-01-23 - 2019-01-22 - 2019-01-21 - 2019-01-20 - 2019-04-10 - 2019-04-11 - 2019-04-12 - 2019-04-13 - 2019-04-14 - 2019-04-15 - 2019-04-16 - 2019-03-04 - 2019-04-17 - 2019-03-05 - 2019-04-18 - 2019-03-06 - 2019-04-19 - 2019-03-07 - 2019-03-08 - 2019-03-09 - 2019-04-30 - 2019-05-10 - 2019-05-11 - 2019-05-01 - 2019-05-02 - 2019-05-03 - 2019-05-04 - 2019-05-05 - 2019-05-06 - 2019-05-07 - 2019-04-29 - 2019-05-08 - 2019-05-09 - - diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index f7cb944af..fa7e9c6ef 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -56,7 +56,7 @@ This will generate all resources required by the application and tests. === Verifying the setup -. Run the `seedu.address.MainApp` and try a few commands +. Run the `planmysem.Main` and try a few commands . <> to ensure they all pass. === Configurations to do before writing code @@ -76,9 +76,9 @@ Optionally, you can follow the <> docume ==== Updating documentation to match your fork -After forking the repo, the documentation will still have the SE-EDU branding and refer to the `se-edu/addressbook-level4` repo. +After forking the repo, the documentation will still have the SE-EDU branding and refer to the `https://github.com/CS2113-AY1819S2-T08-3/main` repo. -If you plan to develop this fork as a separate product (i.e. instead of contributing to `se-edu/addressbook-level4`), you should do the following: +If you plan to develop this fork as a separate product (i.e. instead of contributing to `https://github.com/CS2113-AY1819S2-T08-3/main`), you should do the following: . Configure the <> in link:{repoURL}/build.gradle[`build.gradle`], such as the `site-name`, to suit your own project. @@ -118,7 +118,7 @@ The *_Architecture Diagram_* given above explains the high-level design of the A [TIP] The `.pptx` files used to create diagrams in this document can be found in the link:{repoURL}/docs/diagrams/[diagrams] folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose `Save as picture`. -`Main` has only one class called link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp`]. It is responsible for, +`Main` has only one class called link:{repoURL}/src/planmysem/Main.java[`Main`]. It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup method where necessary. @@ -193,59 +193,157 @@ Given below is the Sequence Diagram for interactions within the `Logic` componen .Interactions Inside the Logic Component for the `delete 1` Command image::DeletePersonSdForLogic.png[width="800"] -[[Design-Model]] -=== Model component +[[Design-Planner]] +=== Planner component .Structure of the Model Component image::ModelClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] +*API* : link:{repoURL}/src/planmysem/data/Planner.java[`Planner.java`] + +The `Planner` component, + +* stores a `Planner` object that represents the entire Planner. +* stores the data of the entire application. +* stores the data of the current semester in an unmodifiable `Semester`. + +[[Design-Semester]] + +=== Semester component -The `Model`, +*API* : link:{repoURL}/src/planmysem/data/semester/Semester.java[`Semester.java`] -* stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* does not depend on any of the other three components. +The `Semester` component, + +* stores a `Semester` object that represents the an academic semester. +* stores the data of the entire semester in an unmodifiable `HashMap`. +* Semesters essentially hold "days" in which holds slots. + +[[Design-Slot]] + +=== Slot component + +*API* : link:{repoURL}/src/planmysem/data/slot/Slot.java[`Slot.java`] + +The `Slot` component, + +* stores a `Slot` object that represents a time-slot similar to traditional/conventional calendar/scheduling applications. +** such as outlook or google calendar. +* stores the data of the slot details as well as start time and duration. [NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + - + -image:ModelClassBetterOopDiagram.png[width="800"] +Notice how `Slot` does not hold it's end time but rather it holds the `duration`. This is simply our design choice as it is meaningless to save both variables. [[Design-Storage]] + === Storage component .Structure of the Storage Component image::StorageClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] +*API* : link:{repoURL}/src/planmysem/storage/Storage.java[`Storage.java`] The `Storage` component, * can save `UserPref` objects in json format and read it back. * can save the Address Book data in json format and read it back. -[[Design-Planner]] -=== Planner component - -*API* : link:{repoURL}/src/planmysem/data/Planner.java[`Planner.java`] +[[Design-Common]] -The `Planner` component, - -* can initialise and hold a `Semester` object. - -[[Design-Commons]] === Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `planmysem.commons` package. == Implementation This section describes some noteworthy details on how certain features are implemented. +=== Initializing of the Planner and it's Semesters + +==== Future Implementation + +=== Reinventing the Parser / Command Format and Structure + +Due to the flexibility and huge variation of the envisioned command format and structures, it was decided that it was more appropriate to create a new Parser +instead of relying on the existing regex implementation in AB3 for heavy parsing. + +The AB3 parser was heavily modified to serve unordered command parameters as well as to allow more flexibility such that mistakes in commands will still be +interpreted as valid as long as the "minimal" set of parameters are present. Regex is currently only used to retrieve the command keywords and arguments. +Arguments are then parsed via 2 different methods/techniques according to the format and structure of the command keyword. + +==== Design Considerations + +* User's time, efficiency and productivity is of focus, such as in a planner/calendar/scheduling application. +* Users are error prone when typing long commands. +* Users may repeat parameters due to forgetting what was previously typed. +* Users get frustrated when forced to retype commands or look for errors that they have made. + +==== Current Implementation + +* Ordering of parameters are ignored when possible. +* Repeated parameters are ignored. The first parameter of the same "type" are taken as valid, the rest are discarded. +* Alternate formats of commands are implemented to give freedom of choice and cater to different types of users such as different personalities and comfort levels. +* Shortened versions of command keywords are implemented to give ways for users to shortened commands and be more efficient. + +Hence, parameters in PlanMySem can be categorised into 2 categories: + +1. Prepended parameters such as `n/NAME`, `st/START_TIME`, `des/DESCRIPTION`, etc. +2. Non-prepended parameters, A.K.A. keywords, such as `INDEX`, `TYPE_OF_VIEW`. etc. + +===== Parsing Prepended Parameters + +To retrieve the set of parameters, the function `private static HashMap> getParametersWithArguments(String args)` can be called. +The function returns a `Hashmap` data structure, specifically `HashMap>`, to hold parameters, allowing for easy, quick and efficient access to specific parameters. + +However, this means that exceptions have to be manually taken care of, at the stage of parsing, this exception would particularly be `IncorrectCommand`. + +The following are cases in which `IncorrectCommand` is invoked: + +* When the returned set is `null`, then the parameter was not keyed in at all. +* When the returned set is not `null` but contains empty `strings` such that `string.isEmpty()`, then the paramter was keyed in but was left empty on purpose. + +===== Parsing Keywords + +Here, keywords are thought of as parameters that are not prepended. +In PlanMySem, keywords are utilized in command structures when they are to be used alone or when order of parameters are important. +In such cases, there is no logical need for prepending as the meaning of these parameters can be identified. + +The function `private String getStartingArgument(String args)` provides this functionality. +Here, `IncorrectCommand` is invoked due to different circumstances: + +* When the keyword is null, then the parameter was not keyed in. +* When the keyword data type does not match the intended, then the parameter was keyed in wrongly or is mis-ordered. + +[NOTE] +Additional keywords are purposefully *not* handled to provide ease of use and cater to user mistakes. + +==== Future Implementation + +Though the current implementation has much flexibility, there is more that can be done to elevate user experience to the next level. +These are some possible enhancements: + +1. Parse more formats of date and time. +2. Parse time as a single parameter instead of two. +3. Enhance function calls to retrieve prepended parameters and keywords to handle trivial cases that should invoke `IncorrectCommand`. + +=== Semester, Planner, Days and Slots + +==== Design Considerations + +==== Current Implementation + +===== Semester + +===== Planner + +===== Days + +===== Slots + +==== Future Implementation + === Month/Week/Day View feature -==== Planned Implementation +==== Future Implementation The month/week/day view mechanism is facilitated by `PlannerView`. It extends `Planner` with different calendar views, stored internally as `monthView`, `weekView` and `dayView`. @@ -263,7 +361,6 @@ Step 2. The user executes `add n/CS2113T Tutorial ...` to add a new slot to the Step 3. The user executes `view month March` command to view the calendar for March. The `view` command calls `PlannerView.displayMonthView()`, which loads the data the updated `Semester` from the `Planner` into `monthView`. - ==== Design Considerations ===== Aspect: How a type of view is initialised or updated @@ -272,7 +369,7 @@ Step 3. The user executes `view month March` command to view the calendar for Ma ** Pros: Easy to implement. Less operations performed by the program. * **Alternative 2:** Updated upon any `add`/`edit`/`delete` slot command. ** Pros: `PlannerView` is always updated. -** Cons: Large amount of extra operations. +** Cons: Large amount of additional operations. // tag::undoredo[] @@ -581,7 +678,7 @@ When a pull request has changes to asciidoc files, you can use https://www.netli Here are the steps to create a new release. -. Update the version number in link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp.java`]. +. Update the version number in link:{repoURL}/src/planmysem/Main.java[`Main.java`]. . Generate a JAR file <>. . Tag the repo with the version number. e.g. `v0.1` . https://help.github.com/articles/creating-releases/[Create a new release using GitHub] and upload the JAR file you created. diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 626b7dad1..60d57fbbc 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -72,12 +72,12 @@ Below is the list of parameters for users such as yourself to jump right into gr * Day of Week: `Monday`, `mon`, `1` `st/`:: *Start Time.* Format: + - * 24-Hour: `hh:mm` - * 12-Hour: `hh:mm+AM|PM` + * 24-Hour in the form of “hh:mm”. e.g. `23:00` + * 12-Hour in the form of `hh:mm+AM|PM`. e.g. `12:30 AM` `et/`:: *End Time / duration.* + Format: * 24-Hour in the form of “hh:mm”. e.g. `23:00` - * 12-Hour format in the form of `hh:mm+AM|PM`. e.g. `12:30am` + * 12-Hour in the form of `hh:mm+AM|PM`. e.g. `12:30 AM` * Duration of the event in minutes. e.g. `60` represents 60 minutes `r/`:: *recurrence.* + Format: diff --git a/src/planmysem/Main.java b/src/planmysem/Main.java index 986561074..77adaf88e 100644 --- a/src/planmysem/Main.java +++ b/src/planmysem/Main.java @@ -15,9 +15,7 @@ public class Main extends Application implements Stoppable { /** * Version info of the program. */ - public static final String VERSION = "PlanMySem - Version 1.0"; - - private Gui gui; + public static final String VERSION = "PlanMySem - Version 1.2"; public static void main(String[] args) { launch(args); @@ -25,7 +23,7 @@ public static void main(String[] args) { @Override public void start(Stage primaryStage) throws Exception { - gui = new Gui(new Logic(), VERSION); + Gui gui = new Gui(new Logic(), VERSION); gui.start(primaryStage, this); } @@ -33,7 +31,6 @@ public void start(Stage primaryStage) throws Exception { public void stop() throws Exception { super.stop(); Platform.exit(); - System.exit(0); } } diff --git a/src/planmysem/commands/AddCommand.java b/src/planmysem/commands/AddCommand.java index a88ed9347..3dfe6b798 100644 --- a/src/planmysem/commands/AddCommand.java +++ b/src/planmysem/commands/AddCommand.java @@ -2,11 +2,14 @@ import java.time.LocalDate; import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import planmysem.common.Utils; import planmysem.data.exception.IllegalValueException; import planmysem.data.recurrence.Recurrence; +import planmysem.data.semester.Day; import planmysem.data.semester.Semester; import planmysem.data.slot.Description; import planmysem.data.slot.Location; @@ -62,10 +65,11 @@ public AddCommand(int day, String name, String location, String description, Loc @Override public CommandResult execute() { Set dates = recurrence.generateDates(planner.getSemester()); + HashMap days = new HashMap<>(); for (LocalDate date : dates) { try { - planner.addSlot(date, slot); + days.put(date, planner.addSlot(date, slot)); } catch (Semester.DateNotFoundException dnfe) { return new CommandResult(MESSAGE_FAIL_OUT_OF_BOUNDS); } @@ -75,24 +79,24 @@ public CommandResult execute() { return new CommandResult(MESSAGE_SUCCESS_NO_CHANGE); } else { return new CommandResult(String.format(MESSAGE_SUCCESS, dates.size(), - craftSuccessMessage(dates, slot))); + craftSuccessMessage(days, slot))); } } /** * Craft success message. */ - private String craftSuccessMessage(Set dates, Slot slot) { + public static String craftSuccessMessage(HashMap days, Slot slot) { StringBuilder sb = new StringBuilder(); sb.append("On dates:"); - for (LocalDate date : dates) { + for (Map.Entry day : days.entrySet()) { sb.append("\n\t"); - sb.append(planner.getSemester().getDays().get(date).getType()); + sb.append(day.getValue().getType()); sb.append(", "); - sb.append(date.toString()); + sb.append(day.getKey().toString()); sb.append(", "); - sb.append(date.getDayOfWeek().toString()); + sb.append(day.getKey().getDayOfWeek().toString()); } sb.append("\n\n"); diff --git a/src/planmysem/commands/DeleteCommand.java b/src/planmysem/commands/DeleteCommand.java index f09479393..22c8a1a8f 100644 --- a/src/planmysem/commands/DeleteCommand.java +++ b/src/planmysem/commands/DeleteCommand.java @@ -14,7 +14,7 @@ import planmysem.data.semester.Day; import planmysem.data.slot.ReadOnlySlot; import planmysem.data.slot.Slot; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * Adds a person to the address book. @@ -39,7 +39,7 @@ public class DeleteCommand extends Command { public static final String MESSAGE_FAIL_ILLEGAL_VALUE = MESSAGE_SUCCESS_NO_CHANGE + " Illegal characters were detected."; - private final Set tags = new HashSet<>(); + private final Set tags = new HashSet<>(); /** * Convenience constructor using raw values. @@ -133,7 +133,7 @@ private String craftSelectedMessage() { StringBuilder sb = new StringBuilder(); sb.append("Selected Slots containing tags: \n"); - for (TagP tag : tags) { + for (Tag tag : tags) { sb.append("\t"); sb.append(tag.toString()); sb.append("\n"); diff --git a/src/planmysem/commands/EditCommand.java b/src/planmysem/commands/EditCommand.java index 436d4b8ed..9b7c43010 100644 --- a/src/planmysem/commands/EditCommand.java +++ b/src/planmysem/commands/EditCommand.java @@ -16,7 +16,7 @@ import planmysem.data.semester.Semester; import planmysem.data.slot.ReadOnlySlot; import planmysem.data.slot.Slot; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * Adds a person to the address book. @@ -48,8 +48,8 @@ public class EditCommand extends Command { private final String name; private final String location; private final String description; - private final Set tags = new HashSet<>(); - private final Set newTags = new HashSet<>(); + private final Set tags = new HashSet<>(); + private final Set newTags = new HashSet<>(); /** * Convenience constructor using raw values. @@ -64,7 +64,9 @@ public EditCommand(String name, LocalTime startTime, int duration, String locati this.name = name; this.location = location; this.description = description; - this.tags.addAll(Utils.parseTags(tags)); + if (tags != null) { + this.tags.addAll(Utils.parseTags(tags)); + } if (newTags != null) { this.newTags.addAll(Utils.parseTags(newTags)); } @@ -205,7 +207,7 @@ private String craftSelectedMessage() { StringBuilder sb = new StringBuilder(); sb.append("Selected Slots containing tags: \n"); - for (TagP tag : tags) { + for (Tag tag : tags) { sb.append("\t"); sb.append(tag.toString()); sb.append("\n"); diff --git a/src/planmysem/commands/ExportCommand.java b/src/planmysem/commands/ExportCommand.java index 06c5a1243..f4235c9da 100644 --- a/src/planmysem/commands/ExportCommand.java +++ b/src/planmysem/commands/ExportCommand.java @@ -19,7 +19,6 @@ public class ExportCommand extends Command { @Override public CommandResult execute() { AdaptedSemester semester = new AdaptedSemester(planner.getSemester()); - System.out.println(semester.toString()); try { BufferedWriter writer = new BufferedWriter(new FileWriter("PlanMySem.ics")); writer.write(semester.toString()); diff --git a/src/planmysem/commands/FindCommand.java b/src/planmysem/commands/FindCommand.java index 07a0f823d..099b2384e 100644 --- a/src/planmysem/commands/FindCommand.java +++ b/src/planmysem/commands/FindCommand.java @@ -1,27 +1,33 @@ package planmysem.commands; +import java.time.LocalDate; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; +import java.util.regex.Pattern; + +import javafx.util.Pair; import planmysem.data.semester.Day; +import planmysem.data.slot.ReadOnlySlot; import planmysem.data.slot.Slot; /** - * Finds all slots and tags in planner whose name directly matches any of the argument keywords. + * Finds all slots in planner whose name contains the argument keyword. * Keyword matching is case sensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Finds all slots and tags which directly " - + "matches the specified keywords (case-sensitive).\n\t" + public static final String COMMAND_WORD_SHORT = "f"; + public static final String MESSAGE_SUCCESS = "%1$s Slots listed.\n%2$s"; + public static final String MESSAGE_SUCCESS_NONE = "0 Slots listed.\n"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Finds all slots whose name " + + "contains the specified keywords (case-sensitive).\n\t" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n\t" - + "Example: " + COMMAND_WORD + " CS2113T"; + + "Example: " + COMMAND_WORD + "CS2113T"; private final Set keywords; @@ -29,37 +35,55 @@ public FindCommand(Set keywords) { this.keywords = keywords; } - /** - * Returns copy of keywords in this command. - */ - public Set getKeywords() { - return new HashSet<>(keywords); - } - @Override public CommandResult execute() { - String result = getSlotsWithTag(keywords).stream().map(Object::toString) - .collect(Collectors.joining(", ")); - return new CommandResult(result); - } + final List> relevantSlots = new ArrayList<>(); + List matchedSlots = new ArrayList<>(); + LocalDate date; - /** - * Retrieve all slot in the semesters of the planner whose slots contain some of the specified keywords. - * - * @param keywords for searching - * @return list of persons found - */ - private List getSlotsWithTag(Set keywords) { - List test = new ArrayList<>(); - for (Day days : planner.getSemester().getDays().values()) { - for (Slot slots : days.getSlots()) { + for (Map.Entry entry : planner.getSemester().getDays().entrySet()) { + for (Slot slots : entry.getValue().getSlots()) { for (String keyword : keywords) { - if (slots.getName().value.equalsIgnoreCase(keyword)) { - test.add(slots); + if (Pattern.matches(".*" + keyword + ".*", slots.getName().value) + && !matchedSlots.contains(slots)) { + matchedSlots.add(slots); + date = entry.getKey(); + relevantSlots.add(new Pair<>(date, slots)); } } } } - return test; + + if (matchedSlots.isEmpty()) { + return new CommandResult(MESSAGE_SUCCESS_NONE); + } + setData(this.planner, relevantSlots); + + return new CommandResult(String.format(MESSAGE_SUCCESS, matchedSlots.size(), + craftSuccessMessage(relevantSlots))); + } + + /** + * Craft success message. + */ + private String craftSuccessMessage(List> result) { + int count = 1; + StringBuilder sb = new StringBuilder(); + + for (Pair pair : result) { + sb.append("\n"); + sb.append(count + ".\t"); + sb.append("Name: "); + sb.append(pair.getValue().getName().toString()); + sb.append(",\n\t"); + sb.append("Date: "); + sb.append(pair.getKey().toString()); + sb.append(",\n\t"); + sb.append("Start Time: "); + sb.append(pair.getValue().getStartTime()); + sb.append("\n"); + count++; + } + return sb.toString(); } } diff --git a/src/planmysem/commands/ListCommand.java b/src/planmysem/commands/ListCommand.java index 9b40505af..16f2c751a 100644 --- a/src/planmysem/commands/ListCommand.java +++ b/src/planmysem/commands/ListCommand.java @@ -11,7 +11,7 @@ import planmysem.data.slot.Slot; /** - * Finds and lists all slots in planner whose name contains any of the argument keywords. + * Displays a list of all slots in the planner whose name matches the argument keyword. * Keyword matching is case sensitive. */ public class ListCommand extends Command { @@ -21,9 +21,9 @@ public class ListCommand extends Command { public static final String MESSAGE_SUCCESS = "%1$s Slots listed.\n%2$s"; public static final String MESSAGE_SUCCESS_NONE = "0 Slots listed.\n"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all slots." - + "\n\tOptional Parameters: [past] [next] [all]" - + "\n\tDefault: list all" - + "\n\tExample: " + COMMAND_WORD + " CS1010 tutorial lab"; + //+ "\n\tOptional Parameters: [past] [next] [all]" + //+ "\n\tDefault: list all" + + "\n\tExample: " + COMMAND_WORD + " CS1010"; private final String name; diff --git a/src/planmysem/commands/ViewCommand.java b/src/planmysem/commands/ViewCommand.java new file mode 100644 index 000000000..e5796f1e0 --- /dev/null +++ b/src/planmysem/commands/ViewCommand.java @@ -0,0 +1,137 @@ +package planmysem.commands; + +import java.time.LocalDate; +import java.util.HashMap; + +import planmysem.data.semester.Day; +import planmysem.data.semester.Semester; + +/** + * Adds a person to the address book. + */ +public class ViewCommand extends Command { + + public static final String COMMAND_WORD = "view"; + public static final String COMMAND_WORD_SHORT = "v"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View month/week/day view or all details of planner." + + "\n\tParameters: " + + "\n\t\tMandatory: [viewType] [specifier]" + + "\n\tExample 1: " + COMMAND_WORD + + " month" + + "\n\tExample 2: " + COMMAND_WORD + + " week 7" + + "\n\tExample 3: " + COMMAND_WORD + + " week recess" + + "\n\tExample 4: " + COMMAND_WORD + + " day 01/03/2019" + + "\n\tExample 5: " + COMMAND_WORD + + " all"; + + private final String viewArgs; + + public ViewCommand(String viewArgs) { + this.viewArgs = viewArgs; + } + + @Override + public CommandResult execute() { + String viewType; + //String viewSpecifier; + final Semester currentSemester = planner.getSemester(); + String output = null; + + if ("all".equals(viewArgs)) { + //TODO: print all planner details + output = "all"; + } else if ("month".equals(viewArgs)) { + output = displayMonthView(currentSemester); + } else { + viewType = viewArgs.split(" ")[0]; + //viewSpecifier = viewArgs.split(" ")[1]; + + switch (viewType) { + case "month": + //TODO: month view + break; + case "week": + //TODO: week view + break; + case "day": + //TODO: day view + break; + default: + break; + } + } + + return new CommandResult(output); + } + + /** + * Display all months for the semester. + */ + private String displayMonthView(Semester currentSemester) { + HashMap allDays = currentSemester.getDays(); + LocalDate semesterStartDate = currentSemester.getStartDate(); + LocalDate semesterEndDate = currentSemester.getEndDate(); + int year = semesterStartDate.getYear(); + LocalDate firstDayOfMonth = semesterStartDate.withDayOfMonth(1); + int spaces = firstDayOfMonth.getDayOfWeek().getValue(); + int lastMonthOfSem = semesterEndDate.getMonthValue(); + StringBuilder sb = new StringBuilder(); + + String[] months = {"", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"}; + + int[] days = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + for (int m = 1; m <= lastMonthOfSem; m++) { + // Set number of days in February to 29 if it is a leap year. + if ((((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) && m == 2) { + days[m] = 29; + } + + // Print calendar header. + sb.append(" " + months[m] + " " + year + "\n"); + sb.append("_____________________________________\n"); + sb.append(" Sun Mon Tue Wed Thu Fri Sat\n"); + + // Print spaces required for the start of a month. + spaces = (days[m - 1] + spaces) % 7; + for (int i = 0; i < spaces; i++) { + sb.append(" "); + } + // Print the days in the month. + for (int i = 1; i <= days[m]; i++) { + sb.append(String.format(" %3d", i)); + if (((i + spaces) % 7 == 0)) { + Day tempDay = allDays.get(LocalDate.of(year, m, i)); + String weekType = ""; + if (tempDay != null) { + weekType = tempDay.getType(); + } + sb.append(" | " + weekType + "\n"); + } + if (i == days[m]) { + LocalDate tempDate = LocalDate.of(year, m, i); + Day tempDay = allDays.get(tempDate); + String weekType = ""; + int extraSpaces = 6 - (tempDate.getDayOfWeek().getValue() % 7); + for (int j = 0; j < extraSpaces; j++) { + sb.append(" "); + } + if (tempDay != null) { + weekType = tempDay.getType(); + } + sb.append(" | " + weekType + "\n"); + } + } + + sb.append("\n"); + } + + return sb.toString(); + } + +} diff --git a/src/planmysem/common/Messages.java b/src/planmysem/common/Messages.java index f8232bba6..1882aa075 100644 --- a/src/planmysem/common/Messages.java +++ b/src/planmysem/common/Messages.java @@ -6,6 +6,7 @@ public class Messages { public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL = "Invalid command format! \n%1$s\n\n%2$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; public static final String MESSAGE_INVALID_SLOT_DISPLAYED_INDEX = "The slot index provided is invalid"; public static final String MESSAGE_PERSON_NOT_IN_ADDRESSBOOK = "Person could not be found in address book"; @@ -16,4 +17,14 @@ public class Messages { + "java Main [STORAGE_FILE_PATH]"; public static final String MESSAGE_WELCOME = "Welcome to PlanMySem!"; public static final String MESSAGE_USING_STORAGE_FILE = "Using storage file : %1$s"; + + public static final String MESSAGE_INVALID_DATE = "Date have to be in either these two formats:" + + "\n\tIn the form of \"dd-mm\". e.g. \"01-01\"" + + "\n\tIn the form of \"dd-mm-yyyy\". e.g. \"01-01-2019\"" + + "\n\tOr perhaps target the next day of week. e.g. \"Monday\", \"mon\", \"1\""; + + public static final String MESSAGE_INVALID_TIME = "Time have to be in either these two formats:" + + "\n\t24-Hour in the form of “hh:mm”. e.g. \"23:00\"" + + "\n\t12-Hour in the form of `hh:mm+AM|PM`. e.g. \"12:30am\"" + + "\n\tOr perhaps type a duration in minutes. e.g. \"60\" to represent 60 minutes"; } diff --git a/src/planmysem/common/Utils.java b/src/planmysem/common/Utils.java index 90ff5e57e..edc500fcc 100644 --- a/src/planmysem/common/Utils.java +++ b/src/planmysem/common/Utils.java @@ -14,7 +14,7 @@ import java.util.regex.Pattern; import planmysem.data.exception.IllegalValueException; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * Utility methods @@ -25,7 +25,7 @@ public class Utils { public static final Pattern DATE_FORMAT_NO_YEAR = Pattern.compile("(0?[1-9]|[12][0-9]|3[01])-(0?[1-9]|1[012])"); public static final Pattern TWELVE_HOUR_FORMAT = - Pattern.compile("(1[012]|[1-9]):[0-5][0-9](\\s)?(?i)(am|pm)"); + Pattern.compile("(1[012]|[1-9]):[0-5][0-9](\\s)?(AM|PM)"); public static final Pattern TWENTY_FOUR_HOUR_FORMAT = Pattern.compile("([01]?[0-9]|2[0-3]):[0-5][0-9]"); public static final Pattern INTEGER_FORMAT = @@ -63,10 +63,10 @@ public static boolean elementsAreUnique(Collection items) { */ public static int parseDay(String unknown) { if (unknown.trim().isEmpty()) { - return 0; + return -1; } - String day = unknown.toLowerCase(); - int result = 0; + String day = unknown.trim().toLowerCase(); + int result = -1; switch (day) { case "monday": case "mon": @@ -98,8 +98,20 @@ public static int parseDay(String unknown) { result = 5; break; + case "saturday": + case "sat": + case "6": + result = 6; + break; + + case "sunday": + case "sun": + case "7": + result = 7; + break; + default: - result = 0; + result = -1; break; } @@ -110,7 +122,7 @@ public static int parseDay(String unknown) { * Parse String LocalDate. */ public static LocalDate parseDate(String date) { - if (DATE_FORMAT.matcher(date).matches()) { + if (date != null && DATE_FORMAT.matcher(date).matches()) { return LocalDate.parse(date, DateTimeFormatter.ofPattern("d-MM-yyyy")); } else if (DATE_FORMAT_NO_YEAR.matcher(date).matches()) { return LocalDate.parse(date + "-" + Year.now().getValue(), DateTimeFormatter.ofPattern("d-MM-yyyy")); @@ -123,20 +135,20 @@ public static LocalDate parseDate(String date) { * Parse String to 12 hour or 24 hour time format. */ public static LocalTime parseTime(String time) { - if (TWELVE_HOUR_FORMAT.matcher(time).matches()) { - return LocalTime.parse(time, DateTimeFormatter.ofPattern("hh:mm am")); + LocalTime result = null; + if (time != null && TWELVE_HOUR_FORMAT.matcher(time).matches()) { + result = LocalTime.parse(time, DateTimeFormatter.ofPattern("h[h]:mm a")); } else if (TWENTY_FOUR_HOUR_FORMAT.matcher(time).matches()) { - return LocalTime.parse(time, DateTimeFormatter.ofPattern("kk:mm")); + result = LocalTime.parse(time, DateTimeFormatter.ofPattern("H[H]:mm")); } - - return null; + return result; } /** * Parse string into integer. */ public static int parseInteger(String value) { - if (INTEGER_FORMAT.matcher(value).matches()) { + if (value != null && INTEGER_FORMAT.matcher(value).matches()) { try { return Integer.parseInt(value); } catch (NumberFormatException nfe) { @@ -152,10 +164,10 @@ public static int parseInteger(String value) { * * @throws IllegalValueException if any of the raw values are invalid */ - public static Set parseTags(Set tags) throws IllegalValueException { - Set tagSet = new HashSet<>(); + public static Set parseTags(Set tags) throws IllegalValueException { + Set tagSet = new HashSet<>(); for (String tag : tags) { - tagSet.add(new TagP(tag)); + tagSet.add(new Tag(tag)); } return tagSet; } diff --git a/src/planmysem/data/Planner.java b/src/planmysem/data/Planner.java index ef22434c3..473f92451 100644 --- a/src/planmysem/data/Planner.java +++ b/src/planmysem/data/Planner.java @@ -18,7 +18,7 @@ import planmysem.data.semester.Semester; import planmysem.data.slot.ReadOnlySlot; import planmysem.data.slot.Slot; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * Represents the entire Planner. Contains the data of the Planner. @@ -42,10 +42,6 @@ public Planner(Semester semester) { this.semester = new Semester(semester); } - public static Planner empty() { - return new Planner(); - } - /** * Generates current semester based on current date. * As long as the current date falls within a semester, the generated semester is always the same. @@ -299,8 +295,8 @@ public void addDay(LocalDate date, Day day) throws Semester.DuplicateDayExceptio * Adds a slot to the Planner. * */ - public void addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { - semester.addSlot(date, slot); + public Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { + return semester.addSlot(date, slot); } /** @@ -311,7 +307,7 @@ public void addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundExcep */ public void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, LocalTime startTime, int duration, String name, String location, - String description, Set tags) + String description, Set tags) throws Semester.DateNotFoundException, IllegalValueException { semester.editSlot(targetDate, targetSlot, date, startTime, duration, name, location, description, tags); } diff --git a/src/planmysem/data/semester/Day.java b/src/planmysem/data/semester/Day.java index 9221645c2..46f8b2af4 100644 --- a/src/planmysem/data/semester/Day.java +++ b/src/planmysem/data/semester/Day.java @@ -3,6 +3,7 @@ import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; +import java.util.Objects; import planmysem.data.exception.IllegalValueException; import planmysem.data.slot.ReadOnlySlot; @@ -102,6 +103,19 @@ public ArrayList getSlots() { return slots; } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ReadOnlyDay // instanceof handles nulls + && this.isSameStateAs((ReadOnlyDay) other)); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(dayOfWeek, type, slots); + } + /** * Signals that an operation targeting a specified slot in the list would fail because * there is no such matching slot in the list. diff --git a/src/planmysem/data/semester/ReadOnlyDay.java b/src/planmysem/data/semester/ReadOnlyDay.java index 226ecb5a8..8f34daade 100644 --- a/src/planmysem/data/semester/ReadOnlyDay.java +++ b/src/planmysem/data/semester/ReadOnlyDay.java @@ -17,4 +17,17 @@ public interface ReadOnlyDay { String getType(); ArrayList getSlots(); + + /** + * Returns true if the values inside this object is same as those of the other + * (Note: interfaces cannot override .equals) + */ + default boolean isSameStateAs(ReadOnlyDay other) { + return other == this // short circuit if same object + || (other != null // this is first to avoid NPE below + && other.getDayOfWeek().equals(this.getDayOfWeek()) // state checks here onwards + && other.getDay().equals(this.getDay()) + && other.getType().equals(this.getType()) + && other.getSlots().equals(this.getSlots())); + } } diff --git a/src/planmysem/data/semester/Semester.java b/src/planmysem/data/semester/Semester.java index 943b77cfb..d5296a10b 100644 --- a/src/planmysem/data/semester/Semester.java +++ b/src/planmysem/data/semester/Semester.java @@ -5,13 +5,14 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import planmysem.data.exception.DuplicateDataException; import planmysem.data.exception.IllegalValueException; import planmysem.data.slot.ReadOnlySlot; import planmysem.data.slot.Slot; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * A list of days. Does not allow null elements or duplicates. @@ -96,11 +97,12 @@ public void addDay(LocalDate date, Day day) throws DuplicateDayException { * * @throws DateNotFoundException if a date is not found in the semester. */ - public void addSlot(LocalDate date, Slot slot) throws DateNotFoundException { + public Day addSlot(LocalDate date, Slot slot) throws DateNotFoundException { if (date == null || (date.isBefore(startDate) || date.isAfter(endDate))) { throw new DateNotFoundException(); } days.get(date).addSlot(slot); + return days.get(date); } /** @@ -110,7 +112,7 @@ public void addSlot(LocalDate date, Slot slot) throws DateNotFoundException { * @throws IllegalValueException if a targetDate is not found in the semester. */ public void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, LocalTime startTime, - int duration, String name, String location, String description, Set tags) + int duration, String name, String location, String description, Set tags) throws DateNotFoundException, IllegalValueException { if (targetDate == null || (targetDate.isBefore(startDate) || targetDate.isAfter(endDate))) { throw new DateNotFoundException(); @@ -251,6 +253,28 @@ public Set getExamDays() { return examDays; } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Semester // instanceof handles nulls + && this.name.equals(((Semester) other).name) + && this.academicYear.equals(((Semester) other).academicYear) + && this.days.equals(((Semester) other).days) + && this.startDate.equals(((Semester) other).startDate) + && this.endDate.equals(((Semester) other).endDate) + && this.noOfWeeks == (((Semester) other).noOfWeeks) + && this.recessDays.equals(((Semester) other).recessDays) + && this.readingDays.equals(((Semester) other).readingDays) + && this.normalDays.equals(((Semester) other).normalDays) + && this.examDays.equals(((Semester) other).examDays)); + } + + @Override + public int hashCode() { + return Objects.hash(name, academicYear, days, startDate, endDate, noOfWeeks, + recessDays, readingDays, normalDays, examDays); + } + /** * Signals that an operation would have violated the 'no duplicates' property. */ @@ -266,5 +290,4 @@ protected DuplicateDayException() { */ public static class DateNotFoundException extends Exception { } - } diff --git a/src/planmysem/data/slot/Location.java b/src/planmysem/data/slot/Location.java index 4ac5532f2..9f7ebe1ea 100644 --- a/src/planmysem/data/slot/Location.java +++ b/src/planmysem/data/slot/Location.java @@ -40,8 +40,8 @@ public String toString() { @Override public boolean equals(Object other) { return other == this // short circuit if same object - || (other instanceof Description // instanceof handles nulls - && this.value.equals(((Description) other).value)); // state check + || (other instanceof Location // instanceof handles nulls + && this.value.equals(((Location) other).value)); // state check } @Override diff --git a/src/planmysem/data/slot/ReadOnlySlot.java b/src/planmysem/data/slot/ReadOnlySlot.java index 168992609..1409b7ee7 100644 --- a/src/planmysem/data/slot/ReadOnlySlot.java +++ b/src/planmysem/data/slot/ReadOnlySlot.java @@ -4,7 +4,7 @@ import java.util.Set; import planmysem.common.Utils; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * A read-only immutable interface for a Slot in the Planner. @@ -21,7 +21,7 @@ public interface ReadOnlySlot { * The returned {@code Set} is a deep copy of the internal {@code Set}, * changes on the returned list will not affect the person's internal tags. */ - Set getTags(); + Set getTags(); /** * Returns true if the values inside this object is same as @@ -33,7 +33,9 @@ default boolean isSameStateAs(ReadOnlySlot other) { && other.getName().equals(this.getName()) // state checks here onwards && other.getLocation().equals(this.getLocation()) && other.getDescription().equals(this.getDescription()) - && other.getStartTime().equals(this.getStartTime())); + && other.getStartTime().equals(this.getStartTime()) + && other.getDuration() == this.getDuration() + && other.getTags().equals(this.getTags())); } /** @@ -63,7 +65,7 @@ default String getAsTextShowAll() { sb.append(getDuration()); sb.append("\n\tTags: "); - for (TagP tag : getTags()) { + for (Tag tag : getTags()) { sb.append("\n\t\t"); sb.append(tag.toString()); } diff --git a/src/planmysem/data/slot/Slot.java b/src/planmysem/data/slot/Slot.java index f3d35e952..c1e510355 100644 --- a/src/planmysem/data/slot/Slot.java +++ b/src/planmysem/data/slot/Slot.java @@ -7,14 +7,14 @@ import planmysem.common.Utils; import planmysem.data.exception.IllegalValueException; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * Represents a Person in the address book. * Guarantees: details are present and not null, field values are validated. */ public class Slot implements ReadOnlySlot { - private final Set tags = new HashSet<>(); + private final Set tags = new HashSet<>(); private Name name; private Location location; private Description description; @@ -25,7 +25,7 @@ public class Slot implements ReadOnlySlot { * Assumption: Every field must be present and not null. */ public Slot(Name name, Location location, Description description, - LocalTime startTime, LocalTime endTime, Set tags) { + LocalTime startTime, LocalTime endTime, Set tags) { this.name = name; this.location = location; this.description = description; @@ -40,7 +40,7 @@ public Slot(Name name, Location location, Description description, * Assumption: Every field must be present and not null. */ public Slot(Name name, Location location, Description description, - LocalTime startTime, int duration, Set tags) { + LocalTime startTime, int duration, Set tags) { this.name = name; this.location = location; this.description = description; @@ -79,7 +79,7 @@ public void setLocation(String value) throws IllegalValueException { if (value == null) { return; } - if (value.equals("")) { + if ("".equals(value)) { location = new Location(null); } else { location = new Location(value); @@ -141,14 +141,14 @@ public LocalTime getStartTime() { } @Override - public Set getTags() { + public Set getTags() { return tags; } /** * Replaces this slot's tags with the tags in {@code replacement}. */ - public void setTags(Set tags) { + public void setTags(Set tags) { this.tags.clear(); this.tags.addAll(tags); } @@ -163,7 +163,7 @@ public boolean equals(Object other) { @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, location, description, startTime, tags); + return Objects.hash(name, location, description, startTime, duration, tags); } @Override diff --git a/src/planmysem/data/tag/Tag.java b/src/planmysem/data/tag/Tag.java index 9c78c0d2f..5bdda71d2 100644 --- a/src/planmysem/data/tag/Tag.java +++ b/src/planmysem/data/tag/Tag.java @@ -3,51 +3,51 @@ import planmysem.data.exception.IllegalValueException; /** - * Represents a Tag in the address book. + * Represents a Tag in the Planner. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ public class Tag { - public static final String MESSAGE_TAG_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String TAG_VALIDATION_REGEX = "\\p{Alnum}+"; + private static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; + private static final String VALIDATION_REGEX = ".+"; - public final String tagName; + public final String value; /** - * Validates given tag name. + * Validates given value. * * @throws IllegalValueException if the given tag name string is invalid. */ public Tag(String name) throws IllegalValueException { - name = name.trim(); - if (!isValidTagName(name)) { - throw new IllegalValueException(MESSAGE_TAG_CONSTRAINTS); + String value = name.trim(); + if (!isValidTagName(value)) { + throw new IllegalValueException(MESSAGE_CONSTRAINTS); } - this.tagName = name; + this.value = value; } /** - * Returns true if a given string is a valid tag name. + * Returns true if a given value is a valid. */ - public static boolean isValidTagName(String test) { - return test.matches(TAG_VALIDATION_REGEX); + private static boolean isValidTagName(String test) { + return test.matches(VALIDATION_REGEX); } @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof Tag // instanceof handles nulls - && this.tagName.equals(((Tag) other).tagName)); // state check + && this.value.equals(((Tag) other).value)); // state check } @Override public int hashCode() { - return tagName.hashCode(); + return value.hashCode(); } @Override public String toString() { - return '[' + tagName + ']'; + return value; } } diff --git a/src/planmysem/data/tag/TagP.java b/src/planmysem/data/tag/TagP.java deleted file mode 100644 index 48597a661..000000000 --- a/src/planmysem/data/tag/TagP.java +++ /dev/null @@ -1,53 +0,0 @@ -package planmysem.data.tag; - -import planmysem.data.exception.IllegalValueException; - -/** - * Represents a Tag in the Planner. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class TagP { - - private static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - private static final String VALIDATION_REGEX = ".+"; - - public final String value; - - /** - * Validates given value. - * - * @throws IllegalValueException if the given tag name string is invalid. - */ - public TagP(String name) throws IllegalValueException { - String value = name.trim(); - if (!isValidTagName(value)) { - throw new IllegalValueException(MESSAGE_CONSTRAINTS); - } - this.value = value; - } - - /** - * Returns true if a given value is a valid. - */ - private static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof TagP // instanceof handles nulls - && this.value.equals(((TagP) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - @Override - public String toString() { - return '[' + value + ']'; - } - -} diff --git a/src/planmysem/logic/Logic.java b/src/planmysem/logic/Logic.java index 1d5ae4620..5c43625ed 100644 --- a/src/planmysem/logic/Logic.java +++ b/src/planmysem/logic/Logic.java @@ -1,6 +1,7 @@ package planmysem.logic; import java.time.LocalDate; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -24,7 +25,7 @@ public class Logic { /** * The list of Slots shown to the user most recently. */ - private List> lastShownSlots; + private List> lastShownSlots = Collections.emptyList(); public Logic() throws Exception { setStorage(initializeStorage()); @@ -96,7 +97,6 @@ private CommandResult execute(Command command) throws Exception { /** * Updates the {@link #lastShownSlots} if the result contains a list of Days. - * TODO: */ private void recordResult(CommandResult result) { final Optional>> slots = result.getRelevantSlots(); diff --git a/src/planmysem/parser/Parser.java b/src/planmysem/parser/Parser.java index 3cdf637bd..d886c5668 100644 --- a/src/planmysem/parser/Parser.java +++ b/src/planmysem/parser/Parser.java @@ -1,6 +1,9 @@ package planmysem.parser; import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL; +import static planmysem.common.Messages.MESSAGE_INVALID_DATE; +import static planmysem.common.Messages.MESSAGE_INVALID_TIME; import java.time.LocalDate; import java.time.LocalTime; @@ -18,10 +21,12 @@ import planmysem.commands.DeleteCommand; import planmysem.commands.EditCommand; import planmysem.commands.ExitCommand; +import planmysem.commands.ExportCommand; import planmysem.commands.FindCommand; import planmysem.commands.HelpCommand; import planmysem.commands.IncorrectCommand; import planmysem.commands.ListCommand; +import planmysem.commands.ViewCommand; import planmysem.common.Utils; import planmysem.data.exception.IllegalValueException; @@ -30,8 +35,6 @@ */ public class Parser { - public static final Pattern PERSON_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)"); - public static final Pattern KEYWORDS_ARGS_FORMAT = Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace @@ -51,7 +54,6 @@ public class Parser { private static final String PARAMETER_NEW_DESCRIPTION = "ndes"; private static final String PARAMETER_NEW_TAG = "nt"; - /** * Used for initial separation of command word and args. */ @@ -86,16 +88,27 @@ public Command parseCommand(String userInput) { case DeleteCommand.COMMAND_WORD_SHORT: return prepareDelete(arguments); + case FindCommand.COMMAND_WORD: + case FindCommand.COMMAND_WORD_SHORT: + return prepareFind(arguments); + case ListCommand.COMMAND_WORD: case ListCommand.COMMAND_WORD_SHORT: return prepareList(arguments); + case ViewCommand.COMMAND_WORD: + case ViewCommand.COMMAND_WORD_SHORT: + return prepareView(arguments); + case ClearCommand.COMMAND_WORD: return new ClearCommand(); case ExitCommand.COMMAND_WORD: return new ExitCommand(); + case ExportCommand.COMMAND_WORD: + return new ExportCommand(); + case HelpCommand.COMMAND_WORD: // Fallthrough default: @@ -125,20 +138,22 @@ private Command prepareAdd(String args) { // Either date or day must be present String dateOrDay = getFirstInSet(arguments.get(PARAMETER_DATE_OR_DAY)); - int day = Utils.parseDay(dateOrDay); - LocalDate date = null; - if (day == 0) { - date = Utils.parseDate(dateOrDay); + int day = -1; + LocalDate date = Utils.parseDate(dateOrDay); + if (date == null) { + day = Utils.parseDay(dateOrDay); } - if (day == 0 && date == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + if (day == -1 && date == null) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_DATE)); } // Start time is mandatory String stringStartTime = getFirstInSet(arguments.get(PARAMETER_START_TIME)); LocalTime startTime = Utils.parseTime(stringStartTime); if (startTime == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); } // determine if "end time" is a duration or time @@ -148,7 +163,8 @@ private Command prepareAdd(String args) { if (duration == -1) { LocalTime endTime = Utils.parseTime(stringEndTime); if (endTime == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); } duration = Utils.getDuration(startTime, endTime); } @@ -168,6 +184,8 @@ private Command prepareAdd(String args) { AddCommand.MESSAGE_USAGE)); } } + } else { + tags = new HashSet<>(); } // Recurrences is not mandatory @@ -296,6 +314,25 @@ private Command prepareDelete(String args) { } } + /** + * Parses arguments in the context of the find person command. + * + * @param args partial args string + * @return the arguments sorted by its relevant options + */ + private Command prepareFind(String args) { + final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindCommand.MESSAGE_USAGE)); + } + + // keywords delimited by whitespace + final String[] keywords = matcher.group("keywords").split("\\s+"); + final Set keywordSet = new HashSet<>(Arrays.asList(keywords)); + return new FindCommand(keywordSet); + } + /** * Parses arguments in the context of the list command. * @@ -317,50 +354,28 @@ private Command prepareList(String args) { * @param args full command args string * @return the prepared command */ - // private Command prepareView(String args) { - // - // try { - // final int targetIndex = parseArgsAsDisplayedIndex(args); - // return new ViewCommand(targetIndex); - // } catch (ParseException | NumberFormatException e) { - // return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, - // ViewCommand.MESSAGE_USAGE)); - // } - // } - - /** - * Parses the given arguments string as a single index number. - * - * @param args arguments string to parse as index number - * @return the parsed index number - * @throws ParseException if no region of the args string could be found for the index - * @throws NumberFormatException the args string region is not a valid number - */ - private int parseArgsAsDisplayedIndex(String args) throws ParseException, NumberFormatException { - final Matcher matcher = PERSON_INDEX_ARGS_FORMAT.matcher(args.trim()); - if (!matcher.matches()) { - throw new ParseException("Could not find index number to parse"); + private Command prepareView(String args) { + if (args == null || args.trim().isEmpty()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE)); } - return Integer.parseInt(matcher.group("targetIndex")); - } - /** - * Parses arguments in the context of the find person command. - * - * @param args partial args string - * @return the arguments sorted by its relevant options - */ - private Command prepareFind(String args) { - final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim()); - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, - FindCommand.MESSAGE_USAGE)); + String[] viewArgs = args.split(" "); + if ("all".equals(viewArgs[1]) && viewArgs.length == 2) { + return new ViewCommand(viewArgs[1]); + } else if ("month".equals(viewArgs[1]) && viewArgs.length == 2) { + return new ViewCommand(viewArgs[1]); + } else if ("month".equals(viewArgs[1]) && viewArgs.length == 3) { + //TODO: ensure month arguments + return new ViewCommand(viewArgs[1] + " " + viewArgs[2]); + } else if ("week".equals(viewArgs[1]) && viewArgs.length == 3) { + //TODO: ensure week arguments + return new ViewCommand(viewArgs[1] + " " + viewArgs[2]); + } else if ("day".equals(viewArgs[1]) && viewArgs.length == 3) { + //TODO: ensure day arguments + return new ViewCommand(viewArgs[1] + " " + viewArgs[2]); } - // keywords delimited by whitespace - final String[] keywords = matcher.group("keywords").split("\\s+"); - final Set keywordSet = new HashSet<>(Arrays.asList(keywords)); - return new FindCommand(keywordSet); + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE)); } /** diff --git a/src/planmysem/storage/jaxb/AdaptedSemester.java b/src/planmysem/storage/jaxb/AdaptedSemester.java index d4bc5589c..1cce90d96 100644 --- a/src/planmysem/storage/jaxb/AdaptedSemester.java +++ b/src/planmysem/storage/jaxb/AdaptedSemester.java @@ -108,6 +108,11 @@ public Semester toModelType() throws IllegalValueException { final String academicYear = this.academicYear; final String startDate = this.startDate; final String endDate = this.endDate; + + if (startDate == null || endDate == null) { + throw new IllegalValueException(""); + } + final int noOfWeeks = this.noOfWeeks; final HashMap days = new HashMap<>(); diff --git a/src/planmysem/storage/jaxb/AdaptedSlot.java b/src/planmysem/storage/jaxb/AdaptedSlot.java index eff8c9405..dc5422bcb 100644 --- a/src/planmysem/storage/jaxb/AdaptedSlot.java +++ b/src/planmysem/storage/jaxb/AdaptedSlot.java @@ -15,7 +15,7 @@ import planmysem.data.slot.Name; import planmysem.data.slot.ReadOnlySlot; import planmysem.data.slot.Slot; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * JAXB-friendly adapted person data holder class. @@ -53,7 +53,7 @@ public AdaptedSlot(ReadOnlySlot source) { startTime = source.getStartTime().toString(); tags = new ArrayList<>(); - for (TagP tag : source.getTags()) { + for (Tag tag : source.getTags()) { tags.add(new AdaptedTag(tag)); } } @@ -83,7 +83,7 @@ public Slot toModelType() throws IllegalValueException { final LocalTime startTime = LocalTime.parse(this.startTime); final int duration = this.duration; - final Set tags = new HashSet<>(); + final Set tags = new HashSet<>(); for (AdaptedTag tag : this.tags) { tags.add(tag.toModelType()); } diff --git a/src/planmysem/storage/jaxb/AdaptedTag.java b/src/planmysem/storage/jaxb/AdaptedTag.java index 2da949bb2..9b39ad3cc 100644 --- a/src/planmysem/storage/jaxb/AdaptedTag.java +++ b/src/planmysem/storage/jaxb/AdaptedTag.java @@ -4,7 +4,7 @@ import planmysem.common.Utils; import planmysem.data.exception.IllegalValueException; -import planmysem.data.tag.TagP; +import planmysem.data.tag.Tag; /** * JAXB-friendly adapted tag data holder class. @@ -24,7 +24,7 @@ public AdaptedTag() { * * @param source future changes to this will not affect the created AdaptedTag */ - public AdaptedTag(TagP source) { + public AdaptedTag(Tag source) { value = source.value; } @@ -45,7 +45,7 @@ public boolean isAnyRequiredFieldMissing() { * * @throws IllegalValueException if there were any data constraints violated in the adapted person */ - public TagP toModelType() throws IllegalValueException { - return new TagP(value); + public Tag toModelType() throws IllegalValueException { + return new Tag(value); } } diff --git a/src/planmysem/ui/DarkTheme.css b/src/planmysem/ui/DarkTheme.css index 5b2b70656..1c619d1de 100644 --- a/src/planmysem/ui/DarkTheme.css +++ b/src/planmysem/ui/DarkTheme.css @@ -10,7 +10,7 @@ .text-area { -fx-background-color: black; -fx-control-inner-background: black; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "Lucida Console"; -fx-font-size: 10pt; -fx-padding: 5 5 5 5; } diff --git a/src/planmysem/ui/MainWindow.java b/src/planmysem/ui/MainWindow.java index 6d971c91d..08130860e 100644 --- a/src/planmysem/ui/MainWindow.java +++ b/src/planmysem/ui/MainWindow.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Optional; -import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; @@ -39,7 +38,7 @@ public void setMainApp(Stoppable mainApp) { * TODO: Add Javadoc comment. */ @FXML - void onCommand(ActionEvent event) { + private void onCommand() { try { String userCommandText = commandInput.getText(); CommandResult result = logic.execute(userCommandText); @@ -51,7 +50,7 @@ void onCommand(ActionEvent event) { clearCommandInput(); } catch (Exception e) { display(e.getMessage()); - throw new RuntimeException(e); + // throw new RuntimeException(e); } } diff --git a/test/data/StorageFileTest/InvalidData.txt b/test/data/StorageFileTest/InvalidData.txt index ad994ee9b..206e95798 100644 --- a/test/data/StorageFileTest/InvalidData.txt +++ b/test/data/StorageFileTest/InvalidData.txt @@ -1,6 +1,6 @@ - + data - + diff --git a/test/data/StorageFileTest/ValidData.txt b/test/data/StorageFileTest/ValidData.txt index fc6b00df6..b0bfb777b 100644 --- a/test/data/StorageFileTest/ValidData.txt +++ b/test/data/StorageFileTest/ValidData.txt @@ -1,17 +1,956 @@ - - - John Doe - 98765432 - johnd@gmail.com -
John street, block 123, #01-01
-
- - Betsy Crowe - 1234567 - betsycrowe@gmail.com -
Newgate Prison
- friend - criminal -
-
+ + + Sem 2 + AY2018/2019 + 2019-01-14 + 2019-05-12 + 17 + + + 2019-02-09 + + SATURDAY + Week 4 + + + + 2019-02-08 + + FRIDAY + Week 4 + + + + 2019-02-07 + + THURSDAY + Week 4 + + + + 2019-02-06 + + WEDNESDAY + Week 4 + + + + 2019-02-05 + + TUESDAY + Week 4 + + + + 2019-02-04 + + MONDAY + Week 4 + + + + 2019-02-03 + + SUNDAY + Week 3 + + + + 2019-02-02 + + SATURDAY + Week 3 + + + + 2019-02-01 + + FRIDAY + Week 3 + + + + 2019-01-31 + + THURSDAY + Week 3 + + + + 2019-01-30 + + WEDNESDAY + Week 3 + + + + 2019-04-30 + + TUESDAY + Examination Week + + + + 2019-04-20 + + SATURDAY + Week 13 + + + + 2019-04-21 + + SUNDAY + Week 13 + + + + 2019-04-22 + + MONDAY + Reading Week + + + + 2019-04-23 + + TUESDAY + Reading Week + + + + 2019-04-24 + + WEDNESDAY + Reading Week + + + + 2019-04-25 + + THURSDAY + Reading Week + + + + 2019-04-26 + + FRIDAY + Reading Week + + + + 2019-04-27 + + SATURDAY + Reading Week + + + + 2019-04-28 + + SUNDAY + Reading Week + + + + 2019-04-29 + + MONDAY + Examination Week + + + + 2019-02-19 + + TUESDAY + Week 6 + + + + 2019-02-18 + + MONDAY + Week 6 + + + + 2019-02-17 + + SUNDAY + Week 5 + + + + 2019-02-16 + + SATURDAY + Week 5 + + + + 2019-02-15 + + FRIDAY + Week 5 + + + + 2019-02-14 + + THURSDAY + Week 5 + + + + 2019-02-13 + + WEDNESDAY + Week 5 + + + + 2019-02-12 + + TUESDAY + Week 5 + + + + 2019-02-11 + + MONDAY + Week 5 + + + + 2019-02-10 + + SUNDAY + Week 4 + + + + 2019-05-01 + + WEDNESDAY + Examination Week + + + + 2019-05-02 + + THURSDAY + Examination Week + + + + 2019-05-03 + + FRIDAY + Examination Week + + + + 2019-05-04 + + SATURDAY + Examination Week + + + + 2019-05-05 + + SUNDAY + Examination Week + + + + 2019-05-06 + + MONDAY + Examination Week + + + + 2019-05-07 + + TUESDAY + Examination Week + + + + 2019-05-08 + + WEDNESDAY + Examination Week + + + + 2019-05-09 + + THURSDAY + Examination Week + + + + 2019-02-28 + + THURSDAY + Recess Week + + + + 2019-02-27 + + WEDNESDAY + Recess Week + + + + 2019-02-26 + + TUESDAY + Recess Week + + + + 2019-02-25 + + MONDAY + Recess Week + + + + 2019-02-24 + + SUNDAY + Week 6 + + + + 2019-02-23 + + SATURDAY + Week 6 + + + + 2019-02-22 + + FRIDAY + Week 6 + + + + 2019-02-21 + + THURSDAY + Week 6 + + + + 2019-02-20 + + WEDNESDAY + Week 6 + + + + 2019-05-10 + + FRIDAY + Examination Week + + + + 2019-05-11 + + SATURDAY + Examination Week + + + + 2019-03-01 + + FRIDAY + Recess Week + + + + 2019-03-02 + + SATURDAY + Recess Week + + + + 2019-03-03 + + SUNDAY + Recess Week + + + + 2019-03-04 + + MONDAY + Week 7 + + + + 2019-03-05 + + TUESDAY + Week 7 + + + + 2019-03-06 + + WEDNESDAY + Week 7 + + + + 2019-03-07 + + THURSDAY + Week 7 + + + + 2019-03-08 + + FRIDAY + Week 7 + + + + 2019-03-09 + + SATURDAY + Week 7 + + + + 2019-03-10 + + SUNDAY + Week 7 + + + + 2019-03-11 + + MONDAY + Week 8 + + + + 2019-03-12 + + TUESDAY + Week 8 + + + + 2019-03-13 + + WEDNESDAY + Week 8 + + + + 2019-03-14 + + THURSDAY + Week 8 + + + + 2019-03-15 + + FRIDAY + Week 8 + + + + 2019-03-16 + + SATURDAY + Week 8 + + + + 2019-03-17 + + SUNDAY + Week 8 + + + + 2019-03-18 + + MONDAY + Week 9 + + + + 2019-03-19 + + TUESDAY + Week 9 + + + + 2019-03-20 + + WEDNESDAY + Week 9 + + + + 2019-03-21 + + THURSDAY + Week 9 + + + + 2019-03-22 + + FRIDAY + Week 9 + + + + 2019-03-23 + + SATURDAY + Week 9 + + + + 2019-03-24 + + SUNDAY + Week 9 + + + + 2019-03-25 + + MONDAY + Week 10 + + + + 2019-03-26 + + TUESDAY + Week 10 + + + + 2019-03-27 + + WEDNESDAY + Week 10 + + + + 2019-03-28 + + THURSDAY + Week 10 + + + + 2019-03-29 + + FRIDAY + Week 10 + + + + 2019-01-19 + + SATURDAY + Week 1 + + + + 2019-01-18 + + FRIDAY + Week 1 + + + + 2019-01-17 + + THURSDAY + Week 1 + + + + 2019-01-16 + + WEDNESDAY + Week 1 + + + + 2019-01-15 + + TUESDAY + Week 1 + + + + 2019-01-14 + + MONDAY + Week 1 + + + + 2019-03-30 + + SATURDAY + Week 10 + + + + 2019-03-31 + + SUNDAY + Week 10 + + + + 2019-04-01 + + MONDAY + Week 11 + + + + 2019-04-02 + + TUESDAY + Week 11 + + + + 2019-04-03 + + WEDNESDAY + Week 11 + + + + 2019-04-04 + + THURSDAY + Week 11 + + + + 2019-04-05 + + FRIDAY + Week 11 + + + + 2019-04-06 + + SATURDAY + Week 11 + + + + 2019-04-07 + + SUNDAY + Week 11 + + + + 2019-04-08 + + MONDAY + Week 12 + + + + 2019-04-09 + + TUESDAY + Week 12 + + + + 2019-01-29 + + TUESDAY + Week 3 + + + + 2019-01-28 + + MONDAY + Week 3 + + + + 2019-01-27 + + SUNDAY + Week 2 + + + + 2019-01-26 + + SATURDAY + Week 2 + + + + 2019-01-25 + + FRIDAY + Week 2 + + + + 2019-01-24 + + THURSDAY + Week 2 + + + + 2019-01-23 + + WEDNESDAY + Week 2 + + + + 2019-01-22 + + TUESDAY + Week 2 + + + + 2019-01-21 + + MONDAY + Week 2 + + + + 2019-01-20 + + SUNDAY + Week 1 + + + + 2019-04-10 + + WEDNESDAY + Week 12 + + + + 2019-04-11 + + THURSDAY + Week 12 + + + + 2019-04-12 + + FRIDAY + Week 12 + + + + 2019-04-13 + + SATURDAY + Week 12 + + + + 2019-04-14 + + SUNDAY + Week 12 + + + + 2019-04-15 + + MONDAY + Week 13 + + + + 2019-04-16 + + TUESDAY + Week 13 + + + + 2019-04-17 + + WEDNESDAY + Week 13 + + + + 2019-04-18 + + THURSDAY + Week 13 + + + + 2019-04-19 + + FRIDAY + Week 13 + + + + 2019-03-01 + 2019-02-28 + 2019-03-02 + 2019-02-27 + 2019-03-03 + 2019-02-26 + 2019-02-25 + 2019-04-22 + 2019-04-23 + 2019-04-24 + 2019-04-25 + 2019-04-26 + 2019-04-27 + 2019-04-28 + 2019-02-09 + 2019-02-08 + 2019-02-07 + 2019-02-06 + 2019-02-05 + 2019-02-04 + 2019-02-03 + 2019-02-02 + 2019-02-01 + 2019-01-31 + 2019-01-30 + 2019-04-20 + 2019-04-21 + 2019-03-10 + 2019-03-11 + 2019-03-12 + 2019-03-13 + 2019-03-14 + 2019-03-15 + 2019-03-16 + 2019-03-17 + 2019-03-18 + 2019-03-19 + 2019-02-19 + 2019-02-18 + 2019-02-17 + 2019-02-16 + 2019-02-15 + 2019-02-14 + 2019-02-13 + 2019-02-12 + 2019-02-11 + 2019-02-10 + 2019-03-20 + 2019-03-21 + 2019-03-22 + 2019-03-23 + 2019-03-24 + 2019-03-25 + 2019-03-26 + 2019-03-27 + 2019-03-28 + 2019-03-29 + 2019-01-19 + 2019-01-18 + 2019-01-17 + 2019-01-16 + 2019-01-15 + 2019-01-14 + 2019-02-24 + 2019-02-23 + 2019-02-22 + 2019-02-21 + 2019-02-20 + 2019-03-30 + 2019-03-31 + 2019-04-01 + 2019-04-02 + 2019-04-03 + 2019-04-04 + 2019-04-05 + 2019-04-06 + 2019-04-07 + 2019-04-08 + 2019-04-09 + 2019-01-29 + 2019-01-28 + 2019-01-27 + 2019-01-26 + 2019-01-25 + 2019-01-24 + 2019-01-23 + 2019-01-22 + 2019-01-21 + 2019-01-20 + 2019-04-10 + 2019-04-11 + 2019-04-12 + 2019-04-13 + 2019-04-14 + 2019-04-15 + 2019-04-16 + 2019-03-04 + 2019-04-17 + 2019-03-05 + 2019-04-18 + 2019-03-06 + 2019-04-19 + 2019-03-07 + 2019-03-08 + 2019-03-09 + 2019-04-30 + 2019-05-10 + 2019-05-11 + 2019-05-01 + 2019-05-02 + 2019-05-03 + 2019-05-04 + 2019-05-05 + 2019-05-06 + 2019-05-07 + 2019-04-29 + 2019-05-08 + 2019-05-09 + + diff --git a/test/java/planmysem/common/UtilsTest.java b/test/java/planmysem/common/UtilsTest.java index ef0a2520f..a919a488c 100644 --- a/test/java/planmysem/common/UtilsTest.java +++ b/test/java/planmysem/common/UtilsTest.java @@ -1,8 +1,10 @@ package planmysem.common; +import static junit.framework.TestCase.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.time.LocalTime; import java.util.Arrays; import java.util.List; @@ -38,7 +40,7 @@ public void isAnyNull() { } @Test - public void elementsAreUnique() throws Exception { + public void elementsAreUnique() { // empty list assertAreUnique(); @@ -62,6 +64,78 @@ public void elementsAreUnique() throws Exception { assertNotUnique(null, "a", "b", null); } + @Test + public void parse_day_successful() { + assertEquals(Utils.parseDay("Monday"), 1); + assertEquals(Utils.parseDay("monday "), 1); + assertEquals(Utils.parseDay("Mon"), 1); + assertEquals(Utils.parseDay("mon"), 1); + assertEquals(Utils.parseDay("1"), 1); + + assertEquals(Utils.parseDay("Tuesday"), 2); + assertEquals(Utils.parseDay("tuesday "), 2); + assertEquals(Utils.parseDay("Tues"), 2); + assertEquals(Utils.parseDay("tues"), 2); + assertEquals(Utils.parseDay("2"), 2); + + assertEquals(Utils.parseDay("Wednesday"), 3); + assertEquals(Utils.parseDay("Wed"), 3); + assertEquals(Utils.parseDay("wed"), 3); + assertEquals(Utils.parseDay("3"), 3); + + assertEquals(Utils.parseDay("Thursday"), 4); + assertEquals(Utils.parseDay(" thursday"), 4); + assertEquals(Utils.parseDay("Thurs"), 4); + assertEquals(Utils.parseDay("thurs"), 4); + assertEquals(Utils.parseDay("4"), 4); + + assertEquals(Utils.parseDay("Friday"), 5); + assertEquals(Utils.parseDay(" friday"), 5); + assertEquals(Utils.parseDay("Fri"), 5); + assertEquals(Utils.parseDay("fri"), 5); + assertEquals(Utils.parseDay("5"), 5); + + assertEquals(Utils.parseDay("Saturday"), 6); + assertEquals(Utils.parseDay(" saturday"), 6); + assertEquals(Utils.parseDay("Sat"), 6); + assertEquals(Utils.parseDay("sat"), 6); + assertEquals(Utils.parseDay("6"), 6); + + assertEquals(Utils.parseDay("Sunday"), 7); + assertEquals(Utils.parseDay(" sunday"), 7); + assertEquals(Utils.parseDay("Sun"), 7); + assertEquals(Utils.parseDay("sun"), 7); + assertEquals(Utils.parseDay("7"), 7); + } + + @Test + public void parse_day_unsuccessful() { + assertEquals(Utils.parseDay("Mond"), -1); + assertEquals(Utils.parseDay("Mo"), -1); + assertEquals(Utils.parseDay("Fr"), -1); + assertEquals(Utils.parseDay("8"), -1); + assertEquals(Utils.parseDay("0"), -1); + } + + @Test + public void parse_time_successful() { + assertEquals(Utils.parseTime("08:00"), LocalTime.of(8, 0)); + assertEquals(Utils.parseTime("8:00 PM"), LocalTime.of(20, 0)); + assertEquals(Utils.parseTime("14:00"), LocalTime.of(14, 0)); + assertEquals(Utils.parseTime("00:00"), LocalTime.of(0, 0)); + assertEquals(Utils.parseTime("8:00"), LocalTime.of(8, 0)); + assertEquals(Utils.parseTime("8:00 AM"), LocalTime.of(8, 0)); + } + + @Test + public void parse_time_unsuccessful() { + assertEquals(Utils.parseTime("8-00"), null); + assertEquals(Utils.parseTime("8:00 am"), null); + assertEquals(Utils.parseTime("8:00 pm"), null); + assertEquals(Utils.parseTime("14:00 am"), null); + assertEquals(Utils.parseTime("16:00 pm"), null); + } + private void assertAreUnique(Object... objects) { assertTrue(Utils.elementsAreUnique(Arrays.asList(objects))); } diff --git a/test/java/planmysem/logic/LogicTest.java b/test/java/planmysem/logic/LogicTest.java index 62b251e32..d9c2995e5 100644 --- a/test/java/planmysem/logic/LogicTest.java +++ b/test/java/planmysem/logic/LogicTest.java @@ -1,14 +1,45 @@ package planmysem.logic; +import static junit.framework.TestCase.assertEquals; +import static planmysem.common.Messages.MESSAGE_INVALID_DATE; +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL; +import static planmysem.common.Messages.MESSAGE_INVALID_TIME; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +import javafx.util.Pair; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import planmysem.commands.AddCommand; +import planmysem.commands.ClearCommand; +import planmysem.commands.CommandResult; +import planmysem.commands.DeleteCommand; +import planmysem.commands.EditCommand; +import planmysem.commands.ExitCommand; +import planmysem.commands.HelpCommand; import planmysem.data.Planner; +import planmysem.data.semester.Day; +import planmysem.data.slot.Description; +import planmysem.data.slot.Location; +import planmysem.data.slot.Name; +import planmysem.data.slot.ReadOnlySlot; +import planmysem.data.slot.Slot; +import planmysem.data.tag.Tag; import planmysem.storage.StorageFile; - public class LogicTest { /** @@ -17,16 +48,16 @@ public class LogicTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); - private StorageFile storgageFile; + private StorageFile storageFile; private Planner planner; private Logic logic; @Before public void setup() throws Exception { - // saveFile = new StorageFile(saveFolder.newFile("testSaveFile.txt").getPath()); - // addressBook = new AddressBook(); - // saveFile.save(addressBook); - // logic = new Logic(saveFile, addressBook); + storageFile = new StorageFile(temporaryFolder.newFile("testSaveFile.txt").getPath()); + planner = new Planner(); + storageFile.save(planner); + logic = new Logic(storageFile, planner); } @Test @@ -34,6 +65,300 @@ public void constructor() { //Constructor is called in the setup() method which executes before every test, no need to call it here again. //Confirm the last shown list is empty - // assertEquals(Collections.emptyList(), logic.getLastShownList()); + assertEquals(Collections.emptyList(), logic.getLastShownSlots()); + } + + @Test + public void execute_invalid() throws Exception { + String invalidCommand = " "; + assertCommandBehavior(invalidCommand, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + @Test + public void execute_unknownCommandWord() throws Exception { + String unknownCommand = "uicfhmowqewca"; + assertCommandBehavior(unknownCommand, HelpCommand.MESSAGE_ALL_USAGES); + } + + @Test + public void execute_help() throws Exception { + assertCommandBehavior("help", HelpCommand.MESSAGE_ALL_USAGES); + } + + @Test + public void execute_exit() throws Exception { + assertCommandBehavior("exit", ExitCommand.MESSAGE_EXIT_ACKNOWEDGEMENT); + } + + @Test + public void execute_clear() throws Exception { + TestDataHelper helper = new TestDataHelper(); + planner.addSlot(LocalDate.now(), helper.generateSlot(1)); + planner.addSlot(LocalDate.now(), helper.generateSlot(2)); + planner.addSlot(LocalDate.now(), helper.generateSlot(3)); + + assertCommandBehavior("clear", ClearCommand.MESSAGE_SUCCESS, new Planner(), false, Collections.emptyList()); + } + + /** + * Test add command + */ + + @Test + public void execute_add_invalidArgsFormat() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); + assertCommandBehavior( + "add wrong args wrong args", expectedMessage); + assertCommandBehavior( + "add d/mon st/08:00 et/09:00", expectedMessage); + assertCommandBehavior( + "add n/CS2113T Tutorial st/08:00 et/09:00", expectedMessage); + assertCommandBehavior( + "add n/CS2113T Tutorial d/mon et/09:00", expectedMessage); + } + + @Test + public void execute_add_invalidSlotData() throws Exception { + assertCommandBehavior("add n/CS2113T Tutorial d/mon st/08:00 et/25:00", String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + assertCommandBehavior("add n/CS2113T Tutorial d/mon st/08:00 et/13:00am", String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + assertCommandBehavior("add n/CS2113T Tutorial d/Superday st/08:00 et/11:00", String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_DATE)); + assertCommandBehavior("add n/CS2113T Tutorial d/01-13-2019 st/08:00 et/11:00", String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_DATE)); + } + + @Test + public void execute_add_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Slot slotToBeAdded = helper.slotOne(); + LocalDate dateToBeAdded = LocalDate.now(); + HashMap days = new HashMap<>(); + days.put(dateToBeAdded, planner.getSemester().getDays().get(dateToBeAdded)); + + Planner expectedPlanner = new Planner(); + expectedPlanner.addSlot(dateToBeAdded, slotToBeAdded); + + // execute command and verify result + assertCommandBehavior(helper.generateAddCommand(dateToBeAdded, slotToBeAdded), + String.format(AddCommand.MESSAGE_SUCCESS, 1, AddCommand.craftSuccessMessage(days, slotToBeAdded)), + expectedPlanner, + false, + Collections.emptyList()); + } + + /** + * Test delete command + */ + + @Test + public void execute_edit_invalidArgsFormat() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + assertCommandBehavior( + "edit wrong arguments", expectedMessage); + assertCommandBehavior( + "edit nl/COM2 04-01", expectedMessage); + assertCommandBehavior( + "edit -1", expectedMessage); + assertCommandBehavior( + "e nl/COM2 04-01", expectedMessage); + } + + /** + * Test delete command + */ + + @Test + public void execute_delete_invalidArgsFormat() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); + assertCommandBehavior( + "delete wrong args wrong args", expectedMessage); + assertCommandBehavior( + "delete t", expectedMessage); + assertCommandBehavior( + "del wrong", expectedMessage); + assertCommandBehavior( + "d wrong", expectedMessage); + } + + /** + * Executes the command and confirms that the result message is correct. + * Both the 'planner' and the 'last shown list' are expected to be empty. + * @see #assertCommandBehavior(String, String, Planner, boolean, List) + */ + private void assertCommandBehavior(String inputCommand, String expectedMessage) throws Exception { + assertCommandBehavior(inputCommand, expectedMessage, planner,false, Collections.emptyList()); + } + + /** + * Executes the command and confirms that the result message is correct and + * also confirms that the following three parts of the Logic object's state are as expected:
+ * - the internal planner data are same as those in the {@code expectedPlanner}
+ * - the internal 'last shown slots' matches the {@code expectedLastList}
+ * - the storage file content matches data in {@code expectedPlanner}
+ */ + private void assertCommandBehavior(String inputCommand, + String expectedMessage, + Planner expectedPlanner, + boolean isRelevantSlotsExpected, + List lastShownSlots) throws Exception { + + //Execute the command + CommandResult r = logic.execute(inputCommand); + + //Confirm the result contains the right data + assertEquals(expectedMessage, r.feedbackToUser); + assertEquals(r.getRelevantSlots().isPresent(), isRelevantSlotsExpected); + if(isRelevantSlotsExpected){ + assertEquals(lastShownSlots, r.getRelevantSlots().get()); + } + + //Confirm the state of data is as expected + assertEquals(expectedPlanner, planner); + assertEquals(lastShownSlots, logic.getLastShownSlots()); + assertEquals(planner, storageFile.load()); + } + + + /** + * A utility class to generate test data. + */ + class TestDataHelper{ + + Slot slotOne() throws Exception { + Name name = new Name("CS2113T Tutorial"); + Location location = new Location("COM2 04-11"); + Description description = new Description("Topic: Sequence Diagram"); + LocalTime startTime = LocalTime.parse("08:00"); + LocalTime endTime = LocalTime.parse("09:00"); + Tag tag1 = new Tag("CS2113T"); + Tag tag2 = new Tag("Tutorial"); + Set tags = new HashSet<>(Arrays.asList(tag1, tag2)); + return new Slot(name, location, description, startTime, endTime, tags); + } + + /** + * Generates a valid slot using the given seed. + * Running this function with the same parameter values guarantees the returned slot will have the same state. + * Each unique seed will generate a unique slot object. + * + * @param seed used to generate the person data field values + */ + Slot generateSlot(int seed) throws Exception { + return new Slot( + new Name("slot " + seed), + new Location("location " + Math.abs(seed)), + new Description("description " + Math.abs(seed)), + LocalTime.parse("00:00"), + LocalTime.parse("00:00"), + new HashSet<>(Arrays.asList(new Tag("tag" + Math.abs(seed)), new Tag("tag" + Math.abs(seed + 1)))) + ); + } + + /** Generates the correct add command based on the person given */ + String generateAddCommand(LocalDate date, Slot s) { + StringJoiner cmd = new StringJoiner(" "); + + cmd.add("add"); + + cmd.add("n/" + s.getName()); + cmd.add("d/" + date.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"))); + cmd.add("st/" + s.getStartTime()); + cmd.add("et/" + s.getDuration()); + if (s.getLocation() != null) { + cmd.add("l/" + s.getLocation()); + } + if (s.getDescription() != null) { + cmd.add("des/" + s.getDescription()); + } + + Set tags = s.getTags(); + if (tags != null) { + for(Tag tag: tags){ + cmd.add("t/" + tag); + } + } + + return cmd.toString(); + } + + /** + * Generates an AddressBook with auto-generated persons. + * @param isPrivateStatuses flags to indicate if all contact details of respective persons should be set to + * private. + */ + // AddressBook generateAddressBook(Boolean... isPrivateStatuses) throws Exception{ + // AddressBook addressBook = new AddressBook(); + // addToAddressBook(addressBook, isPrivateStatuses); + // return addressBook; + // } + + /** + * Generates an AddressBook based on the list of Persons given. + */ + // AddressBook generateAddressBook(List persons) throws Exception{ + // AddressBook addressBook = new AddressBook(); + // addToAddressBook(addressBook, persons); + // return addressBook; + // } + + /** + * Adds auto-generated Person objects to the given AddressBook + * @param planner The AddressBook to which the Persons will be added + */ + // void addToPlanner(Planner planner) throws Exception{ + // addToPlanner(planner, generatePersonList(isPrivateStatuses)); + // } + + /** + * Adds the given list of slots to the given Planner + */ + void addToPlanner(Planner planner, List> slotsToAdd) throws Exception{ + for(Pair p: slotsToAdd){ + planner.addSlot(p.getKey(), p.getValue()); + } + } + + /** + * Creates a list of Persons based on the give Person objects. + */ + // List generatePersonList(Person... persons) throws Exception{ + // List personList = new ArrayList<>(); + // for(Person p: persons){ + // personList.add(p); + // } + // return personList; + // } + + /** + * Generates a list of Persons based on the flags. + * @param isPrivateStatuses flags to indicate if all contact details of respective persons should be set to + * private. + */ + // List generatePersonList(Boolean... isPrivateStatuses) throws Exception{ + // List persons = new ArrayList<>(); + // int i = 1; + // for(Boolean p: isPrivateStatuses){ + // persons.add(generatePerson(i++, p)); + // } + // return persons; + // } + + /** + * Generates a Person object with given name. Other fields will have some dummy values. + */ + // Person generatePersonWithName(String name) throws Exception { + // return new Person( + // new Name(name), + // new Phone("1", false), + // new Email("1@email", false), + // new Address("House of 1", false), + // Collections.singleton(new Tag("tag")) + // ); + // } } + } \ No newline at end of file diff --git a/test/java/planmysem/parser/ParserTest.java b/test/java/planmysem/parser/ParserTest.java index 29baea1a5..f4819c68e 100644 --- a/test/java/planmysem/parser/ParserTest.java +++ b/test/java/planmysem/parser/ParserTest.java @@ -6,7 +6,12 @@ import org.junit.Before; import org.junit.Test; +import planmysem.commands.AddCommand; +import planmysem.commands.ClearCommand; import planmysem.commands.Command; +import planmysem.commands.DeleteCommand; +import planmysem.commands.EditCommand; +import planmysem.commands.ExitCommand; import planmysem.commands.HelpCommand; import planmysem.commands.IncorrectCommand; @@ -26,6 +31,275 @@ public void emptyInput_returnsIncorrect() { parseAndAssertIncorrectWithMessage(resultMessage, emptyInputs); } + @Test + public void unknownCommandWord_returnsHelp() { + final String input = "unknowncommandword arguments arguments"; + parseAndAssertCommandType(input, HelpCommand.class); + } + + /** + * Test 0-argument commands + */ + + @Test + public void helpCommand_parsedCorrectly() { + final String input = "help"; + parseAndAssertCommandType(input, HelpCommand.class); + } + + @Test + public void clearCommand_parsedCorrectly() { + final String input = "clear"; + parseAndAssertCommandType(input, ClearCommand.class); + } + + // @Test + // public void listCommand_parsedCorrectly() { + // final String input = "list"; + // parseAndAssertCommandType(input, ListCommand.class); + // } + + @Test + public void exitCommand_parsedCorrectly() { + final String input = "exit"; + parseAndAssertCommandType(input, ExitCommand.class); + } + + + /** + * Test add command + */ + + @Test + public void addCommand_invalidArgs() { + final String[] inputs = { + "add", + "add ", + "add wrong args format", + // no name prefix + "add Tutorial d/mon st/08:00 et/09:00", + // no date prefix + "add n/CS2113T Tutorial et/09:00 et/09:00", + // no start time prefix + "add n/CS2113T Tutorial d/mon et/09:00", + // no end time prefix + "add n/CS2113T Tutorial d/mon st/08:00 ", + }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, inputs); + } + + // @Test + // public void addCommand_invalidPersonDataInArgs() { + // final String invalidName = "[]\\[;]"; + // final String validName = Name.EXAMPLE; + // final String invalidPhoneArg = "p/not__numbers"; + // final String validPhoneArg = "p/" + Phone.EXAMPLE; + // final String invalidEmailArg = "e/notAnEmail123"; + // final String validEmailArg = "e/" + Email.EXAMPLE; + // final String invalidTagArg = "t/invalid_-[.tag"; + // + // // address can be any string, so no invalid address + // final String addCommandFormatString = "add $s $s $s a/" + Address.EXAMPLE; + // + // // test each incorrect person data field argument individually + // final String[] inputs = { + // // invalid name + // String.format(addCommandFormatString, invalidName, validPhoneArg, validEmailArg), + // // invalid phone + // String.format(addCommandFormatString, validName, invalidPhoneArg, validEmailArg), + // // invalid email + // String.format(addCommandFormatString, validName, validPhoneArg, invalidEmailArg), + // // invalid tag + // String.format(addCommandFormatString, validName, validPhoneArg, validEmailArg) + " " + invalidTagArg + // }; + // for (String input : inputs) { + // parseAndAssertCommandType(input, IncorrectCommand.class); + // } + // } + // + // @Test + // public void addCommand_validPersonData_parsedCorrectly() { + // final Person testPerson = generateTestPerson(); + // final String input = convertPersonToAddCommandString(testPerson); + // final AddCommand result = parseAndAssertCommandType(input, AddCommand.class); + // assertEquals(result.getPerson(), testPerson); + // } + // + // @Test + // public void addCommand_duplicateTags_merged() throws IllegalValueException { + // final Person testPerson = generateTestPerson(); + // String input = convertPersonToAddCommandString(testPerson); + // for (Tag tag : testPerson.getTags()) { + // // create duplicates by doubling each tag + // input += " t/" + tag.tagName; + // } + // + // final AddCommand result = parseAndAssertCommandType(input, AddCommand.class); + // assertEquals(result.getPerson(), testPerson); + // } + // + // private static Person generateTestPerson() { + // try { + // return new Person( + // new Name(Name.EXAMPLE), + // new Phone(Phone.EXAMPLE, true), + // new Email(Email.EXAMPLE, false), + // new Address(Address.EXAMPLE, true), + // new HashSet<>(Arrays.asList(new Tag("tag1"), new Tag("tag2"), new Tag("tag3"))) + // ); + // } catch (IllegalValueException ive) { + // throw new RuntimeException("test person data should be valid by definition"); + // } + // } + // + // private static String convertPersonToAddCommandString(ReadOnlyPerson person) { + // String addCommand = "add " + // + person.getName().fullName + // + (person.getPhone().isPrivate() ? " pp/" : " p/") + person.getPhone().value + // + (person.getEmail().isPrivate() ? " pe/" : " e/") + person.getEmail().value + // + (person.getAddress().isPrivate() ? " pa/" : " a/") + person.getAddress().value; + // for (Tag tag : person.getTags()) { + // addCommand += " t/" + tag.tagName; + // } + // return addCommand; + // } + /** + * + * Test edit command + */ + + @Test + public void editCommand_noArgs() { + final String[] inputs = { "edit", "edit " }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, inputs); + } + + @Test + public void editcommand_argsNoTagNoIndex() { + final String[] inputs = { "edit nl/COM2 04-01", "edit net/100", "edit d/description" }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, inputs); + } + + @Test + public void editcommand_argsIndexNegative() { + final String[] inputs = { "edit -1", "edit -100" }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, inputs); + } + + /** + * Test delete command + */ + + @Test + public void deleteCommand_noArgs() { + final String[] inputs = { "delete", "delete " }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, inputs); + } + + @Test + public void deleteCommand_argsIsNotSingleNumber() { + final String[] inputs = { "delete notAnumber ", "delete 8*wh12", "delete 1 2 3 4 5" }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, inputs); + } + + // @Test + // public void deleteCommand_numericArg_indexParsedCorrectly() { + // final int testIndex = 1; + // final String input = "delete " + testIndex; + // final DeleteCommand result = parseAndAssertCommandType(input, DeleteCommand.class); + // assertEquals(result.getTargetIndex(), testIndex); + // } + + // @Test + // public void viewCommand_noArgs() { + // final String[] inputs = { "view", "view " }; + // final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE); + // parseAndAssertIncorrectWithMessage(resultMessage, inputs); + // } + // + // @Test + // public void viewCommand_argsIsNotSingleNumber() { + // final String[] inputs = { "view notAnumber ", "view 8*wh12", "view 1 2 3 4 5" }; + // final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE); + // parseAndAssertIncorrectWithMessage(resultMessage, inputs); + // } + // + // @Test + // public void viewCommand_numericArg_indexParsedCorrectly() { + // final int testIndex = 2; + // final String input = "view " + testIndex; + // final ViewCommand result = parseAndAssertCommandType(input, ViewCommand.class); + // assertEquals(result.getTargetIndex(), testIndex); + // } + // + // @Test + // public void viewAllCommand_noArgs() { + // final String[] inputs = { "viewall", "viewall " }; + // final String resultMessage = + // String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewAllCommand.MESSAGE_USAGE); + // parseAndAssertIncorrectWithMessage(resultMessage, inputs); + // } + + // @Test + // public void viewAllCommand_argsIsNotSingleNumber() { + // final String[] inputs = { "viewall notAnumber ", "viewall 8*wh12", "viewall 1 2 3 4 5" }; + // final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewAllCommand.MESSAGE_USAGE); + // parseAndAssertIncorrectWithMessage(resultMessage, inputs); + // } + // + // @Test + // public void viewAllCommand_numericArg_indexParsedCorrectly() { + // final int testIndex = 3; + // final String input = "viewall " + testIndex; + // final ViewAllCommand result = parseAndAssertCommandType(input, ViewAllCommand.class); + // assertEquals(result.getTargetIndex(), testIndex); + // } + + /** + * Test find slot by keyword in name command + */ + + // @Test + // public void findCommand_invalidArgs() { + // // no keywords + // final String[] inputs = { + // "find", + // "find " + // }; + // final String resultMessage = + // String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE); + // parseAndAssertIncorrectWithMessage(resultMessage, inputs); + // } + + // @Test + // public void findCommand_validArgs_parsedCorrectly() { + // final String[] keywords = { "key1", "key2", "key3" }; + // final Set keySet = new HashSet<>(Arrays.asList(keywords)); + // + // final String input = "find " + String.join(" ", keySet); + // final FindCommand result = + // parseAndAssertCommandType(input, FindCommand.class); + // assertEquals(keySet, result.getKeywords()); + // } + // + // @Test + // public void findCommand_duplicateKeys_parsedCorrectly() { + // final String[] keywords = { "key1", "key2", "key3" }; + // final Set keySet = new HashSet<>(Arrays.asList(keywords)); + // + // // duplicate every keyword + // final String input = "find " + String.join(" ", keySet) + " " + String.join(" ", keySet); + // final FindCommand result = + // parseAndAssertCommandType(input, FindCommand.class); + // assertEquals(keySet, result.getKeywords()); + // } + /** * Utility methods */ diff --git a/test/java/planmysem/storage/StorageFileTest.java b/test/java/planmysem/storage/StorageFileTest.java index 7e0415d38..3706a6aea 100644 --- a/test/java/planmysem/storage/StorageFileTest.java +++ b/test/java/planmysem/storage/StorageFileTest.java @@ -1,8 +1,14 @@ package planmysem.storage; +import static planmysem.util.TestUtil.assertTextFilesEqual; + +import java.nio.file.Paths; + import org.junit.Rule; +import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; +import planmysem.data.exception.IllegalValueException; public class StorageFileTest { private static final String TEST_DATA_FOLDER = "test/data/StorageFileTest"; @@ -11,28 +17,28 @@ public class StorageFileTest { public ExpectedException thrown = ExpectedException.none(); @Rule - public TemporaryFolder testFolder = new TemporaryFolder(); + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void constructor_nullFilePath_exceptionThrown() throws Exception { + thrown.expect(NullPointerException.class); + new StorageFile(null); + } + + @Test + public void constructor_noTxtExtension_exceptionThrown() throws Exception { + thrown.expect(IllegalValueException.class); + new StorageFile(TEST_DATA_FOLDER + "/" + "InvalidfileName"); + } + + @Test + public void load_invalidFormat_exceptionThrown() throws Exception { + // The file contains valid xml data, but does not match the Planner class + StorageFile storage = getStorage("InvalidData.txt"); + thrown.expect(StorageFile.StorageOperationException.class); + storage.load(); + } - // @Test - // public void constructor_nullFilePath_exceptionThrown() throws Exception { - // thrown.expect(NullPointerException.class); - // new StorageFile(null); - // } - // - // @Test - // public void constructor_noTxtExtension_exceptionThrown() throws Exception { - // thrown.expect(IllegalValueException.class); - // new StorageFile(TEST_DATA_FOLDER + "/" + "InvalidfileName"); - // } - // - // @Test - // public void load_invalidFormat_exceptionThrown() throws Exception { - // // The file contains valid xml data, but does not match the AddressBook class - // StorageFile storage = getStorage("InvalidData.txt"); - // thrown.expect(StorageOperationException.class); - // storage.load(); - // } - // // @Test // public void load_validFormat() throws Exception { // AddressBook actualAB = getStorage("ValidData.txt").load(); @@ -61,33 +67,33 @@ public class StorageFileTest { // // // getPath() method in StorageFile class is trivial so it is not tested // - // /** - // * Asserts that the contents of two storage files are the same. - // */ - // private void assertStorageFilesEqual(StorageFile sf1, StorageFile sf2) throws Exception { - // assertTextFilesEqual(Paths.get(sf1.getPath()), Paths.get(sf2.getPath())); - // } - // - // private StorageFile getStorage(String fileName) throws Exception { - // return new StorageFile(TEST_DATA_FOLDER + "/" + fileName); - // } - // - // private StorageFile getTempStorage() throws Exception { - // return new StorageFile(testFolder.getRoot().getPath() + "/" + "temp.txt"); - // } - // - // private AddressBook getTestAddressBook() throws Exception { - // AddressBook ab = new AddressBook(); - // ab.addPerson(new Person(new Name("John Doe"), + /** + * Asserts that the contents of two storage files are the same. + */ + private void assertStorageFilesEqual(StorageFile sf1, StorageFile sf2) throws Exception { + assertTextFilesEqual(Paths.get(sf1.getPath()), Paths.get(sf2.getPath())); + } + + private StorageFile getStorage(String fileName) throws Exception { + return new StorageFile(TEST_DATA_FOLDER + "/" + fileName); + } + + private StorageFile getTempStorage() throws Exception { + return new StorageFile(temporaryFolder.getRoot().getPath() + "/" + "temp.txt"); + } + + // private Planner getTestAddressBook() throws Exception { + // Planner planner = new Planner(); + // planner.addPerson(new Person(new Name("John Doe"), // new Phone("98765432", false), // new Email("johnd@gmail.com", false), // new Address("John street, block 123, #01-01", false), // Collections.emptySet())); - // ab.addPerson(new Person(new Name("Betsy Crowe"), + // planner.addPerson(new Person(new Name("Betsy Crowe"), // new Phone("1234567", true), // new Email("betsycrowe@gmail.com", false), // new Address("Newgate Prison", true), // new HashSet<>(Arrays.asList(new Tag("friend"), new Tag("criminal"))))); - // return ab; + // return planner; // } } From 045fc75cc53424e790b9f779a6f2742330a3d82e Mon Sep 17 00:00:00 2001 From: marcus_pzj Date: Mon, 18 Mar 2019 21:09:41 +0800 Subject: [PATCH 3/4] Update documentation: User Guide & Developer Guide --- docs/DeveloperGuide.adoc | 58 ++++++++----------------- docs/UserGuide.adoc | 33 +++++++------- src/planmysem/commands/FindCommand.java | 4 +- src/planmysem/commands/ListCommand.java | 5 ++- 4 files changed, 42 insertions(+), 58 deletions(-) diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index fa7e9c6ef..24815d4d7 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -376,75 +376,55 @@ Step 3. The user executes `view month March` command to view the calendar for Ma === Undo/Redo feature ==== Current Implementation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. +The undo/redo mechanism is facilitated by `VersionedPLanner`. +It extends `Planner` with an undo/redo history, stored internally as an `plannerStateList` and `currentStatePointer`. Additionally, it implements the following operations: -* `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. +* `VersionedPlanner#commit()` -- Saves the current address book state in its history. +* `VersionedPlanner#undo()` -- Restores the previous address book state from its history. +* `VersionedPlanner#redo()` -- Restores a previously undone address book state from its history. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +These operations are exposed in the `Model` interface as `Model#commitPlanner()`, `Model#undoPlanner()` and `Model#redoPlanner()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -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. +Step 1. The user launches the application for the first time. The `VersionedPlanner` will be initialized with the initial planner state, and the `currentStatePointer` pointing to that single planner state. -image::UndoRedoStartingStateListDiagram.png[width="800"] +Step 2. The user executes `delete 5` command to delete the 5th slot in the planner. The `delete` command calls `Model#commitPlanner()`, causing the modified state of the planner after the `delete 5` command executes to be saved in the `plannerStateList`, and the `currentStatePointer` is shifted to the newly inserted planner state. -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. - -image::UndoRedoNewCommand1StateListDiagram.png[width="800"] - -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`. - -image::UndoRedoNewCommand2StateListDiagram.png[width="800"] +Step 3. The user executes `add n/CS2113T ...` to add a new slot. The `add` command also calls `Model#commitPlanner()`, causing another modified planner state to be saved into the `plannerStateList`. [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`. +If a command fails its execution, it will not call `Model#commitPlanner()`, so the address book state will not be saved into the `addressBookStateList`. -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. - -image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] +Step 4. The user now decides that adding the slot was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoPlanner()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous planner state, and restores the planner to that state. [NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book 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. - -The following sequence diagram shows how the undo operation works: +If the `currentStatePointer` is at index 0, pointing to the initial planner state, then there are no previous planner states to restore. The `undo` command uses `Model#canUndoPlanner()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. -image::UndoRedoSequenceDiagram.png[width="800"] - -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. +The `redo` command does the opposite -- it calls `Model#redoPlanner()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the planner to that state. [NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book 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. - -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. - -image::UndoRedoNewCommand3StateListDiagram.png[width="800"] - -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. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. - -image::UndoRedoNewCommand4StateListDiagram.png[width="800"] +If the `currentStatePointer` is at index `plannerStateList.size() - 1`, pointing to the latest planner state, then there are no undone planner states to restore. The `redo` command uses `Model#canRedoPlanner()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. -The following activity diagram summarizes what happens when a user executes a new command: +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#commitPlanner()`, `Model#undoPlanner()` or `Model#redoPlanner()`. Thus, the `plannerStateList` remains unchanged. -image::UndoRedoActivityDiagram.png[width="650"] +Step 6. The user executes `clear`, which calls `Model#commitPlanner()`. Since the `currentStatePointer` is not pointing at the end of the `plannerStateList`, all planner states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/CS2113T ...` command. This is the behavior that most modern desktop applications follow. ==== Design Considerations ===== Aspect: How undo & redo executes -* **Alternative 1 (current choice):** Saves the entire address book. +* **Alternative 1 (current choice):** Saves the entire planner. ** Pros: Easy to implement. ** Cons: May have performance issues in terms of memory usage. * **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). +** Pros: Will use less memory (e.g. for `delete`, just save the slot being deleted). ** Cons: We must ensure that the implementation of each individual command are correct. ===== Aspect: Data structure to support the undo/redo commands -* **Alternative 1 (current choice):** Use a list to store the history of address book states. +* **Alternative 1 (current choice):** Use a list to store the history of planner states. ** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. ** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. * **Alternative 2:** Use `HistoryManager` for undo/redo diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 60d57fbbc..4a24377c5 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -163,32 +163,35 @@ Delete the second _slot_ shown via the `list` command. === Listing Slots: `list` / `l` -List _slots_ in the planner. + -Format: `list [past] [next] [all]` +Lists all slots whose name directly matches the specified keyword (not case-sensitive). + +//Format: `list [past] [next] [all]` +Format: `list n/NAME` -[NOTE] -==== -If you do not specify a keyword, then the `all` keyword will be used and all _slots_ will be listed. -==== +//[NOTE] +//==== +//If you do not specify a keyword, then the `all` keyword will be used and all _slots_ will be listed. +//==== Examples: -* `list next` + -List all _slots_ that has yet to occurred in the planner. +* `list n/CS2113T` + +List all _slots_ that is named `CS2113T` in the planner. === Locating Slots: `find` / `f` -Find all _slots_ that have a similar name and or contains specified _tags_. + -Format: `find [n/SLOT_NAMES] [t/TAG]...` +Find all _slots_ whose part of their name contains the specified keyword and displays them as a list. + +Format: `find [KEYWORD]...` -[NOTE] -==== -TODO -==== +//[NOTE] +//==== +//TODO +//==== Example: -* TODO +* `find CS` +Find all _slots_ whose name contains `CS` (eg. CS1010, CS2113T, SCS1010) + === View the Planner : `view` diff --git a/src/planmysem/commands/FindCommand.java b/src/planmysem/commands/FindCommand.java index 099b2384e..6cebde0f9 100644 --- a/src/planmysem/commands/FindCommand.java +++ b/src/planmysem/commands/FindCommand.java @@ -25,9 +25,9 @@ public class FindCommand extends Command { public static final String MESSAGE_SUCCESS = "%1$s Slots listed.\n%2$s"; public static final String MESSAGE_SUCCESS_NONE = "0 Slots listed.\n"; public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Finds all slots whose name " - + "contains the specified keywords (case-sensitive).\n\t" + + "contains the specified keywords (not case-sensitive).\n\t" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n\t" - + "Example: " + COMMAND_WORD + "CS2113T"; + + "Example: " + COMMAND_WORD + "CS"; private final Set keywords; diff --git a/src/planmysem/commands/ListCommand.java b/src/planmysem/commands/ListCommand.java index 16f2c751a..623afba7e 100644 --- a/src/planmysem/commands/ListCommand.java +++ b/src/planmysem/commands/ListCommand.java @@ -20,10 +20,11 @@ public class ListCommand extends Command { public static final String COMMAND_WORD_SHORT = "l"; public static final String MESSAGE_SUCCESS = "%1$s Slots listed.\n%2$s"; public static final String MESSAGE_SUCCESS_NONE = "0 Slots listed.\n"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all slots." + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all slots whose name " + + "directly matches the specified keyword (not case-sensitive)." //+ "\n\tOptional Parameters: [past] [next] [all]" //+ "\n\tDefault: list all" - + "\n\tExample: " + COMMAND_WORD + " CS1010"; + + "\n\tExample: " + COMMAND_WORD + " n/CS1010"; private final String name; From 83899ea61f8bd51e188a71c77c0af5332ebb312a Mon Sep 17 00:00:00 2001 From: marcus_pzj Date: Mon, 18 Mar 2019 21:32:08 +0800 Subject: [PATCH 4/4] Edit Developer Guide to remove AB --- docs/DeveloperGuide.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 24815d4d7..b3c4ebb13 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -376,13 +376,13 @@ Step 3. The user executes `view month March` command to view the calendar for Ma === Undo/Redo feature ==== Current Implementation -The undo/redo mechanism is facilitated by `VersionedPLanner`. +The undo/redo mechanism is facilitated by `VersionedPlanner`. It extends `Planner` with an undo/redo history, stored internally as an `plannerStateList` and `currentStatePointer`. Additionally, it implements the following operations: -* `VersionedPlanner#commit()` -- Saves the current address book state in its history. -* `VersionedPlanner#undo()` -- Restores the previous address book state from its history. -* `VersionedPlanner#redo()` -- Restores a previously undone address book state from its history. +* `VersionedPlanner#commit()` -- Saves the current planner state in its history. +* `VersionedPlanner#undo()` -- Restores the previous planner state from its history. +* `VersionedPlanner#redo()` -- Restores a previously undone planner state from its history. These operations are exposed in the `Model` interface as `Model#commitPlanner()`, `Model#undoPlanner()` and `Model#redoPlanner()` respectively. @@ -395,7 +395,7 @@ Step 2. The user executes `delete 5` command to delete the 5th slot in the plann Step 3. The user executes `add n/CS2113T ...` to add a new slot. The `add` command also calls `Model#commitPlanner()`, causing another modified planner state to be saved into the `plannerStateList`. [NOTE] -If a command fails its execution, it will not call `Model#commitPlanner()`, so the address book state will not be saved into the `addressBookStateList`. +If a command fails its execution, it will not call `Model#commitPlanner()`, so the planner state will not be saved into the `plannerStateList`. Step 4. The user now decides that adding the slot was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoPlanner()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous planner state, and restores the planner to that state. @@ -407,7 +407,7 @@ The `redo` command does the opposite -- it calls `Model#redoPlanner()`, which sh [NOTE] If the `currentStatePointer` is at index `plannerStateList.size() - 1`, pointing to the latest planner state, then there are no undone planner states to restore. The `redo` command uses `Model#canRedoPlanner()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. -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#commitPlanner()`, `Model#undoPlanner()` or `Model#redoPlanner()`. Thus, the `plannerStateList` remains unchanged. +Step 5. The user then decides to execute the command `list`. Commands that do not modify the planner, such as `list`, will usually not call `Model#commitPlanner()`, `Model#undoPlanner()` or `Model#redoPlanner()`. Thus, the `plannerStateList` remains unchanged. Step 6. The user executes `clear`, which calls `Model#commitPlanner()`. Since the `currentStatePointer` is not pointing at the end of the `plannerStateList`, all planner states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/CS2113T ...` command. This is the behavior that most modern desktop applications follow. @@ -426,7 +426,7 @@ Step 6. The user executes `clear`, which calls `Model#commitPlanner()`. Since th * **Alternative 1 (current choice):** Use a list to store the history of planner states. ** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. -** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. +** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedPlanner`. * **Alternative 2:** Use `HistoryManager` for undo/redo ** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. ** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things.