diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 5e7576c15..23a96df2a 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -15,7 +15,7 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/CS2113-AY1819S2-T08-3/main +:repoURL: https://github.com/CS2113-AY1819S2-T08-3/main/tree/master By: `T08-3` Since: `Jan 2019` Licence: `MIT` @@ -46,7 +46,7 @@ Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plu . Locate the `build.gradle` file and select it. Click `OK` . Click `Open as Project` . Click `OK` to accept the default settings -. Run the `planmysem.Main` class (right-click the `Main` class and click `Run Main.main()`) and try executing a few commands +. Run the `PlanMySem.Main` class (right-click the `Main` class and click `Run Main.main()`) and try executing a few commands . Run all the tests (right-click the `test` folder, and click `Run 'All Tests'`) and ensure that they pass . Open the `StorageFile` file and check for any code errors . Open a console and run the command `gradlew processResources` (Mac/Linux: `./gradlew processResources`). It should finish with the `BUILD SUCCESSFUL` message. + @@ -58,7 +58,7 @@ This will generate all resources required by the application and tests. === Verifying the setup -. Run the `planmysem.Main` 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 @@ -78,7 +78,7 @@ Optionally, you can follow the <> docume ==== Updating documentation to match your fork -After forking the repo, the documentation will still have the _PlanMySem_ branding and refer to the `https://github.com/CS2113-AY1819S2-T08-3/main` repo. +After forking the repo, the documentation will still have the *PlanMySem* 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 `https://github.com/CS2113-AY1819S2-T08-3/main`), you should do the following: @@ -113,28 +113,25 @@ When you are ready to start coding, === Architecture .Architecture Diagram -image::Architecture.png[width="600"] +image::Architecture.png[width="100%"] The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component. [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/planmysem/Main.java[`Main`]. 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. <> represents a collection of classes used by multiple other components. -//The following class plays an important role at the architecture level: -// -//* `LogsCenter` : Used by many classes to write log messages to the App's log file. -The following class plays an important role at the architecture level, the App consists of four components. +The following class plays an important role at the architecture level, the App consists of four components: * <>: The UI of the App. * <>: The command executor. -* <>: Holds the data of the App in-memory. +* <>: Holds the data of the App in-memory. * <>: Reads data from, and writes data to, the hard disk. Each of the four components @@ -144,8 +141,8 @@ Each of the four components For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `Logic.java` class. -.Class Diagram of the Logic Component -image::LogicClassDiagram.png[width="800"] +.Class Diagram of overall application. +image::OverallClassDiagram.png[width="100%"] [discrete] ==== How the architecture components interact with each other @@ -153,7 +150,7 @@ image::LogicClassDiagram.png[width="800"] The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. .Component interactions for `delete 1` command -image::SDforDeleteSlot.png[width="800"] +image::SDforDeleteSlot.png[width="100%"] The sections below give more details of each component. @@ -163,17 +160,18 @@ The sections below give more details of each component. .Structure of the UI Component image::UiClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/planmysem/ui/Gui.java[`Gui.java`] +*API* : link:{repoURL}/src/main/PlanMySem/ui/Ui.java[`Ui.java`] -The UI consists of a `MainWindow` that is made up of parts e.g.`commandInput`, `outputConsole` etc. -//All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +The UI consists of a `MainWindow` that is made up of just `commandInput` and `outputConsole`. +This application is mainly a text-based application, hence here are not much componenets here. -//The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] +The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. +For example, the layout of the link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] The `UI` component, * Executes user commands read from `commandInput`, using the `Logic` component. -* Displays `commandResult` to the user via the `outputConsole`. +* Displays `commandResult` to the user via `outputConsole`. [[Design-Logic]] === Logic component @@ -183,7 +181,7 @@ The `UI` component, image::LogicClassDiagram.png[width="800"] *API* : -link:{repoURL}/src/main/java/plamysem/logic/Logic.java[`Logic.java`] +link:{repoURL}/src/main/java/plamysem/logicManager/Logic.java[`Logic.java`] . `Logic` uses the `parser` class to parse the user command. . This results in a `Command` object which is executed. @@ -196,18 +194,18 @@ 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-Data]] -=== Data component +[[Design-Model]] +=== Model component -.Overall structure of the Data Component -image::DataClassDiagram.png[width="800"] +.Overall structure of the Model Component +image::ModelClassDiagram.png[width="400"] -//*API* : link:{repoURL}/src/planmysem/data/Planner.java[`Planner.java`] +*API* : link:{repoURL}/src/PlanMySem/model/Model.java[`Model.java`] [[Design-Planner]] ==== Planner component -*API* : link:{repoURL}/src/planmysem/data/Planner.java[`Planner.java`] +*API* : link:{repoURL}/src/PlanMySem/model/Planner.java[`Planner.java`] The `Planner` component, @@ -218,7 +216,7 @@ The `Planner` component, [[Design-Semester]] ==== Semester component -*API* : link:{repoURL}/src/planmysem/data/semester/Semester.java[`Semester.java`] +*API* : link:{repoURL}/src/PlanMySem/model/semester/Semester.java[`Semester.java`] The `Semester` component, @@ -229,7 +227,7 @@ The `Semester` component, [[Design-Slot]] ==== Slot component -*API* : link:{repoURL}/src/planmysem/data/slot/Slot.java[`Slot.java`] +*API* : link:{repoURL}/src/PlanMySem/model/slot/Slot.java[`Slot.java`] The `Slot` component, @@ -244,9 +242,9 @@ Notice how `Slot` does not hold it's end time but rather it holds the `duration` === Storage component .Structure of the Storage Component -image::StorageClassDiagram.png[width="800"] +image::StorageClassDiagram.png[width="400"] -*API* : link:{repoURL}/src/planmysem/storage/Storage.java[`Storage.java`] +*API* : link:{repoURL}/src/PlanMySem/storageFile/Storage.java[`Storage.java`] The `Storage` component, @@ -256,7 +254,7 @@ The `Storage` component, [[Design-Common]] === Common classes -Classes used by multiple components are in the `planmysem.common` package. +Classes used by multiple components are in the `PlanMySem.common` package. == Implementation @@ -264,12 +262,12 @@ This section describes some noteworthy details on how certain features are imple === Initialization of the Planner and it's Semester -The `Planner` and it's `Semester` has to be initialized for _PlanMySem_ to work as all other features of _PlanMySem_ would +The `Planner` and it's `Semester` has to be initialized for *PlanMySem* to work as all other features of *PlanMySem* would interact with this `Semester` object. ==== Current Implementation -Upon launching _PlanMySem_, the initialization of the `Planner` and it's `Semester` would be implemented via two steps: +Upon launching *PlanMySem*, the initialization of the `Planner` and it's `Semester` would be implemented via two steps: 1. Automatically generate the academic calendar from the current date. 2. Setup current `Semester` from the academic calendar. @@ -365,7 +363,7 @@ These information would be assigned to the `Semester` object upon initialization ** Pros: Generation of academic calendar is efficient and not prone to calculation errors. ** Cons: Requires the pre-generated file which may be accidentally edited or deleted by the user. -=== Reinventing the Parser / Command Format and Structure +=== 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. @@ -381,7 +379,7 @@ Arguments are then parsed via 2 different methods/techniques according to the fo * 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: +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. @@ -401,7 +399,7 @@ The following are cases in which `IncorrectCommand` is invoked: ===== 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 *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. @@ -475,6 +473,9 @@ The `Edit` and `Delete` command then makes use of the _tagging_ system to then s ==== Future Implementation +=== List feature + + === Find feature ==== Current Implementation The find function supports searching using a single keyword. + @@ -592,9 +593,9 @@ Step 6. The user executes `clear`, which calls `Model#commitPlanner()`. Since th ** 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. // end::undoredo[] -=== Data Encryption / Decription feature +=== Data Encryption / Decryption feature -The storage file "PlanMySem.txt" is encrypted to prevent easy access of the user's calendar. +The storageFile 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. @@ -602,13 +603,13 @@ The encryption is done using AES/CBC/PKCS5Padding. The key used for encryption/d A initialization vector (IV) is required for the Cipher Block Chain (CBC) mode of encryption. A random IV is generated and appended at the beginning of the data before being stored. The IV is then retrieved from the same file to decrypt the data. -Encryption of the data is done automatically before the file is saved. In the implmentation, the AdaptedPlanner object is first marshalled into a StringWriter before being encrypted and written into the file. This is to ensure that the data is JAXB formatted and the save algorithm is unaffected. -Similarly, decryption of the data is done automatically before it is loaded. In the implementation, the file is read and decrypted and parsed into a StringReader object. The StringReader object is then unmarshalled and loaded. This is to ensure that the file is converted back into a JAXB object before being loaded and the load algorithm is unaffected. +Encryption of the data is done automatically before the file is saved. In the implementation, the AdaptedPlanner object is first marshaled into a StringWriter before being encrypted and written into the file. This is to ensure that the data is JAXB formatted and the save algorithm is unaffected. +Similarly, decryption of the data is done automatically before it is loaded. In the implementation, the file is read and decrypted and parsed into a StringReader object. The StringReader object is then un-marshaled and loaded. This is to ensure that the file is converted back into a JAXB object before being loaded and the load algorithm is unaffected. === Data Exporting / Exporting feature 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. +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. @@ -617,7 +618,7 @@ In our implementation, we have chosen not to export the tags into the .ics file. * **Alternative 1 (current choice):** Ignore tags when exporting. ** Pros: Easier to implement as iCalendar does not have in-built tag fields. ** Cons: Not all the information about the slots will be retained. -** Reason for choice: We do not have much control over other applications, and importing and exporting .ics within PlanMySem can be done using the storage .txt file. +** Reason for choice: We do not have much control over other applications, and importing and exporting .ics within *PlanMySem* can be done using the storageFile .txt file. * **Alternative 2:** Use the notes field and a tag identifier to save the tags. ** Pros: All the information from the semester will be exported. ** Cons: Requires other applications to be coded to read these tag identifiers and also to store and use the tags in their functions. @@ -765,16 +766,12 @@ To run tests in headless mode, open a console and run the command `gradlew clean //We have two types of tests: -//. *GUI Tests* - These are tests involving the GUI. They include, -//.. _System Tests_ that test the entire App by simulating user actions on the GUI. These are in the `systemtests` package. -//.. _Unit tests_ that test the individual components. These are in `seedu.address.ui` package. -. *Non-GUI Tests* - These are tests not involving the GUI. They include, -.. _Unit tests_ targeting the lowest level methods/classes. + -e.g. `planmysem.commons.UtilTest` -.. _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + -e.g. `planmysem.storage.StorageManagerTest` -.. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + -e.g. `planmysem.logic.LogicTest`, `planmysem.parse,ParserTest` +. _Unit tests_ targeting the lowest level methods/classes. + +e.g. `PlanMySem.commons.UtilTest` +. _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + +e.g. `PlanMySem.storageFile.StorageManagerTest` +. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + +e.g. `PlanMySem.logicManager.LogicTest`, `PlanMySem.parse,ParserTest` === Troubleshooting Testing @@ -804,7 +801,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/planmysem/Main.java[`Main.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. @@ -835,7 +832,7 @@ A project often depends on third-party libraries. For example, Address Book depe //[discrete] //==== `Logic` component // -//*Scenario:* You are in charge of `logic`. During dog-fooding, your team realize that it is troublesome for the user to type the whole command in order to execute a command. Your team devise some strategies to help cut down the amount of typing necessary, and one of the suggestions was to implement aliases for the command words. Your job is to implement such aliases. +//*Scenario:* You are in charge of `logicManager`. During dog-fooding, your team realize that it is troublesome for the user to type the whole command in order to execute a command. Your team devise some strategies to help cut down the amount of typing necessary, and one of the suggestions was to implement aliases for the command words. Your job is to implement such aliases. // //[TIP] //Do take a look at <> before attempting to modify the `Logic` component. @@ -844,10 +841,10 @@ A project often depends on third-party libraries. For example, Address Book depe //+ //**** //* Hints -//** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). -//** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. +//** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logicManager/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logicManager/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). +//** link:{repoURL}/src/main/java/seedu/address/logicManager/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. //* Solution -//** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. +//** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logicManager/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. //** Add new tests for each of the aliases that you have added. //** Update the user guide to document the new aliases. //** See this https://github.com/se-edu/addressbook-level4/pull/785[PR] for the full solution. @@ -856,7 +853,7 @@ A project often depends on third-party libraries. For example, Address Book depe //[discrete] //==== `Model` component // -//*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. +//*Scenario:* You are in charge of `model`. One day, the `logicManager`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. // //[TIP] //Do take a look at <> before attempting to modify the `Model` component. @@ -866,8 +863,8 @@ A project often depends on third-party libraries. For example, Address Book depe //**** //* Hints //** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] API need to be updated. -//** Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags? -//** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. +//** Think about how you can use SLAP to design the method. Where should we place the main logicManager of deleting tags? +//** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logicManager. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. //* Solution //** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`]. Loop through each person, and remove the `tag` from each person. //** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `AddressBook#removeTag(Tag)`. @@ -901,7 +898,7 @@ A project often depends on third-party libraries. For example, Address Book depe //* Solution //** You can modify the existing test methods for `PersonCard` 's to include testing the tag's color as well. //** See this https://github.com/se-edu/addressbook-level4/pull/798[PR] for the full solution. -//*** The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storage, so that tags retain their colors even if the hash code algorithm changes. +//*** The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storageFile, so that tags retain their colors even if the hash code algorithm changes. //**** // //. Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). @@ -953,7 +950,7 @@ A project often depends on third-party libraries. For example, Address Book depe //[discrete] //==== `Storage` component // -//*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storage. +//*Scenario:* You are in charge of `storageFile`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storageFile. // //[TIP] //Do take a look at <> before attempting to modify the `Storage` component. @@ -962,8 +959,8 @@ A project often depends on third-party libraries. For example, Address Book depe //+ //**** //* Hint -//** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. -//** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/JsonAddressBookStorage.java[`JsonAddressBookStorage`] class. +//** Add the API method in link:{repoURL}/src/main/java/seedu/address/storageFile/AddressBookStorage.java[`AddressBookStorage`] interface. +//** Implement the logicManager in link:{repoURL}/src/main/java/seedu/address/storageFile/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storageFile/JsonAddressBookStorage.java[`JsonAddressBookStorage`] class. //* Solution //** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. //**** @@ -989,17 +986,17 @@ A project often depends on third-party libraries. For example, Address Book depe //==== Step-by-step Instructions // //===== [Step 1] Logic: Teach the app to accept 'remark' which does nothing -//Let's start by teaching the application how to parse a `remark` command. We will add the logic of `remark` later. +//Let's start by teaching the application how to parse a `remark` command. We will add the logicManager of `remark` later. // //**Main:** // -//. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. -//. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. +//. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logicManager/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. +//. Modify link:{repoURL}/src/main/java/seedu/address/logicManager/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. // //**Tests:** // //. Add `RemarkCommandTest` that tests that `execute()` throws an Exception. -//. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. +//. Add new test method to link:{repoURL}/src/test/java/seedu/address/logicManager/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. // //===== [Step 2] Logic: Teach the app to accept 'remark' arguments //Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` @@ -1008,14 +1005,14 @@ A project often depends on third-party libraries. For example, Address Book depe // //. Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. //. Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. -//. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. +//. Modify link:{repoURL}/src/main/java/seedu/address/logicManager/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. // //**Tests:** // //. Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. //. Add `RemarkCommandParserTest` that tests different boundary values //for `RemarkCommandParser`. -//. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. +//. Modify link:{repoURL}/src/test/java/seedu/address/logicManager/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. // //===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` //Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. @@ -1048,10 +1045,10 @@ A project often depends on third-party libraries. For example, Address Book depe // //. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. //. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). -//. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `data/addressbook.json` so that the application will load the sample data when you launch it.) +//. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample model (delete your `model/addressbook.json` so that the application will load the sample model when you launch it.) // //===== [Step 6] Storage: Add `Remark` field to `JsonAdaptedPerson` class -//We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/JsonAdaptedPerson.java[`JsonAdaptedPerson`] to include a `Remark` field so that it will be saved. +//We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storageFile/JsonAdaptedPerson.java[`JsonAdaptedPerson`] to include a `Remark` field so that it will be saved. // //**Main:** // @@ -1080,16 +1077,16 @@ A project often depends on third-party libraries. For example, Address Book depe // //. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the now-functioning remark label. // -//===== [Step 8] Logic: Implement `RemarkCommand#execute()` logic -//We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logic for our `remark` command. +//===== [Step 8] Logic: Implement `RemarkCommand#execute()` logicManager +//We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logicManager for our `remark` command. // //**Main:** // -//. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. +//. Replace the logicManager in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logicManager to modify the remarks of a person. // //**Tests:** // -//. Update `RemarkCommandTest` to test that the `execute()` logic works. +//. Update `RemarkCommandTest` to test that the `execute()` logicManager works. // //==== Full Solution // @@ -1264,9 +1261,9 @@ _{More to be added}_ . Should work on any <> as long as it has Java 9 or higher installed. . Should be able to hold up a fully packed schedule, three times over, without a noticeable sluggishness in performance for typical usage. . A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. -. The system should respond relatively quickly to user commands so as to not make the user wait around; this is an advantage of using PlanMySem. +. The system should respond relatively quickly to user commands so as to not make the user wait around; this is an advantage of using *PlanMySem*. . The system should take up relatively little space on the local machine so as to cater to all students and OS. -. The system should be easy to use, intuitive and simple, such that any student regardless of past experience with calendar/scheduling softwares is able to use it. +. The system should be easy to use, intuitive and simple, such that any student regardless of past experience with calendar/scheduling software is able to use it. . The system should be flexible to allow all kinds of schedules that target users might have. . The data should be encrypted to prevent private data from being accessed. @@ -1279,7 +1276,7 @@ Windows, Linux, Unix, OS-X //// Should only include this if we actually make any mention to private planner detail. [[private-planner-detail]] Private planner detail:: -Any data stored on to the planner is not meant to be shared with others, unless the data is willingly exported. +Any model stored on to the planner is not meant to be shared with others, unless the model is willingly exported. //// [appendix] diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index ca7198a46..43f4d0918 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -20,45 +20,45 @@ endif::[] By: `Team T08-3` Since: `Jan 2019` Licence: `MIT` == Introduction -Welcome to _PlanMySem_! +Welcome to *PlanMySem*! -_PlanMySem_ is a text-based (Command Line Interface) scheduling/calendar application that targets NUS students and staff who prefer to use a desktop application for managing their schedule/calendar. -_PlanMySem_ automatically creates a planner that is synchronised according to the NUS academic calendar for the current semester and enables easy creation, editing and deleting of items. +*PlanMySem* is a text-based (Command Line Interface) scheduling/calendar application that targets NUS students and staff who prefer to use a desktop application for managing their schedule/calendar. +*PlanMySem* automatically creates a planner that is synchronised according to the NUS academic calendar for the current semester and enables easy creation, editing and deleting of items. Special weeks such as recess week and reading week are taken into account within our unique recursion system. Items can then be efficiently managed via the intuitive tagging system. -_PlanMySem_ is optimized for those who prefer to work with a Command Line Interface (CLI) and/or are learning to work more efficiently with CLI tools. Additionally, unlike traditional calendar/scheduling applications, _PlanMySem_ utilizes minimal resources on the user’s machine while still allowing the user to view their schedules swiftly and efficiently. +*PlanMySem* is optimized for those who prefer to work with a Command Line Interface (CLI) and/or are learning to work more efficiently with CLI tools. Additionally, unlike traditional calendar/scheduling applications, *PlanMySem* utilizes minimal resources on the user’s machine while still allowing the user to view their schedules swiftly and efficiently. {zwsp} {zwsp} == About this User Guide -This user guide provides a quick start guide for you to easily setup and install _PlanMySem_, documentation of all the various features _PlanMySem_ offers, frequently asked questions and a summary of the available commands. To navigate between the different sections, you could use the table of contents above. +This user guide provides a quick start guide for you to easily setup and install *PlanMySem*, documentation of all the various features *PlanMySem* offers, frequently asked questions and a summary of the available commands. To navigate between the different sections, you could use the table of contents above. For ease of communication, this document will refer to lessons/activities/events/appointments that you might add into the planner as _slots_. Additionally, throughout this user guide, there will be various icons used as described below. [TIP] -This is a tip. Follow these suggested tips to make your life much simpler when using _PlanMySem_! +This is a tip. Follow these suggested tips to make your life much simpler when using *PlanMySem*! [NOTE] -This is a note. These are things for you to take note of when using _PlanMySem_. +This is a note. These are things for you to take note of when using *PlanMySem*. [IMPORTANT] -This is a sign-post dictating important information. These are information that you will surely need to know to use _PlanMySem_ properly. +This is a sign-post dictating important information. These are information that you will surely need to know to use *PlanMySem* efficiently. [CAUTION] This is a sign-post informing caution. Please take note of these items and exercise some care. [WARNING] -This is a rule. Ensure that you follow these rule to make your life using _PlanMySem_ a pleasant one. +This is a rule. Ensure that you follow these rules to ensure proper usage of *PlanMySem*. {zwsp} {zwsp} == Quick Start -This section guides you through the installation of _PlanMySem_ and provides a few example commands you may try. +This section guides you through the installation of *PlanMySem* and provides a few example commands you may try. . Ensure you have Java version `9` or later installed in your Computer. . Download the latest `planmysem.jar` link:{repoURL}/releases[here]. @@ -86,11 +86,11 @@ Add a _slot_, named "CS2113T" on the coming monday, from 0800hrs to 0900hrs with [[Features]] == Features -This section displays the available features of _PlanMySem_ together with examples for you to refer to. +This section displays the available features of *PlanMySem* together with examples for you to refer to. *Tagging System* -Unlike other commercial calendar/scheduling/planner software, _PlanMySem_ makes use of a tagging system to manage _slots_. +Unlike other commercial calendar/scheduling/planner software, *PlanMySem* makes use of a tagging system to manage _slots_. Using tags to tag _slots_ will make tasks easier for you in the future. Performing tasks such as viewing, deleting and editing _slots_ will be more efficient. @@ -119,7 +119,7 @@ You can save time by utilizing the alternate and shortcut commands. E.g. instead *Identifiers and Parameters* -Identifiers in _PlanMySem_ are designed to be, short and easy to memorise. +Identifiers in *PlanMySem* are designed to be, short and easy to memorise. Once you are familiarised with them, they should be intuitive to use to add your parameters. The table of Identifiers and Parameters and their descriptions (Table 1) below is useful for your reference as you jump right into grasping the system. @@ -170,7 +170,7 @@ Identifiers may be appended with a `n` to dictate "new". + E.g. `nt/NEW_TAG` signifies new tags in which you want to replace existing tags with. [CAUTION] -While table 1 shows you all the identifiers and parameters that _PlanMySem_ uses, there are some commands that do not make use of identifiers nor parameters. +While table 1 shows you all the identifiers and parameters that *PlanMySem* uses, there are some commands that do not make use of identifiers nor parameters. The view command is one such exception that make use of keywords that must be typed in a specific order. // @@ -478,13 +478,13 @@ There is no need to save manually. {zwsp} == FAQ -*Q*: How do I transfer my data to another Computer? + -*A*: In order to transfer your data to another Computer, you should: +*Q*: How do I transfer my data to another computer? + +*A*: In order to transfer your data to another computer, you should: 1. Install the app on the other computer + -2. Copy _PlanMySem.txt_ from your old _PlanMySem_ folder and paste it into the new _PlanMySem_ folder. + +2. Transfer _PlanMySem.txt_ from your old *PlanMySem* folder and place it into the new *PlanMySem* folder. + -This will overwrite the empty data file it creates with the file that contains the data of your previous _PlanMySem_ folder. +This will overwrite the empty data file it creates with the file that contains the data of your previous *PlanMySem* folder. {zwsp} {zwsp} @@ -510,13 +510,13 @@ This will overwrite the empty data file it creates with the file that contains t //* *View planner* : `view day [DATE] | view week [WEEK] | view month [MONTH]` + //E.g.`view month` //* *View all details* : `view all` -//* *Clear all data* : `clear` +//* *Clear all model* : `clear` //* *Exit the program* : `exit` //* *Export .ics file* : `export` //* *Import .ics file* : `import FILENAME` === General Commands -General commands that you might find useful in helping you to navigate and configure _PlanMySem_: +General commands that you might find useful in helping you to navigate and configure *PlanMySem*: [width="100%",cols="20%,<30%,<20%,<30",options="header"] |======================================================================= |Task |Purpose |Command |Example @@ -531,7 +531,7 @@ General commands that you might find useful in helping you to navigate and confi |_<>_ |Clear your planner | `clear` | `clear` -|_<>_ |Exit the _PlanMySem_ | `exit` | `exit` +|_<>_ |Exit the *PlanMySem* | `exit` | `exit` |======================================================================= {zwsp} diff --git a/docs/diagrams/DataClassDiagram.uml b/docs/diagrams/DataClassDiagram.uml deleted file mode 100644 index 7d03df367..000000000 --- a/docs/diagrams/DataClassDiagram.uml +++ /dev/null @@ -1,185 +0,0 @@ - - - JAVA - planmysem.data.Planner - - planmysem.data.semester.ReadOnlyDay - planmysem.data.semester.Day - planmysem.data.recurrence.Recurrence - planmysem.data.exception.DuplicateDataException - planmysem.data.tag.Tag - planmysem.data.exception.IllegalValueException - planmysem.data.slot.Slot - planmysem.data.semester.Semester - planmysem.data.slot.ReadOnlySlot - planmysem.data.Planner - planmysem.data.semester.ReadOnlySemester - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - All - private - - diff --git a/docs/diagrams/LogicClassDiagram.uml b/docs/diagrams/LogicClassDiagram.uml index f3461f1e9..9854bca26 100644 --- a/docs/diagrams/LogicClassDiagram.uml +++ b/docs/diagrams/LogicClassDiagram.uml @@ -1,365 +1,139 @@ JAVA - planmysem.logic.Logic + planmysem.storage.Storage - planmysem.commands.EditCommand - planmysem.commands.HistoryCommand - planmysem.parser.Parser - planmysem.commands.FindCommand - planmysem.commands.HelpCommand - planmysem.commands.Command - planmysem.commands.ExportCommand - planmysem.commands.ExitCommand - planmysem.commands.ListCommand - planmysem.logic.Logic - planmysem.commands.CommandResult - planmysem.commands.ViewCommand - planmysem.commands.ClearCommand - planmysem.commands.IncorrectCommand - planmysem.parser.CommandHistory - planmysem.commands.AddCommand - planmysem.commands.DeleteCommand + planmysem.logic.commands.Command + planmysem.logic.parser.ParserManager + planmysem.logic.commands.EditCommand + planmysem.logic.commands.AddCommand + planmysem.logic.CommandHistory + planmysem.logic.commands.CommandResult + planmysem.logic.Logic + planmysem.logic.parser.Parser + planmysem.logic.LogicManager - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - planmysem.logic.Logic - + + All private diff --git a/docs/diagrams/ModelClassDiagram.uml b/docs/diagrams/ModelClassDiagram.uml new file mode 100644 index 000000000..104c1c334 --- /dev/null +++ b/docs/diagrams/ModelClassDiagram.uml @@ -0,0 +1,73 @@ + + + JAVA + planmysem.model.Planner + + planmysem.model.ModelManager + planmysem.model.VersionedPlanner + planmysem.model.semester.Day + planmysem.model.slot.Slot + planmysem.model.semester.Semester + planmysem.model.Model + planmysem.model.Planner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All + private + + diff --git a/docs/diagrams/StorageClassDiagram.uml b/docs/diagrams/StorageClassDiagram.uml index f48c4b79e..7bfc8c777 100644 --- a/docs/diagrams/StorageClassDiagram.uml +++ b/docs/diagrams/StorageClassDiagram.uml @@ -3,54 +3,48 @@ JAVA planmysem.storage.StorageFile - planmysem.storage.jaxb.AdaptedSlot - planmysem.storage.jaxb.AdaptedTag - planmysem.storage.jaxb.AdaptedDay - planmysem.storage.jaxb.AdaptedPlanner - planmysem.storage.jaxb.AdaptedSemester - planmysem.storage.StorageFile - planmysem.storage.KeyStorage - planmysem.storage.Encryptor + planmysem.storage.jaxb.AdaptedSlot + planmysem.storage.jaxb.AdaptedDay + planmysem.storage.jaxb.AdaptedPlanner + planmysem.storage.jaxb.AdaptedSemester + planmysem.storage.StorageFile + planmysem.storage.Storage - - - - + + + + - - - - - - - - - - - - + + + + - - + + - - - - + + + + - - + + + + - - + + + planmysem.storage.StorageFile + All private diff --git a/docs/diagrams/UiClassDiagram.uml b/docs/diagrams/UiClassDiagram.uml new file mode 100644 index 000000000..0189b1646 --- /dev/null +++ b/docs/diagrams/UiClassDiagram.uml @@ -0,0 +1,67 @@ + + + JAVA + planmysem.ui.Ui + + planmysem.logic + planmysem.ui.MainWindow + planmysem.ui.Ui + planmysem.ui.Gui + planmysem.model + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All + private + + diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx index 30453daf4..b27be3ed8 100644 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index 491625739..f8b3d8286 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png new file mode 100644 index 000000000..eace1b592 Binary files /dev/null and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/OverallClassDiagram.png b/docs/images/OverallClassDiagram.png new file mode 100644 index 000000000..a916b09a0 Binary files /dev/null and b/docs/images/OverallClassDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 29de89ba7..9d0f1a071 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/team/seanieyap.adoc b/docs/team/seanieyap.adoc index 1f3917996..f2d6b2771 100644 --- a/docs/team/seanieyap.adoc +++ b/docs/team/seanieyap.adoc @@ -9,6 +9,10 @@ == Overview +This portfolio documents my contributions to _PlanMySem_, a software engineering module project. +This module was taken in my third year during the second semester. + + AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. == Summary of contributions diff --git a/src/planmysem/Main.java b/src/planmysem/Main.java index 77adaf88e..a0fbfc073 100644 --- a/src/planmysem/Main.java +++ b/src/planmysem/Main.java @@ -3,7 +3,11 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; -import planmysem.logic.Logic; +import planmysem.logic.LogicManager; +import planmysem.model.Model; +import planmysem.model.ModelManager; +import planmysem.storage.Storage; +import planmysem.storage.StorageFile; import planmysem.ui.Gui; import planmysem.ui.Stoppable; @@ -23,7 +27,9 @@ public static void main(String[] args) { @Override public void start(Stage primaryStage) throws Exception { - Gui gui = new Gui(new Logic(), VERSION); + Storage storageFile = new StorageFile(); + Model modelManager = new ModelManager(); + Gui gui = new Gui(new LogicManager(storageFile, modelManager), VERSION); gui.start(primaryStage, this); } diff --git a/src/planmysem/commands/Command.java b/src/planmysem/commands/Command.java deleted file mode 100644 index c84ab0584..000000000 --- a/src/planmysem/commands/Command.java +++ /dev/null @@ -1,96 +0,0 @@ -package planmysem.commands; - -import static planmysem.ui.Gui.DISPLAYED_INDEX_OFFSET; - -import java.time.LocalDate; -import java.util.Map; - -import javafx.util.Pair; -import planmysem.common.Messages; -import planmysem.data.Planner; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; - -/** - * Represents an executable command. - */ -public abstract class Command { - protected Planner planner; - protected Map> relevantSlots; - private int targetIndex = -1; - - /** - * @param slots last visible listing index of the target person - */ - // public Command(List> slots) { - // this.relevantSlots = slots; - // } - - /** - * @param targetIndex last visible listing index of the target person - */ - public Command(int targetIndex) { - this.targetIndex = targetIndex; - } - - protected Command() { - } - - /** - * Executes the command and returns the result. - */ - public abstract CommandResult execute(); - - /** - * Constructs a feedback message to summarise an operation that displayed a listing of persons. - * - * @param slots used to generate summary - * @return summary message for persons displayed - */ - public static String getMessageForSlotsListShownSummary(Map> slots) { - return String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, slots.size()); - } - - /** - * Supplies the data the command will operate on. - */ - public void setData(Planner planner, Map> slots) { - this.planner = planner; - this.relevantSlots = slots; - } - - public int getTargetIndex() { - return targetIndex; - } - - // public void setTargetIndex(int targetIndex) { - // this.targetIndex = targetIndex; - // } - - - /** - * Extracts the the target Day in the last shown list from the given arguments. - * - * @throws IndexOutOfBoundsException if the target index is out of bounds of the last viewed listing - */ - // protected Map> getTargetSlots() throws IndexOutOfBoundsException { - // return relevantSlots; - // } - - protected Pair> getTargetSlot() throws IndexOutOfBoundsException { - if (relevantSlots == null || relevantSlots.size() < targetIndex) { - throw new IndexOutOfBoundsException(); - } - - int count = 0; - for (Map.Entry> entry : relevantSlots.entrySet()) { - if (count == targetIndex - DISPLAYED_INDEX_OFFSET) { - return new Pair<>(entry.getKey(), entry.getValue()); - } - count++; - } - - throw new IndexOutOfBoundsException(); - } -} diff --git a/src/planmysem/commands/DeleteCommand.java b/src/planmysem/commands/DeleteCommand.java deleted file mode 100644 index 1500054f3..000000000 --- a/src/planmysem/commands/DeleteCommand.java +++ /dev/null @@ -1,78 +0,0 @@ -package planmysem.commands; - -import java.time.LocalDate; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -import javafx.util.Pair; -import planmysem.common.Messages; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; - -/** - * Adds a person to the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - public static final String COMMAND_WORD_ALT = "del"; - public static final String COMMAND_WORD_SHORT = "d"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Delete single or multiple slots in the Planner." - + "\n\tParameters: " - + "\n\t\tMandatory: t/TAG... or INDEX" - + "\n\tExample 1: " + COMMAND_WORD - + " t/CS2113T t/Tutorial" - + "\n\tExample 2: " + COMMAND_WORD - + " 2"; - - public static final String MESSAGE_SUCCESS_NO_CHANGE = "No Slots were deleted.\n\n%1$s"; - public static final String MESSAGE_SUCCESS = "%1$s Slots deleted.\n\n%2$s\n%3$s"; - - private final Set tags = new HashSet<>(); - - /** - * Convenience constructor using raw values. - */ - public DeleteCommand(Set tags) { - this.tags.addAll(tags); - } - - /** - * Convenience constructor using raw values. - */ - public DeleteCommand(int index) { - super(index); - } - - @Override - public CommandResult execute() { - Map> selectedSlots = new TreeMap<>(); - - if (getTargetIndex() == -1) { - selectedSlots.putAll(planner.getSlots(tags)); - - if (selectedSlots.size() == 0) { - return new CommandResult(String.format(MESSAGE_SUCCESS_NO_CHANGE, - Messages.craftSelectedMessage(tags))); - } - } else { - try { - final Pair> target = getTargetSlot(); - selectedSlots.put(target.getKey(), target.getValue()); - } catch (IndexOutOfBoundsException ie) { - return new CommandResult(Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); - } - } - - // perform deletion of slots from the planner - for (Map.Entry> entry: selectedSlots.entrySet()) { - planner.removeSlot(entry.getKey(), entry.getValue().getValue()); - } - - return new CommandResult(String.format(MESSAGE_SUCCESS, selectedSlots.size(), - Messages.craftSelectedMessage(tags), Messages.craftSelectedMessage("Deleted Slots:", selectedSlots))); - } -} diff --git a/src/planmysem/commands/IncorrectCommand.java b/src/planmysem/commands/IncorrectCommand.java deleted file mode 100644 index dd1c71144..000000000 --- a/src/planmysem/commands/IncorrectCommand.java +++ /dev/null @@ -1,18 +0,0 @@ -package planmysem.commands; - -/** - * Represents an incorrect command. Upon execution, produces some feedback to the user. - */ -public class IncorrectCommand extends Command { - - public final String feedbackToUser; - - public IncorrectCommand(String feedbackToUser) { - this.feedbackToUser = feedbackToUser; - } - - @Override - public CommandResult execute() { - return new CommandResult(feedbackToUser); - } -} diff --git a/src/planmysem/common/Clock.java b/src/planmysem/common/Clock.java new file mode 100644 index 000000000..62f9eab5f --- /dev/null +++ b/src/planmysem/common/Clock.java @@ -0,0 +1,24 @@ +package planmysem.common; + +import java.time.Instant; +import java.time.ZoneId; + +/** + * Utility methods + */ +public class Clock { + + private static java.time.Clock clock = java.time.Clock.systemDefaultZone(); + + public static java.time.Clock get() { + return clock; + } + + /** + * Checks whether any of the given items are null. + */ + public static void set(String dateTime) { + clock = java.time.Clock.fixed(Instant.parse(dateTime), ZoneId.of("UTC")); + } +} + diff --git a/src/planmysem/common/Messages.java b/src/planmysem/common/Messages.java index 79a036666..2fd48dcb8 100644 --- a/src/planmysem/common/Messages.java +++ b/src/planmysem/common/Messages.java @@ -5,8 +5,8 @@ import java.util.Set; import javafx.util.Pair; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; /** * Container for user visible messages. @@ -35,10 +35,12 @@ public class Messages { + "\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"; + public static final String MESSAGE_INVALID_TAG = "Tags cannot be empty !"; + public static final String MESSAGE_ILLEGAL_VALUE = "Illegal value detected!"; /** - * Craft selected message. + * Craft selected message via tags. */ public static String craftSelectedMessage(Set tags) { StringBuilder sb = new StringBuilder(); @@ -56,6 +58,17 @@ public static String craftSelectedMessage(Set tags) { return sb.toString(); } + /** + * Craft selected message via index. + */ + public static String craftSelectedMessage(int index) { + StringBuilder sb = new StringBuilder(); + sb.append("Selected index: "); + sb.append(index); + + return sb.toString(); + } + /** * Craft selected message with header. */ diff --git a/src/planmysem/common/Utils.java b/src/planmysem/common/Utils.java index 414fdc762..5d20cced6 100644 --- a/src/planmysem/common/Utils.java +++ b/src/planmysem/common/Utils.java @@ -22,7 +22,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)?(AM|PM)"); + Pattern.compile("(1[012]|[1-9]):[0-5][0-9](\\s)?(?i)(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 = @@ -142,13 +142,16 @@ public static String parseDate(LocalDate date) { * Parse String to 12 hour or 24 hour time format. */ public static LocalTime parseTime(String time) { - LocalTime result = null; - if (time != null && TWELVE_HOUR_FORMAT.matcher(time).matches()) { - result = LocalTime.parse(time, DateTimeFormatter.ofPattern("h[h]:mm a")); + if (time == null) { + return null; + } + if (TWELVE_HOUR_FORMAT.matcher(time).matches()) { + return LocalTime.parse(time.toUpperCase(), DateTimeFormatter.ofPattern("h[h]:mm a")); } else if (TWENTY_FOUR_HOUR_FORMAT.matcher(time).matches()) { - result = LocalTime.parse(time, DateTimeFormatter.ofPattern("H[H]:mm")); + return LocalTime.parse(time, DateTimeFormatter.ofPattern("H[H]:mm")); } - return result; + + return null; } /** @@ -166,21 +169,6 @@ public static int parseInteger(String value) { } } - /** - * Convert set of strings into set of tags. - */ - // public static Set parseTags(Set tags) { - // if (tags == null) { - // return null; - // } - // Set tagSet = new HashSet<>(); - // for (String tag : tags) { - // tagSet.add(new String(tag)); - // } - // return tagSet; - // } - - /** * Get the time difference between two LocalTimes in minutes. */ diff --git a/src/planmysem/data/exception/IllegalValueException.java b/src/planmysem/common/exceptions/IllegalValueException.java similarity index 52% rename from src/planmysem/data/exception/IllegalValueException.java rename to src/planmysem/common/exceptions/IllegalValueException.java index 327776901..bef2195df 100644 --- a/src/planmysem/data/exception/IllegalValueException.java +++ b/src/planmysem/common/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package planmysem.data.exception; +package planmysem.common.exceptions; /** * Signals that some given data does not fulfill some constraints. @@ -10,4 +10,12 @@ public class IllegalValueException extends Exception { public IllegalValueException(String message) { super(message); } + + /** + * @param message should contain relevant information on the failed constraint(s) + * @param cause of the main exception + */ + public IllegalValueException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/planmysem/data/Planner.java b/src/planmysem/data/Planner.java deleted file mode 100644 index 9aacecc01..000000000 --- a/src/planmysem/data/Planner.java +++ /dev/null @@ -1,175 +0,0 @@ -package planmysem.data; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -import javafx.util.Pair; -import planmysem.data.semester.Day; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.semester.Semester; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; - -/** - * Represents the entire Planner. Contains the data of the Planner. - */ -public class Planner { - private final Semester semester; - - /** - * Creates an empty planner. - */ - public Planner() { - semester = Semester.generateSemester(LocalDate.now()); - } - - /** - * Constructs a Planner with the given data. - * - * @param semester external changes to this will not affect this Planner - */ - public Planner(Semester semester) { - this.semester = new Semester(semester); - } - - /** - * Adds a day to the Planner. - * - * @throws Semester.DuplicateDayException if a date is not found in the semester. - */ - // public void addDay(LocalDate date, Day day) throws Semester.DuplicateDayException { - // semester.addDay(date, day); - // } - - /** - * Adds a slot to the Planner. - * - */ - public Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { - return semester.addSlot(date, slot); - } - - /** - * Removes a Slot in the Planner. - */ - public void removeSlot(LocalDate date, ReadOnlySlot slot) { - semester.removeSlot(date, slot); - } - - /** - * Edit specific slot within the planner. - */ - public void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, - LocalTime startTime, int duration, String name, String location, - String description, Set tags) { - semester.editSlot(targetDate, targetSlot, date, startTime, duration, name, location, description, tags); - } - - /** - * Checks if an slot exists in planner. - */ - public boolean containsSlot(LocalDate date, ReadOnlySlot slot) { - return semester.contains(date, slot); - } - - /** - * Checks if an equivalent Day exists in the Planner. - */ - public boolean containsDay(ReadOnlyDay day) { - return semester.contains(day); - } - - /** - * Checks if an equivalent Day exists in the Planner. - */ - public boolean containsDay(LocalDate date) { - return semester.contains(date); - } - - /** - * Removes the equivalent day from the Planner. - * - * @throws Semester.DateNotFoundException if no such Day could be found. - */ - public void removeDay(ReadOnlyDay day) throws Semester.DateNotFoundException { - semester.remove(day); - } - - /** - * Removes the equivalent day from the Planner. - * - * @throws Semester.DateNotFoundException if no such Day could be found. - */ - public void removeDay(LocalDate date) throws Semester.DateNotFoundException { - semester.remove(date); - } - - /** - * Clears all days from the Planner. - */ - public void clearDays() { - semester.clearDays(); - } - - /** - * Clears all slots from the Planner. - */ - public void clearSlots() { - semester.clearSlots(); - } - - /** - * Defensively copy the Semester in the Planner at the time of the call. - */ - public Semester getSemester() { - return semester; - } - - /** - * gets all days in the Planner. - */ - public HashMap getAllDays() { - return semester.getDays(); - } - - /** - * gets specific day in the Planner. - */ - public Day getDay(LocalDate date) { - return getAllDays().get(date); - } - - /** - * gets all slots in the Planner containing all specified tags. - */ - public Map> getSlots(Set tags) { - final Map> selectedSlots = new TreeMap<>(); - - for (Map.Entry entry : getAllDays().entrySet()) { - for (Slot slot : entry.getValue().getSlots()) { - if (slot.getTags().containsAll(tags)) { - selectedSlots.put(entry.getKey(), new Pair<>(entry.getValue(), slot)); - } - } - } - - return selectedSlots; - } - - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Planner // instanceof handles nulls - && this.semester.equals(((Planner) other).semester)); - } - - @Override - public int hashCode() { - return semester.hashCode(); - } -} diff --git a/src/planmysem/parser/CommandHistory.java b/src/planmysem/logic/CommandHistory.java similarity index 98% rename from src/planmysem/parser/CommandHistory.java rename to src/planmysem/logic/CommandHistory.java index a2098f3d8..aff77e52c 100644 --- a/src/planmysem/parser/CommandHistory.java +++ b/src/planmysem/logic/CommandHistory.java @@ -1,4 +1,4 @@ -package planmysem.parser; +package planmysem.logic; import static java.util.Objects.requireNonNull; @@ -18,6 +18,7 @@ public CommandHistory() {} public CommandHistory(CommandHistory commandHistory) { userInputHistory.addAll(commandHistory.userInputHistory); } + /** * Appends {@code userInput} to the list of user input entered. */ @@ -55,4 +56,3 @@ public int hashCode() { return userInputHistory.hashCode(); } } - diff --git a/src/planmysem/logic/Logic.java b/src/planmysem/logic/Logic.java index 56b5f9df6..024b46a66 100644 --- a/src/planmysem/logic/Logic.java +++ b/src/planmysem/logic/Logic.java @@ -2,106 +2,38 @@ import java.time.LocalDate; import java.util.Map; -import java.util.Optional; - -import javax.xml.bind.JAXBException; +import javafx.collections.ObservableList; import javafx.util.Pair; -import planmysem.commands.Command; -import planmysem.commands.CommandResult; -import planmysem.data.Planner; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.parser.Parser; -import planmysem.storage.StorageFile; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.logic.parser.exceptions.ParseException; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; /** - * Represents the main Logic of the Planner. + * API of the Logic component */ -public class Logic { - private StorageFile storage; - private Planner planner; +public interface Logic { /** - * The list of Slots shown to the user most recently. + * Execute command. */ - private Map> lastShownSlots; - - public Logic() throws Exception { - setStorage(initializeStorage()); - setPlanner(storage.load()); - } - - public Logic(StorageFile storageFile, Planner planner) { - setStorage(storageFile); - setPlanner(planner); - } - - public void setStorage(StorageFile storage) { - this.storage = storage; - } - - public void setPlanner(Planner planner) { - this.planner = planner; - } - - /** - * Creates the StorageFile object based on the user specified path (if any) or the default storage path. - * - * @throws StorageFile.InvalidStorageFilePathException if the target file path is incorrect. - */ - private StorageFile initializeStorage() throws JAXBException, StorageFile.InvalidStorageFilePathException { - return new StorageFile(); - } - - public String getStorageFilePath() { - return storage.getPath(); - } - - /** - * Unmodifiable view of the current last shown list. - */ - public Map> getLastShownSlots() { - return lastShownSlots; - } - - protected void setLastShownSlots(Map> slots) { - lastShownSlots = slots; - } + CommandResult execute(String commandText) throws CommandException, ParseException; /** - * Parses the user command, executes it, and returns the result. - * - * @throws Exception if there was any problem during command execution. + * Gets the storage file's path. */ - public CommandResult execute(String userCommandText) throws Exception { - Command command = new Parser().parseCommand(userCommandText); - CommandResult result = execute(command); - recordResult(result); - return result; - } + String getStorageFilePath(); /** - * Executes the command, updates storage, and returns the result. - * - * @param command user command - * @return result of the command - * @throws Exception if there was any problem during command execution. + * Gets unmodifiable view of the current last shown list. */ - private CommandResult execute(Command command) throws Exception { - command.setData(planner, lastShownSlots); - CommandResult result = command.execute(); - storage.save(planner); - return result; - } + Map> getLastShownSlots(); /** - * Updates the {@link #lastShownSlots} if the result contains a list of Days. + * Returns an unmodifiable view of the list of commands entered by the user. + * The list is ordered from the least recent command to the most recent command. */ - private void recordResult(CommandResult result) { - final Optional>> slots = result.getRelevantSlots(); - if (slots.isPresent()) { - lastShownSlots = slots.get(); - } - } + ObservableList getHistory(); } diff --git a/src/planmysem/logic/LogicManager.java b/src/planmysem/logic/LogicManager.java new file mode 100644 index 000000000..65c9ed9bd --- /dev/null +++ b/src/planmysem/logic/LogicManager.java @@ -0,0 +1,89 @@ +package planmysem.logic; + +import java.time.LocalDate; +import java.util.Map; + +import javafx.collections.ObservableList; +import javafx.util.Pair; +import planmysem.logic.commands.Command; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.logic.parser.ParserManager; +import planmysem.logic.parser.exceptions.ParseException; +import planmysem.model.Model; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.storage.Storage; +import planmysem.storage.StorageFile; + +/** + * Represents the main LogicManager of the Planner. + */ +public class LogicManager implements Logic { + public static final String STORAGE_ERROR = "Could not save data to file: "; + + private final Storage storageFile; + private final Model model; + private final CommandHistory history; + private final ParserManager parserManager; + + public LogicManager(Storage storageFile, Model model) { + this.storageFile = storageFile; + this.model = model; + this.history = new CommandHistory(); + this.parserManager = new ParserManager(); + } + + @Override + public String getStorageFilePath() { + return storageFile.getPath(); + } + + @Override + public Map> getLastShownSlots() { + return model.getLastShownList(); + } + + @Override + public CommandResult execute(String userCommandText) throws CommandException, ParseException { + CommandResult result; + try { + Command command = parserManager.parseCommand(userCommandText); + result = command.execute(model, history); + } finally { + history.add(userCommandText); + } + try { + storageFile.save(model); + } catch (StorageFile.StorageOperationException soe) { + throw new CommandException(STORAGE_ERROR + soe, soe); + } + + return result; + } + + @Override + public ObservableList getHistory() { + return history.getHistory(); + } + + + // /** + // * Creates the StorageFile object based on the user specified path (if any) or the default storageFile path. + // * + // * @throws StorageFile.InvalidStorageFilePathException if the target file path is incorrect. + // */ + // private StorageFile initializeStorage() throws JAXBException, StorageFile.InvalidStorageFilePathException { + // return new StorageFile(); + // } + + // /** + // * Updates the {@link #lastShownSlots} if the commandResult contains a list of Days. + // */ + // private void recordResult(CommandResult commandResult) { + // final Optional>> slots = commandResult.getRelevantSlots(); + // if (slots.isPresent()) { + // lastShownSlots = slots.get(); + // } + // } +} diff --git a/src/planmysem/commands/AddCommand.java b/src/planmysem/logic/commands/AddCommand.java similarity index 68% rename from src/planmysem/commands/AddCommand.java rename to src/planmysem/logic/commands/AddCommand.java index 83f4f534e..9582a9ca3 100644 --- a/src/planmysem/commands/AddCommand.java +++ b/src/planmysem/logic/commands/AddCommand.java @@ -1,4 +1,6 @@ -package planmysem.commands; +package planmysem.logic.commands; + +import static java.util.Objects.requireNonNull; import java.time.LocalDate; import java.time.LocalTime; @@ -6,11 +8,13 @@ import java.util.Set; import java.util.TreeMap; -import planmysem.data.exception.IllegalValueException; -import planmysem.data.recurrence.Recurrence; -import planmysem.data.semester.Day; -import planmysem.data.semester.Semester; -import planmysem.data.slot.Slot; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; +import planmysem.model.recurrence.Recurrence; +import planmysem.model.semester.Day; +import planmysem.model.semester.Semester; +import planmysem.model.slot.Slot; /** * Adds a person to the planner. @@ -44,25 +48,34 @@ public AddCommand(LocalDate date, String name, String location, String descripti /** * Convenience constructor using raw values. - * - * @throws IllegalValueException if any of the raw values are invalid */ public AddCommand(int day, String name, String location, String description, LocalTime startTime, - int duration, Set tags, Set recurrences) throws IllegalValueException { + int duration, Set tags, Set recurrences) { slot = new Slot(name, location, description, startTime, duration, tags); recurrence = new Recurrence(recurrences, day); } + /** + * Creates an AddCommand to add the specified {@code slot} + * and using specific {@code recurrence} + */ + public AddCommand(Slot slot, Recurrence recurrence) { + requireNonNull(slot); + requireNonNull(recurrence); + this.slot = slot; + this.recurrence = recurrence; + } + @Override - public CommandResult execute() { - Set dates = recurrence.generateDates(planner.getSemester()); + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + Set dates = recurrence.generateDates(model.getPlanner().getSemester()); Map days = new TreeMap<>(); for (LocalDate date : dates) { try { - days.put(date, planner.addSlot(date, slot)); + days.put(date, model.addSlot(date, slot)); } catch (Semester.DateNotFoundException dnfe) { - return new CommandResult(MESSAGE_FAIL_OUT_OF_BOUNDS); + throw new CommandException(MESSAGE_FAIL_OUT_OF_BOUNDS); } } @@ -95,4 +108,12 @@ public static String craftSuccessMessage(Map days, Slot slot) { return sb.toString(); } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddCommand // instanceof handles nulls + && slot.equals(((AddCommand) other).slot) + && recurrence.equals(((AddCommand) other).recurrence)); + } } diff --git a/src/planmysem/commands/ClearCommand.java b/src/planmysem/logic/commands/ClearCommand.java similarity index 67% rename from src/planmysem/commands/ClearCommand.java rename to src/planmysem/logic/commands/ClearCommand.java index f93ca7e5e..67664cc0f 100644 --- a/src/planmysem/commands/ClearCommand.java +++ b/src/planmysem/logic/commands/ClearCommand.java @@ -1,4 +1,7 @@ -package planmysem.commands; +package planmysem.logic.commands; + +import planmysem.logic.CommandHistory; +import planmysem.model.Model; /** * Clears the planner. @@ -12,8 +15,8 @@ public class ClearCommand extends Command { public static final String MESSAGE_SUCCESS = "The Planner has been cleared!"; @Override - public CommandResult execute() { - planner.clearSlots(); + public CommandResult execute(Model model, CommandHistory commandHistory) { + model.clearSlots(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/planmysem/logic/commands/Command.java b/src/planmysem/logic/commands/Command.java new file mode 100644 index 000000000..ffb7e774a --- /dev/null +++ b/src/planmysem/logic/commands/Command.java @@ -0,0 +1,21 @@ +package planmysem.logic.commands; + +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; + +/** + * Represents an executable command. + */ +public abstract class Command { + + /** + * Executes the command and returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @param history {@code CommandHistory} which the command should operate on. + * @return feedback message of the operation result for display + */ + public abstract CommandResult execute(Model model, CommandHistory history) throws CommandException; + +} diff --git a/src/planmysem/commands/CommandResult.java b/src/planmysem/logic/commands/CommandResult.java similarity index 59% rename from src/planmysem/commands/CommandResult.java rename to src/planmysem/logic/commands/CommandResult.java index 4b45de3b0..07325bba9 100644 --- a/src/planmysem/commands/CommandResult.java +++ b/src/planmysem/logic/commands/CommandResult.java @@ -1,12 +1,13 @@ -package planmysem.commands; +package planmysem.logic.commands; import java.time.LocalDate; import java.util.Map; +import java.util.Objects; import java.util.Optional; import javafx.util.Pair; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; /** * Represents the result of a command execution. @@ -40,4 +41,27 @@ public Optional>> getRelevantSlot return Optional.ofNullable(slots); } + public String getFeedbackToUser() { + return feedbackToUser; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CommandResult)) { + return false; + } + + CommandResult otherCommandResult = (CommandResult) other; + return feedbackToUser.equals(otherCommandResult.feedbackToUser); + } + + @Override + public int hashCode() { + return Objects.hash(feedbackToUser); + } } diff --git a/src/planmysem/logic/commands/DeleteCommand.java b/src/planmysem/logic/commands/DeleteCommand.java new file mode 100644 index 000000000..f1191ebec --- /dev/null +++ b/src/planmysem/logic/commands/DeleteCommand.java @@ -0,0 +1,100 @@ +package planmysem.logic.commands; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javafx.util.Pair; +import planmysem.common.Messages; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; + +/** + * Adds a person to the address book. + */ +public class DeleteCommand extends Command { + + public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_WORD_ALT = "del"; + public static final String COMMAND_WORD_SHORT = "d"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Delete single or multiple slots in the Planner." + + "\n\tParameters: " + + "\n\t\tMandatory: t/TAG... or INDEX" + + "\n\tExample 1: " + COMMAND_WORD + + " t/CS2113T t/Tutorial" + + "\n\tExample 2: " + COMMAND_WORD + + " 2"; + + public static final String MESSAGE_SUCCESS_NO_CHANGE = "No Slots were deleted.\n\n%1$s"; + public static final String MESSAGE_SUCCESS = "%1$s Slots deleted.\n\n%2$s\n%3$s"; + + private final Set tags = new HashSet<>(); + private final int targetIndex; + + /** + * Convenience constructor using raw values. + */ + public DeleteCommand(Set tags) { + targetIndex = -1; + this.tags.addAll(tags); + } + + /** + * Convenience constructor using raw values. + */ + public DeleteCommand(int index) { + this.targetIndex = index; + } + + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + Map> selectedSlots = new TreeMap<>(); + String messageSelected; + String messageSlots; + + if (targetIndex == -1) { + selectedSlots.putAll(model.getSlots(tags)); + + if (selectedSlots.size() == 0) { + throw new CommandException(String.format(MESSAGE_SUCCESS_NO_CHANGE, + Messages.craftSelectedMessage(tags))); + } + + // perform deletion of slots from the planner + for (Map.Entry> entry : selectedSlots.entrySet()) { + model.removeSlot(entry.getKey(), entry.getValue().getValue()); + } + messageSelected = Messages.craftSelectedMessage(tags); + messageSlots = Messages.craftSelectedMessage("Deleted Slots:", selectedSlots); + } else { + try { + final Pair> target = model.getLastShownItem(targetIndex); + selectedSlots.put(target.getKey(), target.getValue()); + + model.removeSlot(target); + + messageSelected = Messages.craftSelectedMessage(targetIndex); + messageSlots = Messages.craftSelectedMessage("Deleted Slot:", selectedSlots); + } catch (IndexOutOfBoundsException ie) { + throw new CommandException(Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); + } + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, + selectedSlots.size(), messageSelected, messageSlots)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteCommand // instanceof handles nulls + && tags.equals(((DeleteCommand) other).tags) + && targetIndex == ((DeleteCommand) other).targetIndex); + } +} diff --git a/src/planmysem/commands/EditCommand.java b/src/planmysem/logic/commands/EditCommand.java similarity index 64% rename from src/planmysem/commands/EditCommand.java rename to src/planmysem/logic/commands/EditCommand.java index 402f2a396..ff6c47d2f 100644 --- a/src/planmysem/commands/EditCommand.java +++ b/src/planmysem/logic/commands/EditCommand.java @@ -1,17 +1,21 @@ -package planmysem.commands; +package planmysem.logic.commands; import java.time.LocalDate; import java.time.LocalTime; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.StringJoiner; import java.util.TreeMap; import javafx.util.Pair; import planmysem.common.Messages; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; /** * Adds a person to the address book. @@ -31,11 +35,8 @@ public class EditCommand extends Command { + "\n\tExample 2: " + COMMAND_WORD + " 2 nl/COM2 04-01"; - public static final String MESSAGE_SUCCESS_NO_CHANGE = "No Slots were edited.\n\n%1$s"; public static final String MESSAGE_SUCCESS = "%1$s Slots edited.\n\n%2$s\n%3$s"; - public static final String MESSAGE_FAIL_ILLEGAL_VALUE = MESSAGE_SUCCESS - + " Illegal characters were detected."; private final LocalDate date; private final LocalTime startTime; @@ -46,11 +47,14 @@ public class EditCommand extends Command { private final Set tags = new HashSet<>(); private final Set newTags = new HashSet<>(); + private final int targetIndex; + /** - * Convenience constructor using raw values. + * Convenience constructor using raw values. Edit via tags. */ public EditCommand(String name, LocalTime startTime, int duration, String location, String description, Set tags, Set newTags) { + targetIndex = -1; this.date = null; this.startTime = startTime; this.duration = duration; @@ -66,11 +70,11 @@ public EditCommand(String name, LocalTime startTime, int duration, String locati } /** - * Convenience constructor using raw values. + * Convenience constructor using raw values. Edit via index. */ public EditCommand(int index, String name, LocalDate date, LocalTime startTime, int duration, String location, String description, Set newTags) { - super(index); + targetIndex = index; this.date = date; this.startTime = startTime; this.duration = duration; @@ -83,35 +87,48 @@ public EditCommand(int index, String name, LocalDate date, LocalTime startTime, } @Override - public CommandResult execute() { + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { final Map> selectedSlots = new TreeMap<>(); + String messageSelected; + String messageSlots; - if (getTargetIndex() == -1) { - selectedSlots.putAll(planner.getSlots(tags)); + if (targetIndex == -1) { + selectedSlots.putAll(model.getSlots(tags)); if (selectedSlots.size() == 0) { - return new CommandResult(String.format(MESSAGE_SUCCESS_NO_CHANGE, + throw new CommandException(String.format(MESSAGE_SUCCESS_NO_CHANGE, Messages.craftSelectedMessage(tags))); } + + // Need to craft success message earlier to get original instead of edited Slots + messageSlots = craftSuccessMessage(selectedSlots); + + for (Map.Entry> entry : selectedSlots.entrySet()) { + model.editSlot(entry.getKey(), entry.getValue().getValue(), date, + startTime, duration, name, location, description, newTags); + } + + messageSelected = Messages.craftSelectedMessage(tags); } else { try { - final Pair> target = getTargetSlot(); + final Pair> target = model.getLastShownItem(targetIndex); selectedSlots.put(target.getKey(), target.getValue()); - } catch (IndexOutOfBoundsException ie) { - return new CommandResult(Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); - } - } - // Need to craft success message earlier to get original instead of edited Slots - String successMessage = craftSuccessMessage(selectedSlots); + // Need to craft success message earlier to get original instead of edited Slots + messageSlots = craftSuccessMessage(selectedSlots); + + model.editSlot(target.getKey(), target.getValue().getValue(), date, + startTime, duration, name, location, description, newTags); - for (Map.Entry> entry : selectedSlots.entrySet()) { - planner.editSlot(entry.getKey(), entry.getValue().getValue(), date, - startTime, duration, name, location, description, newTags); + messageSelected = Messages.craftSelectedMessage(targetIndex); + + } catch (IndexOutOfBoundsException ie) { + throw new CommandException(Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); + } } return new CommandResult(String.format(MESSAGE_SUCCESS, selectedSlots.size(), - Messages.craftSelectedMessage(tags), successMessage)); + messageSelected, messageSlots)); } /** @@ -184,4 +201,19 @@ public String craftSuccessMessage(Map return sb.toString(); } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EditCommand // instanceof handles nulls + && Objects.equals(date, ((EditCommand) other).date) + && Objects.equals(startTime, ((EditCommand) other).startTime) + && duration == ((EditCommand) other).duration + && Objects.equals(name, ((EditCommand) other).name) + && Objects.equals(location, ((EditCommand) other).location) + && Objects.equals(description, ((EditCommand) other).description) + && tags.equals(((EditCommand) other).tags) + && newTags.equals(((EditCommand) other).newTags) + && targetIndex == ((EditCommand) other).targetIndex); + } } diff --git a/src/planmysem/commands/ExitCommand.java b/src/planmysem/logic/commands/ExitCommand.java similarity index 71% rename from src/planmysem/commands/ExitCommand.java rename to src/planmysem/logic/commands/ExitCommand.java index 4d1c36079..7433fbbb2 100644 --- a/src/planmysem/commands/ExitCommand.java +++ b/src/planmysem/logic/commands/ExitCommand.java @@ -1,4 +1,7 @@ -package planmysem.commands; +package planmysem.logic.commands; + +import planmysem.logic.CommandHistory; +import planmysem.model.Model; /** * Terminates the program. @@ -12,7 +15,7 @@ public class ExitCommand extends Command { public static final String MESSAGE_EXIT_ACKNOWEDGEMENT = "Exiting PlanMySem as requested ..."; @Override - public CommandResult execute() { + public CommandResult execute(Model model, CommandHistory commandHistory) { return new CommandResult(MESSAGE_EXIT_ACKNOWEDGEMENT); } diff --git a/src/planmysem/commands/ExportCommand.java b/src/planmysem/logic/commands/ExportCommand.java similarity index 72% rename from src/planmysem/commands/ExportCommand.java rename to src/planmysem/logic/commands/ExportCommand.java index f4235c9da..2051b0c77 100644 --- a/src/planmysem/commands/ExportCommand.java +++ b/src/planmysem/logic/commands/ExportCommand.java @@ -1,10 +1,12 @@ -package planmysem.commands; +package planmysem.logic.commands; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; -import planmysem.data.semester.AdaptedSemester; +import planmysem.logic.CommandHistory; +import planmysem.model.Model; +import planmysem.model.semester.IcsSemester; /** * Exports the calendar into a .ics file. @@ -17,8 +19,8 @@ public class ExportCommand extends Command { public static final String MESSAGE_EXPORT_ACKNOWEDGEMENT = "Calendar exported"; @Override - public CommandResult execute() { - AdaptedSemester semester = new AdaptedSemester(planner.getSemester()); + public CommandResult execute(Model model, CommandHistory commandHistory) { + IcsSemester semester = new IcsSemester(model.getPlanner().getSemester()); try { BufferedWriter writer = new BufferedWriter(new FileWriter("PlanMySem.ics")); writer.write(semester.toString()); diff --git a/src/planmysem/commands/FindCommand.java b/src/planmysem/logic/commands/FindCommand.java similarity index 82% rename from src/planmysem/commands/FindCommand.java rename to src/planmysem/logic/commands/FindCommand.java index c89e70195..51b49a9ca 100644 --- a/src/planmysem/commands/FindCommand.java +++ b/src/planmysem/logic/commands/FindCommand.java @@ -1,5 +1,5 @@ //@@author marcus-pzj -package planmysem.commands; +package planmysem.logic.commands; import java.time.LocalDate; import java.util.Map; @@ -9,10 +9,12 @@ import javafx.util.Pair; import planmysem.common.Messages; -import planmysem.data.semester.Day; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; +import planmysem.logic.CommandHistory; +import planmysem.model.Model; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; /** @@ -39,10 +41,10 @@ public FindCommand(String name, String tag) { } @Override - public CommandResult execute() { + public CommandResult execute(Model model, CommandHistory commandHistory) { Map> selectedSlots = new TreeMap<>(); - for (Map.Entry entry : planner.getAllDays().entrySet()) { + for (Map.Entry entry : model.getDays().entrySet()) { for (Slot slot : entry.getValue().getSlots()) { if (isFindByName) { if (Pattern.matches(".*" + keyword + ".*", slot.getName())) { @@ -62,7 +64,7 @@ public CommandResult execute() { if (selectedSlots.isEmpty()) { return new CommandResult(MESSAGE_SUCCESS_NONE); } - setData(planner, selectedSlots); + model.setLastShownList(selectedSlots); return new CommandResult(String.format(MESSAGE_SUCCESS, selectedSlots.size(), Messages.craftSelectedMessage(selectedSlots))); diff --git a/src/planmysem/commands/HelpCommand.java b/src/planmysem/logic/commands/HelpCommand.java similarity index 83% rename from src/planmysem/commands/HelpCommand.java rename to src/planmysem/logic/commands/HelpCommand.java index 2d7471c4d..773f580ca 100644 --- a/src/planmysem/commands/HelpCommand.java +++ b/src/planmysem/logic/commands/HelpCommand.java @@ -1,5 +1,7 @@ -package planmysem.commands; +package planmysem.logic.commands; +import planmysem.logic.CommandHistory; +import planmysem.model.Model; /** * Shows help instructions. @@ -23,7 +25,7 @@ public class HelpCommand extends Command { + "\n\n" + ExitCommand.MESSAGE_USAGE; @Override - public CommandResult execute() { + public CommandResult execute(Model model, CommandHistory commandHistory) { return new CommandResult(MESSAGE_ALL_USAGES); } } diff --git a/src/planmysem/commands/HistoryCommand.java b/src/planmysem/logic/commands/HistoryCommand.java similarity index 69% rename from src/planmysem/commands/HistoryCommand.java rename to src/planmysem/logic/commands/HistoryCommand.java index 6dcb9d46a..720ebf5c7 100644 --- a/src/planmysem/commands/HistoryCommand.java +++ b/src/planmysem/logic/commands/HistoryCommand.java @@ -1,12 +1,13 @@ //@@author marcus-pzj -package planmysem.commands; +package planmysem.logic.commands; import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Collections; -import planmysem.parser.CommandHistory; +import planmysem.logic.CommandHistory; +import planmysem.model.Model; /** * Lists all the commands entered by user from the start of app launch. @@ -17,20 +18,9 @@ public class HistoryCommand extends Command { public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; - private final CommandHistory commandHistory; - /** - * TODO: java doc - */ - public HistoryCommand(CommandHistory commandHistory) { - this.commandHistory = commandHistory; - } - - /** - * TODO: description - * @return CommandResult with a list of previousCommands - */ - public CommandResult execute() { + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) { requireNonNull(commandHistory); ArrayList previousCommands = new ArrayList<>(commandHistory.getHistory()); diff --git a/src/planmysem/commands/ListCommand.java b/src/planmysem/logic/commands/ListCommand.java similarity index 83% rename from src/planmysem/commands/ListCommand.java rename to src/planmysem/logic/commands/ListCommand.java index c7322a510..e13b7583b 100644 --- a/src/planmysem/commands/ListCommand.java +++ b/src/planmysem/logic/commands/ListCommand.java @@ -1,5 +1,5 @@ //@@author marcus-pzj -package planmysem.commands; +package planmysem.logic.commands; import java.time.LocalDate; import java.util.Map; @@ -8,10 +8,12 @@ import javafx.util.Pair; import planmysem.common.Messages; -import planmysem.data.semester.Day; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; +import planmysem.logic.CommandHistory; +import planmysem.model.Model; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; /** * Displays a list of all slots in the planner whose name matches the argument keyword. @@ -39,10 +41,10 @@ public ListCommand(String name, String tag) { } @Override - public CommandResult execute() { + public CommandResult execute(Model model, CommandHistory commandHistory) { Map> selectedSlots = new TreeMap<>(); - for (Map.Entry entry : planner.getAllDays().entrySet()) { + for (Map.Entry entry : model.getDays().entrySet()) { for (Slot slot : entry.getValue().getSlots()) { if (isListByName) { if (slot.getName().equalsIgnoreCase(keyword)) { @@ -63,7 +65,7 @@ public CommandResult execute() { if (selectedSlots.isEmpty()) { return new CommandResult(MESSAGE_SUCCESS_NONE); } - setData(this.planner, relevantSlots); + model.setLastShownList(selectedSlots); return new CommandResult(String.format(MESSAGE_SUCCESS, selectedSlots.size(), Messages.craftListMessage(selectedSlots))); diff --git a/src/planmysem/logic/commands/RedoCommand.java b/src/planmysem/logic/commands/RedoCommand.java new file mode 100644 index 000000000..34771a1a0 --- /dev/null +++ b/src/planmysem/logic/commands/RedoCommand.java @@ -0,0 +1,30 @@ +package planmysem.logic.commands; + +import static java.util.Objects.requireNonNull; + +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; + +/** + * Reverts the {@code model}'s address book to its previously undone state. + */ +public class RedoCommand extends Command { + + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_SUCCESS = "Redo success!"; + public static final String MESSAGE_FAILURE = "No more commands to redo!"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (!model.canRedo()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.redo(); + model.setLastShownList(null); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/planmysem/logic/commands/UndoCommand.java b/src/planmysem/logic/commands/UndoCommand.java new file mode 100644 index 000000000..e27a9cb75 --- /dev/null +++ b/src/planmysem/logic/commands/UndoCommand.java @@ -0,0 +1,30 @@ +package planmysem.logic.commands; + +import static java.util.Objects.requireNonNull; + +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; + +/** + * Reverts the {@code model}'s address book to its previous state. + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_SUCCESS = "Undo success!"; + public static final String MESSAGE_FAILURE = "No more commands to undo!"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (!model.canUndo()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.undo(); + model.setLastShownList(null); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/planmysem/commands/ViewCommand.java b/src/planmysem/logic/commands/ViewCommand.java similarity index 93% rename from src/planmysem/commands/ViewCommand.java rename to src/planmysem/logic/commands/ViewCommand.java index e5796f1e0..febbfd811 100644 --- a/src/planmysem/commands/ViewCommand.java +++ b/src/planmysem/logic/commands/ViewCommand.java @@ -1,10 +1,12 @@ -package planmysem.commands; +package planmysem.logic.commands; import java.time.LocalDate; import java.util.HashMap; -import planmysem.data.semester.Day; -import planmysem.data.semester.Semester; +import planmysem.logic.CommandHistory; +import planmysem.model.Model; +import planmysem.model.semester.Day; +import planmysem.model.semester.Semester; /** * Adds a person to the address book. @@ -35,10 +37,10 @@ public ViewCommand(String viewArgs) { } @Override - public CommandResult execute() { + public CommandResult execute(Model model, CommandHistory commandHistory) { String viewType; //String viewSpecifier; - final Semester currentSemester = planner.getSemester(); + final Semester currentSemester = model.getPlanner().getSemester(); String output = null; if ("all".equals(viewArgs)) { diff --git a/src/planmysem/logic/commands/exceptions/CommandException.java b/src/planmysem/logic/commands/exceptions/CommandException.java new file mode 100644 index 000000000..d434bcabd --- /dev/null +++ b/src/planmysem/logic/commands/exceptions/CommandException.java @@ -0,0 +1,17 @@ +package planmysem.logic.commands.exceptions; + +/** + * Represents an error which occurs during execution of a {@link Command}. + */ +public class CommandException extends Exception { + public CommandException(String message) { + super(message); + } + + /** + * Constructs a new {@code CommandException} with the specified detail {@code message} and {@code cause}. + */ + public CommandException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/planmysem/logic/parser/AddCommandParser.java b/src/planmysem/logic/parser/AddCommandParser.java new file mode 100644 index 000000000..b1f20d872 --- /dev/null +++ b/src/planmysem/logic/parser/AddCommandParser.java @@ -0,0 +1,124 @@ +package planmysem.logic.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_TAG; +import static planmysem.common.Messages.MESSAGE_INVALID_TIME; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import planmysem.common.Utils; +import planmysem.logic.commands.AddCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * + * @param args full command args string + * @return the prepared command + */ + public AddCommand parse(String args) throws ParseException { + HashMap> arguments = getParametersWithArguments(args); + + // Name is mandatory + String name = getFirstInSet(arguments.get(PREFIX_NAME)); + + if ((arguments == null || arguments.isEmpty()) + || (name == null || name.isEmpty()) + || arguments.get(PREFIX_DATE_OR_DAY) == null + || arguments.get(PREFIX_START_TIME) == null + || arguments.get(PREFIX_END_TIME) == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + // Either date or day must be present + String dateOrDay = getFirstInSet(arguments.get(PREFIX_DATE_OR_DAY)); + int day = -1; + LocalDate date = Utils.parseDate(dateOrDay); + if (date == null) { + day = Utils.parseDay(dateOrDay); + } + if (day == -1 && date == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_DATE)); + } + + // Start time is mandatory + String stringStartTime = getFirstInSet(arguments.get(PREFIX_START_TIME)); + LocalTime startTime = Utils.parseTime(stringStartTime); + if (startTime == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + } + + // determine if "end time" is a duration or time + String stringEndTime = getFirstInSet(arguments.get(PREFIX_END_TIME)); + int duration = Utils.parseInteger(stringEndTime); + + if (duration == -1) { + LocalTime endTime = Utils.parseTime(stringEndTime); + if (endTime == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + } + duration = Utils.getDuration(startTime, endTime); + } + + // Description is not mandatory and can be null + String description = getFirstInSet(arguments.get(PREFIX_DESCRIPTION)); + + // Location is not mandatory and can be null + String location = getFirstInSet(arguments.get(PREFIX_LOCATION)); + + // Tags is not mandatory + Set tags = arguments.get(PREFIX_TAG); + if (tags != null) { + for (String tag : tags) { + if (tag.length() == 0) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TAG)); + } + } + } else { + tags = new HashSet<>(); + } + + // Recurrences is not mandatory + Set recurrences = arguments.get(PREFIX_RECURRENCE); + + if (day != -1) { + return new AddCommand( + day, + name, + location, + description, + startTime, + duration, + tags, + recurrences + ); + } else { + return new AddCommand( + date, + name, + location, + description, + startTime, + duration, + tags, + recurrences + ); + } + } +} diff --git a/src/planmysem/logic/parser/DeleteCommandParser.java b/src/planmysem/logic/parser/DeleteCommandParser.java new file mode 100644 index 000000000..7b6acb833 --- /dev/null +++ b/src/planmysem/logic/parser/DeleteCommandParser.java @@ -0,0 +1,40 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.HashMap; +import java.util.Set; + +import planmysem.common.Utils; +import planmysem.logic.commands.DeleteCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns an DeleteCommand object for execution. + * + * @param args full command args string + * @return the prepared command + */ + public DeleteCommand parse(String args) throws ParseException { + HashMap> arguments = getParametersWithArguments(args); + String stringIndex = getStartingArgument(args); + int index = Utils.parseInteger(stringIndex); + Set tags = arguments.get(PREFIX_TAG); + + if ((index <= 0 && tags == null) || (index > 0 && tags != null)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + if (index == -1) { + return new DeleteCommand(tags); + } else { + return new DeleteCommand(index); + } + } +} diff --git a/src/planmysem/logic/parser/EditCommandParser.java b/src/planmysem/logic/parser/EditCommandParser.java new file mode 100644 index 000000000..4c2993193 --- /dev/null +++ b/src/planmysem/logic/parser/EditCommandParser.java @@ -0,0 +1,76 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Set; + +import planmysem.common.Utils; +import planmysem.logic.commands.EditCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * + * @param args full command args string + * @return the prepared command + */ + public EditCommand parse(String args) throws ParseException { + HashMap> arguments = getParametersWithArguments(args); + String stringIndex = getStartingArgument(args); + int index = Utils.parseInteger(stringIndex); + Set tags = arguments.get(PREFIX_TAG); + + if ((index <= 0 && tags == null) || (index >= 0 && tags != null)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + String nst = getFirstInSet(arguments.get(PREFIX_NEW_START_TIME)); + LocalTime startTime = null; + if (nst != null) { + startTime = Utils.parseTime(nst); + if (startTime == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + } + + // determine if "end time" is a duration or time + String net = getFirstInSet(arguments.get(PREFIX_NEW_END_TIME)); + int duration = -1; + if (net != null) { + duration = Utils.parseInteger(net); + if (duration == -1) { + LocalTime endTime = Utils.parseTime(net); + if (endTime == null) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } else { + duration = Utils.getDuration(startTime, endTime); + } + } + } + + // The following are not mandatory and can be null + String name = getFirstInSet(arguments.get(PREFIX_NEW_NAME)); + String location = getFirstInSet(arguments.get(PREFIX_NEW_LOCATION)); + String description = getFirstInSet(arguments.get(PREFIX_NEW_DESCRIPTION)); + Set newTags = arguments.get(PREFIX_NEW_TAG); + + if (index == -1) { + return new EditCommand(name, startTime, duration, location, description, tags, newTags); + } else { + String nd = getFirstInSet(arguments.get(PREFIX_NEW_DATE)); + LocalDate date = Utils.parseDate(nd); + + return new EditCommand(index, name, date, startTime, duration, location, description, newTags); + } + } +} diff --git a/src/planmysem/logic/parser/FindCommandParser.java b/src/planmysem/logic/parser/FindCommandParser.java new file mode 100644 index 000000000..6b80ca2f6 --- /dev/null +++ b/src/planmysem/logic/parser/FindCommandParser.java @@ -0,0 +1,36 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static planmysem.common.Messages.MESSAGE_INVALID_MULTIPLE_PARAMS; + +import java.util.HashMap; +import java.util.Set; + +import planmysem.logic.commands.FindCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class FindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns an FindCommand object for execution. + * + * @param args full command args string + * @return the prepared command + */ + public FindCommand parse(String args) throws ParseException { + HashMap> arguments = getParametersWithArguments(args); + String name = getFirstInSet(arguments.get(PREFIX_NAME)); + String tag = getFirstInSet(arguments.get(PREFIX_TAG)); + if (name == null && tag == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } else if (name != null && tag != null) { + throw new ParseException(String.format(MESSAGE_INVALID_MULTIPLE_PARAMS, FindCommand.MESSAGE_USAGE)); + + } + return new FindCommand(name, tag); + } +} diff --git a/src/planmysem/logic/parser/ListCommandParser.java b/src/planmysem/logic/parser/ListCommandParser.java new file mode 100644 index 000000000..14089c0ab --- /dev/null +++ b/src/planmysem/logic/parser/ListCommandParser.java @@ -0,0 +1,35 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static planmysem.common.Messages.MESSAGE_INVALID_MULTIPLE_PARAMS; + +import java.util.HashMap; +import java.util.Set; + +import planmysem.logic.commands.ListCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ListCommand object + */ +public class ListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ListCommand + * and returns an ListCommand object for execution. + * + * @param args full command args string + * @return the prepared command + */ + public ListCommand parse(String args) throws ParseException { + HashMap> arguments = getParametersWithArguments(args); + String name = getFirstInSet(arguments.get(PREFIX_NAME)); + String tag = getFirstInSet(arguments.get(PREFIX_TAG)); + if (name == null && tag == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE)); + } else if (name != null && tag != null) { + throw new ParseException(String.format(MESSAGE_INVALID_MULTIPLE_PARAMS, ListCommand.MESSAGE_USAGE)); + } + return new ListCommand(name, tag); + } +} diff --git a/src/planmysem/logic/parser/Parser.java b/src/planmysem/logic/parser/Parser.java new file mode 100644 index 000000000..2e8aea108 --- /dev/null +++ b/src/planmysem/logic/parser/Parser.java @@ -0,0 +1,118 @@ +package planmysem.logic.parser; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import planmysem.logic.commands.Command; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public interface Parser { + + String PREFIX_NAME = "n"; + String PREFIX_DATE_OR_DAY = "d"; + String PREFIX_START_TIME = "st"; + String PREFIX_END_TIME = "et"; + String PREFIX_RECURRENCE = "r"; + String PREFIX_LOCATION = "l"; + String PREFIX_DESCRIPTION = "des"; + String PREFIX_TAG = "t"; + String PREFIX_NEW_NAME = "nn"; + String PREFIX_NEW_DATE = "nd"; + String PREFIX_NEW_START_TIME = "nst"; + String PREFIX_NEW_END_TIME = "net"; + String PREFIX_NEW_LOCATION = "nl"; + String PREFIX_NEW_DESCRIPTION = "ndes"; + String PREFIX_NEW_TAG = "nt"; + + /** + * Parses {@code userInput} into a command and returns it. + * @throws ParseException if {@code userInput} does not conform the expected format + */ + T parse(String userInput) throws ParseException; + + /** + * Parses arguments in the context of the add slots command. + * + * @return hashmap of parameter command with set of parameters. + */ + default HashMap> getParametersWithArguments(String args) { + String parameters = args.trim(); + String parameter; + String option; + int buf; + HashMap> result = new HashMap<>(); + while (true) { + buf = parameters.indexOf('/'); + if (buf == -1) { + break; + } + + option = parameters.substring(0, buf).trim(); + if (option.contains(" ")) { + parameters = parameters.substring(option.lastIndexOf(" ")); + continue; + } + + parameters = parameters.substring(buf + 1); + + if (parameters.indexOf('/') != -1) { + parameter = parameters.substring(0, parameters.indexOf('/')); + if (parameter.indexOf(' ') != -1) { + parameter = parameter.substring(0, parameter.lastIndexOf(" ")); + } + } else { + parameter = parameters; + } + + if (result.get(option) == null) { + result.put(option, new HashSet<>(Collections.singletonList(parameter.trim()))); + } else { + Set ss = result.get(option); + ss.add(parameter.trim()); + result.replace(option, ss); + } + + if (parameters.length() == 0) { + break; + } + parameters = parameters.substring(parameter.length()); + } + + return result; + } + + /** + * Get the first argument. + */ + default String getStartingArgument(String args) { + String result = args; + + // test if firstArgument is present + if (result.trim().length() == 0) { + return null; + } else if (result.indexOf('/') != -1) { + result = result.substring(0, result.indexOf('/')); + if (result.lastIndexOf(" ") == -1) { + return null; + } + return result.substring(0, result.lastIndexOf(" ")).trim(); + } else { + return result.trim(); + } + } + + /** + * Get the first string in a set. + */ + default String getFirstInSet(Set set) { + if (set == null || set.size() == 0) { + return null; + } + return set.stream().findFirst().get(); + } +} diff --git a/src/planmysem/logic/parser/ParserManager.java b/src/planmysem/logic/parser/ParserManager.java new file mode 100644 index 000000000..e7ed8f269 --- /dev/null +++ b/src/planmysem/logic/parser/ParserManager.java @@ -0,0 +1,90 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import planmysem.logic.commands.AddCommand; +import planmysem.logic.commands.ClearCommand; +import planmysem.logic.commands.Command; +import planmysem.logic.commands.DeleteCommand; +import planmysem.logic.commands.EditCommand; +import planmysem.logic.commands.ExitCommand; +import planmysem.logic.commands.ExportCommand; +import planmysem.logic.commands.FindCommand; +import planmysem.logic.commands.HelpCommand; +import planmysem.logic.commands.HistoryCommand; +import planmysem.logic.commands.ListCommand; +import planmysem.logic.commands.ViewCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class ParserManager { + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + switch (commandWord) { + case AddCommand.COMMAND_WORD: + case AddCommand.COMMAND_WORD_SHORT: + return new AddCommandParser().parse(arguments); + + case EditCommand.COMMAND_WORD: + case EditCommand.COMMAND_WORD_SHORT: + return new EditCommandParser().parse(arguments); + + case DeleteCommand.COMMAND_WORD: + case DeleteCommand.COMMAND_WORD_ALT: + case DeleteCommand.COMMAND_WORD_SHORT: + return new DeleteCommandParser().parse(arguments); + + case FindCommand.COMMAND_WORD: + case FindCommand.COMMAND_WORD_SHORT: + return new FindCommandParser().parse(arguments); + + case ListCommand.COMMAND_WORD: + case ListCommand.COMMAND_WORD_SHORT: + return new ListCommandParser().parse(arguments); + + case ViewCommand.COMMAND_WORD: + case ViewCommand.COMMAND_WORD_SHORT: + return new ViewCommandParser().parse(arguments); + + case HistoryCommand.COMMAND_WORD: + return new HistoryCommand(); + + 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: + return new HelpCommand(); + } + } +} diff --git a/src/planmysem/logic/parser/ViewCommandParser.java b/src/planmysem/logic/parser/ViewCommandParser.java new file mode 100644 index 000000000..5fc2a392d --- /dev/null +++ b/src/planmysem/logic/parser/ViewCommandParser.java @@ -0,0 +1,43 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import planmysem.logic.commands.ViewCommand; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ViewCommand object + */ +public class ViewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewCommand + * and returns an ViewCommand object for execution. + * + * @param args full command args string + * @return the prepared command + */ + public ViewCommand parse(String args) throws ParseException { + if (args == null || args.trim().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.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]); + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE)); + } +} diff --git a/src/planmysem/logic/parser/exceptions/ParseException.java b/src/planmysem/logic/parser/exceptions/ParseException.java new file mode 100644 index 000000000..057d24dad --- /dev/null +++ b/src/planmysem/logic/parser/exceptions/ParseException.java @@ -0,0 +1,17 @@ +package planmysem.logic.parser.exceptions; + +import planmysem.common.exceptions.IllegalValueException; + +/** + * Represents a parse error encountered by a parser. + */ +public class ParseException extends IllegalValueException { + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/planmysem/model/Model.java b/src/planmysem/model/Model.java new file mode 100644 index 000000000..4cd3c15ab --- /dev/null +++ b/src/planmysem/model/Model.java @@ -0,0 +1,108 @@ +package planmysem.model; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javafx.util.Pair; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.semester.Semester; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; + +/** + * The API of the Model component. + */ +public interface Model { + + /** + * Set last shown list. + */ + void setLastShownList(Map> list); + + /** + * Saves the current planner state for undo/redo. + */ + void commit(); + + /** + * Get last shown list. + */ + Map> getLastShownList(); + + /** + * Get item in last shown list. + */ + Pair> getLastShownItem(int index); + + /** + * Adds a slot to the Planner. + */ + Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException; + + /** + * Removes a Slot in the Planner. + */ + void removeSlot(LocalDate date, ReadOnlySlot slot); + + /** + * Removes a Slot in the Planner. + */ + void removeSlot(Pair> slot); + + /** + * Edit specific slot within the planner. + */ + void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, + LocalTime startTime, int duration, String name, String location, + String description, Set tags); + + /** + * Clears all slots from the Planner. + */ + void clearSlots(); + + /** + * gets all days in the Planner. + */ + HashMap getDays(); + /** + * Defensively copy the Semester in the Planner at the time of the call. + */ + Planner getPlanner(); + + /** + * gets specific day in the Planner. + */ + Day getDay(LocalDate date); + + /** + * gets all slots in the Planner containing all specified tags. + */ + Map> getSlots(Set tags); + + /** + * Returns true if the model has previous Planner states to restore. + */ + boolean canUndo(); + + /** + * Returns true if the model has undone Planner states to restore. + */ + boolean canRedo(); + + /** + * Restores the model's Planner to its previous state. + */ + void undo(); + + /** + * Restores the model's Planner to its previously undone state. + */ + void redo(); + +} + diff --git a/src/planmysem/model/ModelManager.java b/src/planmysem/model/ModelManager.java new file mode 100644 index 000000000..f31ee7a17 --- /dev/null +++ b/src/planmysem/model/ModelManager.java @@ -0,0 +1,162 @@ +package planmysem.model; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javafx.util.Pair; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.semester.Semester; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; + +/** + * Represents the entire Planner. Contains the model of the Planner. + */ +public class ModelManager implements Model { + protected Map> lastShownList = new TreeMap<>(); + private final VersionedPlanner versionedPlanner; + + /** + * Creates an empty planner. + */ + public ModelManager() { + versionedPlanner = new VersionedPlanner(new Planner()); + } + + /** + * Constructs a Planner with the given model. + * + * @param planner external changes to this will not affect this Planner + */ + public ModelManager(ReadOnlyPlanner planner) { + versionedPlanner = new VersionedPlanner(planner); + } + + @Override + public void setLastShownList(Map> list) { + lastShownList.clear(); + + if (list != null) { + lastShownList.putAll(list); + } + } + + @Override + public void commit() { + versionedPlanner.commit(); + } + + @Override + public Map> getLastShownList() { + return lastShownList; + } + + @Override + public Pair> getLastShownItem(int index) { + if (lastShownList == null || lastShownList.size() < index) { + throw new IndexOutOfBoundsException(); + } + + int adjustedIndex = index - 1; + + int count = 0; + for (Map.Entry> entry : lastShownList.entrySet()) { + if (count == adjustedIndex) { + return new Pair<>(entry.getKey(), entry.getValue()); + } + count++; + } + + throw new IndexOutOfBoundsException(); + } + + @Override + public Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { + return versionedPlanner.addSlot(date, slot); + } + + @Override + public void removeSlot(LocalDate date, ReadOnlySlot slot) { + versionedPlanner.removeSlot(date, slot); + } + + @Override + public void removeSlot(Pair> slot) { + versionedPlanner.removeSlot(slot.getKey(), slot.getValue().getValue()); + } + + @Override + public void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, + LocalTime startTime, int duration, String name, String location, + String description, Set tags) { + versionedPlanner.editSlot(targetDate, targetSlot, date, startTime, duration, name, location, description, tags); + } + + @Override + public void clearSlots() { + versionedPlanner.clearSlots(); + } + + @Override + public Planner getPlanner() { + return versionedPlanner; + } + + @Override + public HashMap getDays() { + return versionedPlanner.getDays(); + } + + @Override + public Day getDay(LocalDate date) { + return getDays().get(date); + } + + @Override + public Map> getSlots(Set tags) { + return versionedPlanner.getSlots(tags); + } + + @Override + public boolean canUndo() { + return versionedPlanner.canUndo(); + } + + @Override + public boolean canRedo() { + return versionedPlanner.canRedo(); + } + + @Override + public void undo() { + versionedPlanner.undo(); + } + + @Override + public void redo() { + versionedPlanner.redo(); + } + + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return versionedPlanner.equals(other.versionedPlanner) + && lastShownList.equals(other.lastShownList); + } +} diff --git a/src/planmysem/model/Planner.java b/src/planmysem/model/Planner.java new file mode 100644 index 000000000..5239ef140 --- /dev/null +++ b/src/planmysem/model/Planner.java @@ -0,0 +1,121 @@ +package planmysem.model; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javafx.util.Pair; +import planmysem.common.Clock; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.semester.Semester; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; + +/** + * Represents the entire Planner. Contains the model of the Planner. + */ +public class Planner implements ReadOnlyPlanner { + private final Semester semester; + + /** + * Creates an empty planner. + */ + public Planner() { + semester = Semester.generateSemester(LocalDate.now(Clock.get())); + } + + /** + * Creates a Planner using the days in the {@code toBeCopied} + */ + public Planner(ReadOnlyPlanner toBeCopied) { + this(); + resetData(toBeCopied); + } + + /** + * Constructs a Planner with the given model. + * + * @param semester external changes to this will not affect this Planner + */ + public Planner(Semester semester) { + this.semester = new Semester(semester); + } + + /** + * Resets the existing data of this {@code Planner} with {@code newData}. + */ + public void resetData(ReadOnlyPlanner newData) { + requireNonNull(newData); + + setDays(newData.getDays()); + } + + public Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { + return semester.addSlot(date, slot); + } + + public void removeSlot(LocalDate date, ReadOnlySlot slot) { + semester.removeSlot(date, slot); + } + + public void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, + LocalTime startTime, int duration, String name, String location, + String description, Set tags) { + semester.editSlot(targetDate, targetSlot, date, startTime, duration, name, location, description, tags); + } + + public void clearSlots() { + semester.clearSlots(); + } + + public Semester getSemester() { + return semester; + } + + /** + * Replaces the days of the planner with {@code days}. + */ + public void setDays(HashMap days) { + this.semester.setDays(days); + } + + public HashMap getDays() { + return semester.getDays(); + } + + public Day getDay(LocalDate date) { + return getDays().get(date); + } + + public Map> getSlots(Set tags) { + final Map> selectedSlots = new TreeMap<>(); + + for (Map.Entry entry : getDays().entrySet()) { + for (Slot slot : entry.getValue().getSlots()) { + if (slot.getTags().containsAll(tags)) { + selectedSlots.put(entry.getKey(), new Pair<>(entry.getValue(), slot)); + } + } + } + + return selectedSlots; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Planner // instanceof handles nulls + && this.semester.equals(((Planner) other).semester)); + } + + @Override + public int hashCode() { + return semester.hashCode(); + } +} diff --git a/src/planmysem/model/ReadOnlyPlanner.java b/src/planmysem/model/ReadOnlyPlanner.java new file mode 100644 index 000000000..66634279a --- /dev/null +++ b/src/planmysem/model/ReadOnlyPlanner.java @@ -0,0 +1,18 @@ +package planmysem.model; + +import java.time.LocalDate; +import java.util.HashMap; + +import planmysem.model.semester.Day; + +/** + * Unmodifiable view of a Planner + */ +public interface ReadOnlyPlanner { + + /** + * Returns an unmodifiable view of all days. + */ + HashMap getDays(); + +} diff --git a/src/planmysem/model/VersionedPlanner.java b/src/planmysem/model/VersionedPlanner.java new file mode 100644 index 000000000..ad6cd9be2 --- /dev/null +++ b/src/planmysem/model/VersionedPlanner.java @@ -0,0 +1,109 @@ +package planmysem.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@code AddressBook} that keeps track of its own history. + */ +public class VersionedPlanner extends Planner { + + private final List plannerListState; + private int currentStatePointer; + + public VersionedPlanner(ReadOnlyPlanner initialState) { + super(initialState); + + plannerListState = new ArrayList<>(); + plannerListState.add(initialState); + currentStatePointer = 0; + } + + /** + * Saves a copy of the current {@code AddressBook} state at the end of the state list. + * Undone states are removed from the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + plannerListState.add(new Planner(this)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + plannerListState.subList(currentStatePointer + 1, plannerListState.size()).clear(); + } + + /** + * Restores the address book to its previous state. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + resetData(plannerListState.get(currentStatePointer)); + } + + /** + * Restores the address book to its previously undone state. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + resetData(plannerListState.get(currentStatePointer)); + } + + /** + * Returns true if {@code undo()} has address book states to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if {@code redo()} has address book states to redo. + */ + public boolean canRedo() { + return currentStatePointer < plannerListState.size() - 1; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedPlanner)) { + return false; + } + + VersionedPlanner otherVersionedPlanner = (VersionedPlanner) other; + + // state check + return super.equals(otherVersionedPlanner) + && plannerListState.equals(otherVersionedPlanner.plannerListState) + && currentStatePointer == otherVersionedPlanner.currentStatePointer; + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of addressBookState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of addressBookState list, unable to redo."); + } + } +} diff --git a/src/planmysem/data/recurrence/Recurrence.java b/src/planmysem/model/recurrence/Recurrence.java similarity index 95% rename from src/planmysem/data/recurrence/Recurrence.java rename to src/planmysem/model/recurrence/Recurrence.java index d24abca20..8f73a1629 100644 --- a/src/planmysem/data/recurrence/Recurrence.java +++ b/src/planmysem/model/recurrence/Recurrence.java @@ -1,4 +1,4 @@ -package planmysem.data.recurrence; +package planmysem.model.recurrence; import static planmysem.common.Utils.getNearestDayOfWeek; @@ -8,7 +8,8 @@ import java.util.Set; import java.util.TreeSet; -import planmysem.data.semester.Semester; +import planmysem.common.Clock; +import planmysem.model.semester.Semester; /** * Represents a Recurrence Value of a slot in the Planner. @@ -27,7 +28,7 @@ public class Recurrence { */ public Recurrence(Set recurrences, int day) { this.day = DayOfWeek.of(day); - date = getNearestDayOfWeek(LocalDate.now(), day); + date = getNearestDayOfWeek(LocalDate.now(Clock.get()), day); if (recurrences == null) { normal = false; @@ -98,7 +99,7 @@ public Set generateDates(Semester semester) { result.addAll(getDates(semester.getExamDays())); } } else { - LocalDate dateStart = LocalDate.now(); + LocalDate dateStart = LocalDate.now(Clock.get()); // recurse over normal days if (normal) { diff --git a/src/planmysem/data/semester/Day.java b/src/planmysem/model/semester/Day.java similarity index 90% rename from src/planmysem/data/semester/Day.java rename to src/planmysem/model/semester/Day.java index 4639e3345..f589cd594 100644 --- a/src/planmysem/data/semester/Day.java +++ b/src/planmysem/model/semester/Day.java @@ -1,12 +1,12 @@ -package planmysem.data.semester; +package planmysem.model.semester; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.Objects; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; /** * Represents a Day in the planner. @@ -20,6 +20,14 @@ public class Day implements ReadOnlyDay { /** * Assumption: Every field must be present and not null. */ + public Day(ReadOnlyDay day) { + this.dayOfWeek = day.getDayOfWeek(); + this.type = day.getType(); + for (Slot slot : day.getSlots()) { + this.slots.add(slot); + } + } + public Day(DayOfWeek dayOfWeek, String weekType) { this.dayOfWeek = dayOfWeek; this.type = weekType; diff --git a/src/planmysem/data/semester/AdaptedSemester.java b/src/planmysem/model/semester/IcsSemester.java similarity index 86% rename from src/planmysem/data/semester/AdaptedSemester.java rename to src/planmysem/model/semester/IcsSemester.java index 91f3f62e1..00acb690b 100644 --- a/src/planmysem/data/semester/AdaptedSemester.java +++ b/src/planmysem/model/semester/IcsSemester.java @@ -1,15 +1,15 @@ -package planmysem.data.semester; +package planmysem.model.semester; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import planmysem.data.slot.Slot; +import planmysem.model.slot.Slot; /** - * JAXB-friendly adapted address book data holder class. + * Converts objects into .ics format. */ -public class AdaptedSemester { +public class IcsSemester { private String icsCalendar; @@ -18,7 +18,7 @@ public class AdaptedSemester { * * @param source Slot object to be converted into .ics format. */ - public AdaptedSemester(Semester source) { + public IcsSemester(Semester source) { DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss"); this.icsCalendar = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"; for (LocalDate date : source.getDays().keySet()) { @@ -29,7 +29,7 @@ public AdaptedSemester(Semester source) { LocalDateTime endDateTime = startDateTime.plusMinutes(slots.getDuration()); this.icsCalendar = this.icsCalendar.concat("DTEND:" + dateFormat.format(endDateTime) + "\r\n"); this.icsCalendar = this.icsCalendar.concat("SUMMARY:" + slots.getName() + "\r\n"); - if (slots.getLocation().toString() != null) { + if (slots.getLocation() != null) { this.icsCalendar = this.icsCalendar.concat("LOCATION:" + slots.getLocation() + "\r\n"); } this.icsCalendar = this.icsCalendar.concat("DESCRIPTION:" + slots.getDescription() + "\r\n"); diff --git a/src/planmysem/data/semester/ReadOnlyDay.java b/src/planmysem/model/semester/ReadOnlyDay.java similarity index 93% rename from src/planmysem/data/semester/ReadOnlyDay.java rename to src/planmysem/model/semester/ReadOnlyDay.java index 8f34daade..5089db170 100644 --- a/src/planmysem/data/semester/ReadOnlyDay.java +++ b/src/planmysem/model/semester/ReadOnlyDay.java @@ -1,9 +1,9 @@ -package planmysem.data.semester; +package planmysem.model.semester; import java.time.DayOfWeek; import java.util.ArrayList; -import planmysem.data.slot.Slot; +import planmysem.model.slot.Slot; /** * Represents a Day in the planner. diff --git a/src/planmysem/data/semester/ReadOnlySemester.java b/src/planmysem/model/semester/ReadOnlySemester.java similarity index 88% rename from src/planmysem/data/semester/ReadOnlySemester.java rename to src/planmysem/model/semester/ReadOnlySemester.java index 18b5ddef3..a7f679602 100644 --- a/src/planmysem/data/semester/ReadOnlySemester.java +++ b/src/planmysem/model/semester/ReadOnlySemester.java @@ -1,4 +1,4 @@ -package planmysem.data.semester; +package planmysem.model.semester; import java.time.LocalDate; import java.util.HashMap; @@ -12,6 +12,7 @@ public interface ReadOnlySemester { String getName(); String getAcademicYear(); HashMap getDays(); + void setDays(HashMap days); LocalDate getStartDate(); LocalDate getEndDate(); int getNoOfWeeks(); diff --git a/src/planmysem/data/semester/Semester.java b/src/planmysem/model/semester/Semester.java similarity index 98% rename from src/planmysem/data/semester/Semester.java rename to src/planmysem/model/semester/Semester.java index 583b6e547..a6cf8751a 100644 --- a/src/planmysem/data/semester/Semester.java +++ b/src/planmysem/model/semester/Semester.java @@ -1,4 +1,4 @@ -package planmysem.data.semester; +package planmysem.model.semester; import java.time.DayOfWeek; import java.time.LocalDate; @@ -15,8 +15,8 @@ import java.util.TreeMap; import java.util.stream.Collectors; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; /** * A list of days. Does not allow null elements or duplicates. @@ -461,6 +461,12 @@ public String getAcademicYear() { return academicYear; } + @Override + public void setDays(HashMap days) { + this.days.clear(); + this.days.putAll(days); + } + @Override public HashMap getDays() { return days; diff --git a/src/planmysem/data/slot/ReadOnlySlot.java b/src/planmysem/model/slot/ReadOnlySlot.java similarity index 91% rename from src/planmysem/data/slot/ReadOnlySlot.java rename to src/planmysem/model/slot/ReadOnlySlot.java index b70101a8b..26c59da9a 100644 --- a/src/planmysem/data/slot/ReadOnlySlot.java +++ b/src/planmysem/model/slot/ReadOnlySlot.java @@ -1,6 +1,7 @@ -package planmysem.data.slot; +package planmysem.model.slot; import java.time.LocalTime; +import java.util.Objects; import java.util.Set; import planmysem.common.Utils; @@ -30,8 +31,8 @@ default boolean isSameStateAs(ReadOnlySlot other) { return other == this // short circuit if same object || (other != null // this is first to avoid NPE below && other.getName().equals(this.getName()) // state checks here onwards - && other.getLocation().equals(this.getLocation()) - && other.getDescription().equals(this.getDescription()) + && Objects.equals(other.getLocation(), this.getLocation()) + && Objects.equals(other.getDescription(), this.getDescription()) && other.getStartTime().equals(this.getStartTime()) && other.getDuration() == this.getDuration() && other.getTags().equals(this.getTags())); diff --git a/src/planmysem/data/slot/Slot.java b/src/planmysem/model/slot/Slot.java similarity index 99% rename from src/planmysem/data/slot/Slot.java rename to src/planmysem/model/slot/Slot.java index 7456ed19d..5f7ffa4ab 100644 --- a/src/planmysem/data/slot/Slot.java +++ b/src/planmysem/model/slot/Slot.java @@ -1,4 +1,4 @@ -package planmysem.data.slot; +package planmysem.model.slot; import java.time.LocalTime; import java.util.HashSet; diff --git a/src/planmysem/parser/Parser.java b/src/planmysem/parser/Parser.java deleted file mode 100644 index ce58e0dd2..000000000 --- a/src/planmysem/parser/Parser.java +++ /dev/null @@ -1,456 +0,0 @@ -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_MULTIPLE_PARAMS; -import static planmysem.common.Messages.MESSAGE_INVALID_TIME; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -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.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; - -/** - * Parses user input. - */ -public class Parser { - - public static final Pattern KEYWORDS_ARGS_FORMAT = - Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace - - private static final String PARAMETER_NAME = "n"; - private static final String PARAMETER_DATE_OR_DAY = "d"; - private static final String PARAMETER_START_TIME = "st"; - private static final String PARAMETER_END_TIME = "et"; - private static final String PARAMETER_RECURRENCE = "r"; - private static final String PARAMETER_LOCATION = "l"; - private static final String PARAMETER_DESCRIPTION = "des"; - private static final String PARAMETER_TAG = "t"; - private static final String PARAMETER_NEW_NAME = "nn"; - private static final String PARAMETER_NEW_DATE = "nd"; - private static final String PARAMETER_NEW_START_TIME = "nst"; - private static final String PARAMETER_NEW_END_TIME = "net"; - private static final String PARAMETER_NEW_LOCATION = "nl"; - 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. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - */ - public Command parseCommand(String userInput) { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - - switch (commandWord) { - case AddCommand.COMMAND_WORD: - case AddCommand.COMMAND_WORD_SHORT: - return prepareAdd(arguments); - - case EditCommand.COMMAND_WORD: - case EditCommand.COMMAND_WORD_SHORT: - return prepareEdit(arguments); - - case DeleteCommand.COMMAND_WORD: - case DeleteCommand.COMMAND_WORD_ALT: - 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: - return new HelpCommand(); - } - } - - /** - * Parses arguments in the context of the add command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareAdd(String args) { - HashMap> arguments = getParametersWithArguments(args); - - // Name is mandatory - String name = getFirstInSet(arguments.get(PARAMETER_NAME)); - - if ((arguments == null || arguments.isEmpty()) - || (name == null || name.isEmpty()) - || arguments.get(PARAMETER_DATE_OR_DAY) == null - || arguments.get(PARAMETER_START_TIME) == null - || arguments.get(PARAMETER_END_TIME) == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - // Either date or day must be present - String dateOrDay = getFirstInSet(arguments.get(PARAMETER_DATE_OR_DAY)); - int day = -1; - LocalDate date = Utils.parseDate(dateOrDay); - if (date == null) { - day = Utils.parseDay(dateOrDay); - } - 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_ADDITIONAL, - AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); - } - - // determine if "end time" is a duration or time - String stringEndTime = getFirstInSet(arguments.get(PARAMETER_END_TIME)); - int duration = Utils.parseInteger(stringEndTime); - - if (duration == -1) { - LocalTime endTime = Utils.parseTime(stringEndTime); - if (endTime == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, - AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); - } - duration = Utils.getDuration(startTime, endTime); - } - - // Description is not mandatory and can be null - String description = getFirstInSet(arguments.get(PARAMETER_DESCRIPTION)); - - // Location is not mandatory and can be null - String location = getFirstInSet(arguments.get(PARAMETER_LOCATION)); - - // Tags is not mandatory - Set tags = arguments.get(PARAMETER_TAG); - if (tags != null) { - for (String tag : tags) { - if (tag.length() == 0) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, - AddCommand.MESSAGE_USAGE)); - } - } - } else { - tags = new HashSet<>(); - } - - // Recurrences is not mandatory - Set recurrences = arguments.get(PARAMETER_RECURRENCE); - - try { - if (day != -1) { - return new AddCommand( - day, - name, - location, - description, - startTime, - duration, - tags, - recurrences - ); - } else { - return new AddCommand( - date, - name, - location, - description, - startTime, - duration, - tags, - recurrences - ); - } - } catch (IllegalValueException ive) { - return new IncorrectCommand(ive.getMessage()); - } - } - - /** - * Parses arguments in the context of the edit command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareEdit(String args) { - HashMap> arguments = getParametersWithArguments(args); - String stringIndex = getStartingArgument(args); - int index = Utils.parseInteger(stringIndex); - Set tags = arguments.get(PARAMETER_TAG); - - if ((index == -1 && tags == null) || (index != -1 && tags != null)) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); - } - - String nst = getFirstInSet(arguments.get(PARAMETER_NEW_START_TIME)); - LocalTime startTime = null; - if (nst != null) { - startTime = Utils.parseTime(nst); - if (startTime == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); - } - } - - // determine if "end time" is a duration or time - String net = getFirstInSet(arguments.get(PARAMETER_NEW_END_TIME)); - int duration = -1; - if (net != null) { - duration = Utils.parseInteger(net); - if (duration == -1) { - LocalTime endTime = Utils.parseTime(net); - if (endTime == null) { - return new IncorrectCommand(String.format( - MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); - } else { - duration = Utils.getDuration(startTime, endTime); - } - } - } - - // The following are not mandatory and can be null - String name = getFirstInSet(arguments.get(PARAMETER_NEW_NAME)); - String location = getFirstInSet(arguments.get(PARAMETER_NEW_LOCATION)); - String description = getFirstInSet(arguments.get(PARAMETER_NEW_DESCRIPTION)); - Set newTags = arguments.get(PARAMETER_NEW_TAG); - - if (index == -1) { - return new EditCommand(name, startTime, duration, location, description, tags, newTags); - } else { - String nd = getFirstInSet(arguments.get(PARAMETER_NEW_DATE)); - LocalDate date = Utils.parseDate(nd); - - return new EditCommand(index, name, date, startTime, duration, location, description, newTags); - } - } - - /** - * Parses arguments in the context of the delete command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareDelete(String args) { - HashMap> arguments = getParametersWithArguments(args); - String stringIndex = getStartingArgument(args); - int index = Utils.parseInteger(stringIndex); - Set tags = arguments.get(PARAMETER_TAG); - - if ((index == -1 && tags == null) || (index != -1 && tags != null)) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); - } - - if (index == -1) { - return new DeleteCommand(tags); - } else { - return new DeleteCommand(index); - } - } - - /** - * 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) { - HashMap> arguments = getParametersWithArguments(args); - String name = getFirstInSet(arguments.get(PARAMETER_NAME)); - String tag = getFirstInSet(arguments.get(PARAMETER_TAG)); - if (name == null && tag == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } else if (name != null && tag != null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_MULTIPLE_PARAMS, FindCommand.MESSAGE_USAGE)); - - } - return new FindCommand(name, tag); - } - - /** - * Parses arguments in the context of the list command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareList(String args) { - HashMap> arguments = getParametersWithArguments(args); - String name = getFirstInSet(arguments.get(PARAMETER_NAME)); - String tag = getFirstInSet(arguments.get(PARAMETER_TAG)); - if (name == null && tag == null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE)); - } else if (name != null && tag != null) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_MULTIPLE_PARAMS, ListCommand.MESSAGE_USAGE)); - } - return new ListCommand(name, tag); - } - - /** - * Parses arguments in the context of the view command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareView(String args) { - if (args == null || args.trim().isEmpty()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.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]); - } - - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE)); - } - - /** - * Parses arguments in the context of the add slots command. - * - * @return hashmap of parameter command with set of parameters. - */ - private static HashMap> getParametersWithArguments(String args) { - String parameters = args.trim(); - String parameter; - String option; - int buf; - HashMap> result = new HashMap<>(); - while (true) { - buf = parameters.indexOf('/'); - if (buf == -1) { - break; - } - - option = parameters.substring(0, buf).trim(); - if (option.contains(" ")) { - parameters = parameters.substring(option.lastIndexOf(" ")); - continue; - } - - parameters = parameters.substring(buf + 1); - - if (parameters.indexOf('/') != -1) { - parameter = parameters.substring(0, parameters.indexOf('/')); - if (parameter.indexOf(' ') != -1) { - parameter = parameter.substring(0, parameter.lastIndexOf(" ")); - } - } else { - parameter = parameters; - } - - if (result.get(option) == null) { - result.put(option, new HashSet<>(Collections.singletonList(parameter.trim()))); - } else { - Set ss = result.get(option); - ss.add(parameter.trim()); - result.replace(option, ss); - } - - if (parameters.length() == 0) { - break; - } - parameters = parameters.substring(parameter.length()); - } - - return result; - } - - /** - * Get the first argument. - */ - private String getStartingArgument(String args) { - String result = args; - - // test if firstArgument is present - if (result.trim().length() == 0) { - return null; - } else if (result.indexOf('/') != -1) { - result = result.substring(0, result.indexOf('/')); - return result.substring(0, result.lastIndexOf(" ")).trim(); - } else { - return result.trim(); - } - } - - /** - * Get the first string in a set. - */ - private String getFirstInSet(Set set) { - if (set == null || set.size() == 0) { - return null; - } - return set.stream().findFirst().get(); - } - - /** - * Signals that the user input could not be parsed. - */ - public static class ParseException extends Exception { - ParseException(String message) { - super(message); - } - } -} diff --git a/src/planmysem/storage/Storage.java b/src/planmysem/storage/Storage.java new file mode 100644 index 000000000..63b811d96 --- /dev/null +++ b/src/planmysem/storage/Storage.java @@ -0,0 +1,48 @@ +package planmysem.storage; + +import planmysem.common.exceptions.IllegalValueException; +import planmysem.model.Model; + +/** + * API of the Logic component + */ +public interface Storage { + + /** + * Saves all model to this storage file. + * + * @throws StorageFile.StorageOperationException if there were errors converting and/or storing model to file. + */ + void save(Model model) throws StorageFile.StorageOperationException; + + /** + * Loads model from this storage file. + * + * @throws StorageFile.StorageOperationException if there were errors reading and/or converting model from file. + */ + Model load() throws StorageFile.StorageOperationException; + + /** + * Gets path of file. + **/ + String getPath(); + + /** + * Signals that the given file path does not fulfill the storage filepath constraints. + */ + class InvalidStorageFilePathException extends IllegalValueException { + public InvalidStorageFilePathException(String message) { + super(message); + } + } + + /** + * Signals that some error has occured while trying to convert and read/write model between the application + * and the storage file. + */ + class StorageOperationException extends Exception { + public StorageOperationException(String message) { + super(message); + } + } +} diff --git a/src/planmysem/storage/StorageFile.java b/src/planmysem/storage/StorageFile.java index ecca95e97..77768767b 100644 --- a/src/planmysem/storage/StorageFile.java +++ b/src/planmysem/storage/StorageFile.java @@ -17,14 +17,15 @@ import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -import planmysem.data.Planner; -import planmysem.data.exception.IllegalValueException; +import planmysem.common.exceptions.IllegalValueException; +import planmysem.model.Model; +import planmysem.model.ModelManager; import planmysem.storage.jaxb.AdaptedPlanner; /** - * Represents the file used to store Planner data. + * Represents the file used to store Planner model. */ -public class StorageFile { +public class StorageFile implements Storage { /** * Default file path used if the user doesn't provide the file name. */ @@ -35,7 +36,7 @@ public class StorageFile { */ public final Path path; private final JAXBContext jaxbContext; - private final boolean isEncrypted = false; //set to true to encrypt data + private final boolean isEncrypted = false; //set to true to encrypt model /** * @throws InvalidStorageFilePathException if the default path is invalid @@ -68,19 +69,15 @@ private static boolean isValidPath(Path filePath) { return filePath.toString().endsWith(".txt"); } - /** - * Saves all data to this storage file. - * - * @throws StorageOperationException if there were errors converting and/or storing data to file. - */ - public void save(Planner planner) throws StorageOperationException { + @Override + public void save(Model model) throws StorageOperationException { /* Note: Note the 'try with resource' statement below. * More info: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html */ try (final Writer fileWriter = new BufferedWriter(new FileWriter(path.toFile()))) { - final AdaptedPlanner toSave = new AdaptedPlanner(planner); + final AdaptedPlanner toSave = new AdaptedPlanner(model.getPlanner()); final Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); if (isEncrypted) { @@ -98,12 +95,8 @@ public void save(Planner planner) throws StorageOperationException { } } - /** - * Loads data from this storage file. - * - * @throws StorageOperationException if there were errors reading and/or converting data from file. - */ - public Planner load() throws StorageOperationException { + @Override + public Model load() throws StorageOperationException { try (final BufferedReader fileReader = new BufferedReader(new FileReader(path.toFile()))) { @@ -119,9 +112,9 @@ public Planner load() throws StorageOperationException { // manual check for missing elements if (loaded.isAnyRequiredFieldMissing()) { - throw new StorageOperationException("File data missing some elements"); + throw new StorageOperationException("File model missing some elements"); } - return loaded.toModelType(); + return new ModelManager(loaded.toModelType()); /* Note: Here, we are using an exception to create the file if it is missing or empty. However, we should * minimize using exceptions to facilitate normal paths of execution. If we consider the missing file as a @@ -130,7 +123,7 @@ public Planner load() throws StorageOperationException { // create empty file if not found or is empty } catch (FileNotFoundException | NullPointerException ex) { - final Planner empty = new Planner(); + final Model empty = new ModelManager(); save(empty); return empty; @@ -138,32 +131,14 @@ public Planner load() throws StorageOperationException { } catch (IOException ioe) { throw new StorageOperationException("Error writing to file: " + path); } catch (JAXBException jaxbe) { - throw new StorageOperationException("Error parsing file data format"); + throw new StorageOperationException("Error parsing file model format"); } catch (IllegalValueException ive) { throw new StorageOperationException("File contains illegal data values; data type constraints not met"); } } + @Override public String getPath() { return path.toString(); } - - /** - * Signals that the given file path does not fulfill the storage filepath constraints. - */ - public static class InvalidStorageFilePathException extends IllegalValueException { - public InvalidStorageFilePathException(String message) { - super(message); - } - } - - /** - * Signals that some error has occured while trying to convert and read/write data between the application - * and the storage file. - */ - public static class StorageOperationException extends Exception { - public StorageOperationException(String message) { - super(message); - } - } } diff --git a/src/planmysem/storage/jaxb/AdaptedDay.java b/src/planmysem/storage/jaxb/AdaptedDay.java index 0eb517f56..98f863c08 100644 --- a/src/planmysem/storage/jaxb/AdaptedDay.java +++ b/src/planmysem/storage/jaxb/AdaptedDay.java @@ -5,13 +5,13 @@ import javax.xml.bind.annotation.XmlElement; -import planmysem.data.exception.IllegalValueException; -import planmysem.data.semester.Day; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.Slot; +import planmysem.common.exceptions.IllegalValueException; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.Slot; /** - * JAXB-friendly adapted Day data holder class. + * JAXB-friendly adapted Day model holder class. */ public class AdaptedDay { @XmlElement(required = true) @@ -46,7 +46,7 @@ public AdaptedDay(ReadOnlyDay source) { * Returns true if any required field is missing. *

* JAXB does not enforce (required = true) without a given XML schema. - * Since we do most of our validation using the data class constructors, the only extra logic we need + * Since we do most of our validation using the model class constructors, the only extra logic we need * is to ensure that every xml element in the document is present. JAXB sets missing elements as null, * so we check for that. */ @@ -64,7 +64,7 @@ public boolean isAnyRequiredFieldMissing() { /** * Converts this jaxb-friendly adapted Day object into the Day object. * - * @throws IllegalValueException if there were any data constraints violated in the AdaptedSemester + * @throws IllegalValueException if there were any model constraints violated in the IcsSemester */ public Day toModelType() throws IllegalValueException { final ArrayList slots = new ArrayList<>(); diff --git a/src/planmysem/storage/jaxb/AdaptedPlanner.java b/src/planmysem/storage/jaxb/AdaptedPlanner.java index ffec202dc..af3ab4852 100644 --- a/src/planmysem/storage/jaxb/AdaptedPlanner.java +++ b/src/planmysem/storage/jaxb/AdaptedPlanner.java @@ -3,11 +3,11 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import planmysem.data.Planner; -import planmysem.data.exception.IllegalValueException; +import planmysem.common.exceptions.IllegalValueException; +import planmysem.model.Planner; /** - * JAXB-friendly adapted Planner data holder class. + * JAXB-friendly adapted Planner model holder class. */ @XmlRootElement(name = "Planner") public class AdaptedPlanner { @@ -33,7 +33,7 @@ public AdaptedPlanner(Planner source) { * Returns true if any required field is missing. *

* JAXB does not enforce (required = true) without a given XML schema. - * Since we do most of our validation using the data class constructors, the only extra logic we need + * Since we do most of our validation using the model class constructors, the only extra logic we need * is to ensure that every xml element in the document is present. JAXB sets missing elements as null, * so we check for that. */ @@ -45,7 +45,7 @@ public boolean isAnyRequiredFieldMissing() { /** * Converts this jaxb-friendly {@code AdaptedPlanner} object into the corresponding(@code Planner} object. * - * @throws IllegalValueException if there were any data constraints violated in the AdaptedSemester + * @throws IllegalValueException if there were any model constraints violated in the IcsSemester */ public Planner toModelType() throws IllegalValueException { return new Planner(semester.toModelType()); diff --git a/src/planmysem/storage/jaxb/AdaptedSemester.java b/src/planmysem/storage/jaxb/AdaptedSemester.java index 7e5eeb269..9601a99c8 100644 --- a/src/planmysem/storage/jaxb/AdaptedSemester.java +++ b/src/planmysem/storage/jaxb/AdaptedSemester.java @@ -8,13 +8,13 @@ import javax.xml.bind.annotation.XmlElement; -import planmysem.data.exception.IllegalValueException; -import planmysem.data.semester.Day; -import planmysem.data.semester.ReadOnlySemester; -import planmysem.data.semester.Semester; +import planmysem.common.exceptions.IllegalValueException; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlySemester; +import planmysem.model.semester.Semester; /** - * JAXB-friendly adapted person data holder class. + * JAXB-friendly adapted person model holder class. */ public class AdaptedSemester { @XmlElement(required = true) @@ -81,7 +81,7 @@ public AdaptedSemester(ReadOnlySemester source) { * Returns true if any required field is missing. *

* JAXB does not enforce (required = true) without a given XML schema. - * Since we do most of our validation using the data class constructors, the only extra logic we need + * Since we do most of our validation using the model class constructors, the only extra logic we need * is to ensure that every xml element in the document is present. JAXB sets missing elements as null, * so we check for that. */ @@ -101,7 +101,7 @@ public boolean isAnyRequiredFieldMissing() { /** * Converts this jaxb-friendly adapted person object into the Person object. * - * @throws IllegalValueException if there were any data constraints violated in the AdaptedSemester + * @throws IllegalValueException if there were any model constraints violated in the IcsSemester */ public Semester toModelType() throws IllegalValueException { final String name = this.name; diff --git a/src/planmysem/storage/jaxb/AdaptedSlot.java b/src/planmysem/storage/jaxb/AdaptedSlot.java index 50f95d9ec..0360b8362 100644 --- a/src/planmysem/storage/jaxb/AdaptedSlot.java +++ b/src/planmysem/storage/jaxb/AdaptedSlot.java @@ -12,12 +12,12 @@ import javax.xml.bind.annotation.XmlElement; import planmysem.common.Utils; -import planmysem.data.exception.IllegalValueException; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; +import planmysem.common.exceptions.IllegalValueException; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; /** - * JAXB-friendly adapted person data holder class. + * JAXB-friendly adapted person model holder class. */ public class AdaptedSlot { @XmlElement(required = true) @@ -72,7 +72,7 @@ public boolean isAnyRequiredFieldMissing() { /** * Converts this jaxb-friendly adapted person object into the Person object. * - * @throws IllegalValueException if there were any data constraints violated in the AdaptedSemester + * @throws IllegalValueException if there were any model constraints violated in the IcsSemester */ public Slot toModelType() throws IllegalValueException { if (hasIllegalValues(name) || hasIllegalValues(location) || hasIllegalValues(description) || duration < 0) { diff --git a/src/planmysem/ui/Formatter.java b/src/planmysem/ui/Formatter.java index 473a91b62..df79dd303 100644 --- a/src/planmysem/ui/Formatter.java +++ b/src/planmysem/ui/Formatter.java @@ -6,8 +6,8 @@ import javafx.util.Pair; import planmysem.common.Messages; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; /** * Used for formatting text for display. e.g. for adding text decorations. diff --git a/src/planmysem/ui/Gui.java b/src/planmysem/ui/Gui.java index 203a16986..709e6bbcc 100644 --- a/src/planmysem/ui/Gui.java +++ b/src/planmysem/ui/Gui.java @@ -6,12 +6,12 @@ import javafx.scene.Scene; import javafx.stage.Stage; import planmysem.Main; -import planmysem.logic.Logic; +import planmysem.logic.LogicManager; /** * The GUI of the App */ -public class Gui { +public class Gui implements Ui { /** * Offset required to convert between 1-indexing and 0-indexing. @@ -20,22 +20,20 @@ public class Gui { public static final int INITIAL_WINDOW_WIDTH = 800; public static final int INITIAL_WINDOW_HEIGHT = 600; - private final Logic logic; + private final LogicManager logicManager; private MainWindow mainWindow; private String version; - public Gui(Logic logic, String version) { - this.logic = logic; + public Gui(LogicManager logicManager, String version) { + this.logicManager = logicManager; this.version = version; } - /** - * TODO: Add Javadoc comment. - */ + @Override public void start(Stage stage, Stoppable mainApp) throws IOException { mainWindow = createMainWindowP(stage, mainApp); - mainWindow.displayWelcomeMessage(version, logic.getStorageFilePath()); + mainWindow.displayWelcomeMessage(version, logicManager.getStorageFilePath()); } /** @@ -49,7 +47,7 @@ private MainWindow createMainWindowP(Stage stage, Stoppable mainApp) throws IOEx stage.setScene(new Scene(loader.load(), INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)); stage.show(); mainWindow = loader.getController(); - mainWindow.setLogic(logic); + mainWindow.setLogicManager(logicManager); mainWindow.setMainApp(mainApp); return mainWindow; } diff --git a/src/planmysem/ui/MainWindow.java b/src/planmysem/ui/MainWindow.java index 4fca1e385..b283dd876 100644 --- a/src/planmysem/ui/MainWindow.java +++ b/src/planmysem/ui/MainWindow.java @@ -8,27 +8,27 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.util.Pair; -import planmysem.commands.CommandResult; -import planmysem.commands.ExitCommand; import planmysem.common.Messages; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.logic.Logic; +import planmysem.logic.LogicManager; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.ExitCommand; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; /** * Main Window of the GUI. */ public class MainWindow { - private Logic logic; + private LogicManager logicManager; private Stoppable mainApp; @FXML private TextArea outputConsole; @FXML private TextField commandInput; - public void setLogic(Logic logic) { - this.logic = logic; + public void setLogicManager(LogicManager logicManager) { + this.logicManager = logicManager; } public void setMainApp(Stoppable mainApp) { @@ -42,7 +42,7 @@ public void setMainApp(Stoppable mainApp) { private void onCommand() { try { String userCommandText = commandInput.getText(); - CommandResult result = logic.execute(userCommandText); + CommandResult result = logicManager.execute(userCommandText); if (isExitCommand(result)) { exitApp(); return; @@ -105,7 +105,6 @@ public void displayWelcomeMessage(String version, String storageFilePath) { * Private contact details are hidden. */ private void display(Map> slots) { - // TODO: rename function call when AddressBook is fully removed from project display(new Formatter().formatSlots(slots)); } @@ -113,6 +112,7 @@ private void display(Map> slots) { * Displays the given messages on the output display area, after formatting appropriately. */ private void display(String... messages) { + clearOutputConsole(); outputConsole.setText(outputConsole.getText() + new Formatter().format(messages)); } } diff --git a/src/planmysem/ui/Ui.java b/src/planmysem/ui/Ui.java new file mode 100644 index 000000000..b3c10df23 --- /dev/null +++ b/src/planmysem/ui/Ui.java @@ -0,0 +1,15 @@ +package planmysem.ui; + +import java.io.IOException; + +import javafx.stage.Stage; + +/** + * API of UI component + */ +public interface Ui { + + /** Starts the UI (and the App). */ + void start(Stage stage, Stoppable mainApp) throws IOException; + +} diff --git a/test/java/planmysem/common/UtilsTest.java b/test/java/planmysem/common/UtilsTest.java index e1c2c45c4..7e511e496 100644 --- a/test/java/planmysem/common/UtilsTest.java +++ b/test/java/planmysem/common/UtilsTest.java @@ -12,9 +12,15 @@ import java.util.Arrays; import java.util.List; +import org.junit.Before; import org.junit.Test; public class UtilsTest { + @Before + public void setup() { + Clock.set("2019-01-14T10:00:00Z"); + } + @Test public void isAnyNull() { // empty list @@ -121,6 +127,22 @@ public void parse_day_unsuccessful() { assertEquals(Utils.parseDay("0"), -1); } + @Test + public void parse_date_successful() { + assertEquals(Utils.parseDate("01-03-2019"), LocalDate.of(2019, 03, 01)); + assertEquals(Utils.parseDate("01-04-2019"), LocalDate.of(2019, 04, 01)); + assertEquals(Utils.parseDate("01-05-2019"), LocalDate.of(2019, 05, 01)); + assertEquals(Utils.parseDate("01-06-2019"), LocalDate.of(2019, 06, 01)); + assertEquals(Utils.parseDate("01-06"), LocalDate.of(2019, 06, 01)); + } + + @Test + public void parse_date_unsuccessful() { + assertEquals(Utils.parseDate("00-06-2019"), null); + assertEquals(Utils.parseDate("01-13-2019"), null); + assertEquals(Utils.parseDate("32-12-2019"), null); + } + @Test public void parse_time_successful() { assertEquals(Utils.parseTime("08:00"), LocalTime.of(8, 0)); @@ -129,13 +151,12 @@ public void parse_time_successful() { 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)); + 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); assertEquals(Utils.parseTime("24:00"), null); @@ -161,7 +182,7 @@ public void parse_integer_unsuccessful() { @Test public void parse_get_duration_successful() { - LocalTime startTime = LocalTime.now(); + LocalTime startTime = LocalTime.now(Clock.get()); LocalTime endTime = startTime.plusMinutes(60); assertEquals(getDuration(startTime, endTime), 60); @@ -169,7 +190,7 @@ public void parse_get_duration_successful() { @Test public void parse_get_end_time_successful() { - LocalTime startTime = LocalTime.now(); + LocalTime startTime = LocalTime.now(Clock.get()); LocalTime endTime = startTime.plusMinutes(60); assertEquals(getEndTime(startTime, 60), endTime); diff --git a/test/java/planmysem/logic/Commands/AddCommandTest.java b/test/java/planmysem/logic/Commands/AddCommandTest.java new file mode 100644 index 000000000..eed8e00fc --- /dev/null +++ b/test/java/planmysem/logic/Commands/AddCommandTest.java @@ -0,0 +1,269 @@ +package planmysem.logic.Commands; + +import static java.util.Objects.requireNonNull; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static planmysem.logic.commands.AddCommand.MESSAGE_SUCCESS; +import static planmysem.logic.commands.AddCommand.craftSuccessMessage; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javafx.util.Pair; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import planmysem.common.Clock; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.AddCommand; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; +import planmysem.model.Planner; +import planmysem.model.recurrence.Recurrence; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.semester.Semester; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.slot.Slot; +import planmysem.testutil.SlotBuilder; + +public class AddCommandTest { + + private static final CommandHistory EMPTY_COMMAND_HISTORY = new CommandHistory(); + + private CommandHistory commandHistory = new CommandHistory(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() { + Clock.set("2019-01-14T10:00:00Z"); + } + + @Test + public void constructor_nullSlotRecursion_throwsNullPointerException() { + thrown.expect(NullPointerException.class); + new AddCommand(null, null); + } + + @Test + public void execute_slotAcceptedByModel_addSuccessful() throws CommandException { + ModelStubAcceptingSlotAdded modelStub = new ModelStubAcceptingSlotAdded(); + Slot validSlot = new SlotBuilder().slotOne(); + Recurrence validRecurrence = new SlotBuilder().recurrenceOne(); + + CommandResult commandResult = new AddCommand(validSlot, validRecurrence).execute(modelStub, commandHistory); + + Set dates = new HashSet<>(); + dates.add(LocalDate.of(2019, 2, 1)); + + Map days = new TreeMap<>(); + Day day = new Day(DayOfWeek.MONDAY, "type"); + day.addSlot(validSlot); + days.put(LocalDate.of(2019, 2, 1), day); + + assertEquals(String.format(MESSAGE_SUCCESS, dates.size(), + craftSuccessMessage(days, validSlot)), commandResult.getFeedbackToUser()); + assertEquals(days, modelStub.days); + assertEquals(EMPTY_COMMAND_HISTORY, commandHistory); + } + + @Test + public void execute_InvalidDate_throwsCommandException() throws Exception { + ModelStubNeverSlotAdded modelStub = new ModelStubNeverSlotAdded(); + Slot validSlot = new SlotBuilder().slotOne(); + Recurrence validRecurrence = new SlotBuilder().recurrenceOne(); + + AddCommand addCommand = new AddCommand(validSlot, validRecurrence); + + thrown.expect(CommandException.class); + thrown.expectMessage(AddCommand.MESSAGE_FAIL_OUT_OF_BOUNDS); + addCommand.execute(modelStub, commandHistory); + } + + @Test + public void equals() throws Exception { + Slot slot1 = new SlotBuilder().generateSlot(1); + Recurrence recurrence = new SlotBuilder().recurrenceOne(); + + AddCommand addCommand1 = new AddCommand(slot1, recurrence); + + // same object -> returns true + assertTrue(addCommand1.equals(addCommand1)); + + // same values -> returns true + AddCommand addCommand1Copy = new AddCommand(slot1, recurrence); + assertTrue(addCommand1.equals(addCommand1Copy)); + + // different types -> returns false + assertFalse(addCommand1.equals(1)); + + // null -> returns false + assertFalse(addCommand1.equals(null)); + + // different command -> returns false + Slot slot2 = new SlotBuilder().generateSlot(2); + AddCommand addCommand2 = new AddCommand(slot2, recurrence); + assertFalse(addCommand1.equals(addCommand2)); + } + + + /** + * A default model stub that have all of the methods failing. + */ + private class ModelStub implements Model { + @Override + public Map> getLastShownList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void commit() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setLastShownList(Map> list) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Pair> getLastShownItem(int index) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { + throw new AssertionError("This method should not be called."); + } + + @Override + public void removeSlot(LocalDate date, ReadOnlySlot slot) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void removeSlot(Pair> slot) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void editSlot(LocalDate targetDate, ReadOnlySlot targetSlot, LocalDate date, + LocalTime startTime, int duration, String name, String location, + String description, Set tags) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void clearSlots() { + throw new AssertionError("This method should not be called."); + } + + @Override + public Planner getPlanner() { + throw new AssertionError("This method should not be called."); + } + + @Override + public HashMap getDays() { + throw new AssertionError("This method should not be called."); + } + + @Override + public Day getDay(LocalDate date) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Map> getSlots(Set tags) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean canUndo() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean canRedo() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void undo() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void redo() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean equals(Object obj) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single slot. + */ + private class ModelStubWithSlot extends ModelStub { + private final Slot slot; + + ModelStubWithSlot(Slot slot) { + requireNonNull(slot); + this.slot = slot; + } + } + + /** + * A Model stub that always accept the slot being added. + */ + private class ModelStubAcceptingSlotAdded extends ModelStub { + Map days = new TreeMap<>(); + + @Override + public Day addSlot(LocalDate date, Slot slot) { + Day day = new Day(DayOfWeek.MONDAY, "type"); + day.addSlot(slot); + + days.put(date, day); + + return day; + } + + @Override + public Planner getPlanner() { + return new Planner(); + } + } + + /** + * A Model stub that never accepts the slot being added. + */ + private class ModelStubNeverSlotAdded extends ModelStub { + + @Override + public Day addSlot(LocalDate date, Slot slot) throws Semester.DateNotFoundException { + throw new Semester.DateNotFoundException(); + } + + @Override + public Planner getPlanner() { + return new Planner(); + } + } + + +} diff --git a/test/java/planmysem/logic/Commands/ClearCommandTest.java b/test/java/planmysem/logic/Commands/ClearCommandTest.java new file mode 100644 index 000000000..f64b79160 --- /dev/null +++ b/test/java/planmysem/logic/Commands/ClearCommandTest.java @@ -0,0 +1,54 @@ +package planmysem.logic.Commands; + +import static planmysem.logic.Commands.CommandTestUtil.assertCommandSuccess; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashSet; + +import org.junit.Before; +import org.junit.Test; +import planmysem.common.Clock; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.ClearCommand; +import planmysem.model.Model; +import planmysem.model.ModelManager; +import planmysem.model.Planner; +import planmysem.model.slot.Slot; + +public class ClearCommandTest { + + private CommandHistory commandHistory = new CommandHistory(); + + @Before + public void setup() { + Clock.set("2019-01-14T10:00:00Z"); + } + + @Test + public void execute_emptyPlanner_success() { + Model model = new ModelManager(); + Model expectedModel = new ModelManager(); + + assertCommandSuccess(new ClearCommand(), model, commandHistory, ClearCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_nonEmptyAddressBook_success() throws Exception { + Model model = new ModelManager(new Planner()); + Model expectedModel = new ModelManager(new Planner()); + + model.addSlot( + LocalDate.of(2019, 1, 21), + new Slot( + "CS2113T Tutorial", + "COM2 04-01", + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + )); + + assertCommandSuccess(new ClearCommand(), model, commandHistory, ClearCommand.MESSAGE_SUCCESS, expectedModel); + } +} diff --git a/test/java/planmysem/logic/Commands/CommandTestUtil.java b/test/java/planmysem/logic/Commands/CommandTestUtil.java new file mode 100644 index 000000000..e8ff6a045 --- /dev/null +++ b/test/java/planmysem/logic/Commands/CommandTestUtil.java @@ -0,0 +1,146 @@ +package planmysem.logic.Commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.util.Map; +import java.util.TreeMap; + +import javafx.util.Pair; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.Command; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.model.Model; +import planmysem.model.Planner; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; + +/** + * Contains helper methods for testing commands. + */ +public class CommandTestUtil { + + // public static final String VALID_NAME_AMY = "Amy Bee"; + // public static final String VALID_NAME_BOB = "Bob Choo"; + // public static final String VALID_PHONE_AMY = "11111111"; + // public static final String VALID_PHONE_BOB = "22222222"; + // public static final String VALID_EMAIL_AMY = "amy@example.com"; + // public static final String VALID_EMAIL_BOB = "bob@example.com"; + // public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; + // public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + // public static final String VALID_TAG_HUSBAND = "husband"; + // public static final String VALID_TAG_FRIEND = "friend"; + + // public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; + // public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; + // public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; + // public static final String PHONE_DESC_BOB = " " + PREFIX_PHONE + VALID_PHONE_BOB; + // public static final String EMAIL_DESC_AMY = " " + PREFIX_EMAIL + VALID_EMAIL_AMY; + // public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; + // public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; + // public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; + // public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; + // public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; + // + // public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names + // public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones + // public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol + // public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses + // public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags + + // public static final String PREAMBLE_WHITESPACE = "\t \r \n"; + // public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; + + // public static final EditCommand.EditPersonDescriptor DESC_AMY; + // public static final EditCommand.EditPersonDescriptor DESC_BOB; + + // static { + // DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) + // .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + // .withTags(VALID_TAG_FRIEND).build(); + // DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) + // .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) + // .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + // } + + /** + * Executes the given {@code command}, confirms that
+ * - the returned {@link CommandResult} matches {@code expectedCommandResult}
+ * - the {@code actualModel} matches {@code expectedModel}
+ * - the {@code actualCommandHistory} remains unchanged. + */ + public static void assertCommandSuccess(Command command, Model actualModel, CommandHistory actualCommandHistory, + CommandResult expectedCommandResult, Model expectedModel) { + CommandHistory expectedCommandHistory = new CommandHistory(actualCommandHistory); + try { + CommandResult result = command.execute(actualModel, actualCommandHistory); + assertEquals(expectedCommandResult, result); + assertEquals(actualModel, expectedModel); + assertEquals(actualCommandHistory, expectedCommandHistory); + } catch (CommandException ce) { + throw new AssertionError("Execution of command should not fail.", ce); + } + } + + /** + * Convenience wrapper to {@link #assertCommandSuccess(Command, Model, CommandHistory, CommandResult, Model)} + * that takes a string {@code expectedMessage}. + */ + public static void assertCommandSuccess(Command command, Model actualModel, CommandHistory actualCommandHistory, + String expectedMessage, Model expectedModel) { + CommandResult expectedCommandResult = new CommandResult(expectedMessage); + assertCommandSuccess(command, actualModel, actualCommandHistory, expectedCommandResult, expectedModel); + } + + /** + * Executes the given {@code command}, confirms that
+ * - a {@code CommandException} is thrown
+ * - the CommandException message matches {@code expectedMessage}
+ * - the planner, last shown list in {@code actualModel} remain unchanged
+ * - {@code actualCommandHistory} remains unchanged. + */ + public static void assertCommandFailure(Command command, Model actualModel, CommandHistory actualCommandHistory, + String expectedMessage) { + Planner expectedPlanner = new Planner(actualModel.getPlanner()); + actualModel.getDays(); + Map> expectedLastShownList = new TreeMap<>(actualModel.getLastShownList()); + + CommandHistory expectedCommandHistory = new CommandHistory(actualCommandHistory); + + try { + command.execute(actualModel, actualCommandHistory); + throw new AssertionError("The expected CommandException was not thrown."); + } catch (CommandException e) { + assertEquals(expectedMessage, e.getMessage()); + assertEquals(expectedPlanner, actualModel.getPlanner()); + assertEquals(expectedLastShownList, actualModel.getLastShownList()); +// assertEquals(expectedSelectedPerson, actualModel.getSelectedPerson()); + assertEquals(expectedCommandHistory, actualCommandHistory); + } + } + + // /** + // * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the + // * {@code model}'s address book. + // */ + // public static void showPersonAtIndex(Model model, Index targetIndex) { + // assertTrue(targetIndex.getZeroBased() < model.getFilteredPersonList().size()); + // + // Person person = model.getFilteredPersonList().get(targetIndex.getZeroBased()); + // final String[] splitName = person.getName().fullName.split("\\s+"); + // model.updateFilteredPersonList(new NameContainsKeywordsPredicate(Arrays.asList(splitName[0]))); + // + // assertEquals(1, model.getFilteredPersonList().size()); + // } + // + // /** + // * Deletes the first person in {@code model}'s filtered list from {@code model}'s address book. + // */ + // public static void deleteFirstPerson(Model model) { + // Person firstPerson = model.getFilteredPersonList().get(0); + // model.deletePerson(firstPerson); + // model.commitAddressBook(); + // } + +} diff --git a/test/java/planmysem/logic/Commands/DeleteCommandTest.java b/test/java/planmysem/logic/Commands/DeleteCommandTest.java new file mode 100644 index 000000000..1595866a7 --- /dev/null +++ b/test/java/planmysem/logic/Commands/DeleteCommandTest.java @@ -0,0 +1,199 @@ +package planmysem.logic.Commands; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static planmysem.logic.Commands.CommandTestUtil.assertCommandFailure; +import static planmysem.logic.Commands.CommandTestUtil.assertCommandSuccess; +import static planmysem.logic.commands.DeleteCommand.MESSAGE_SUCCESS; +import static planmysem.logic.commands.DeleteCommand.MESSAGE_SUCCESS_NO_CHANGE; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javafx.util.Pair; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import planmysem.common.Clock; +import planmysem.common.Messages; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.DeleteCommand; +import planmysem.model.Model; +import planmysem.model.ModelManager; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.testutil.SlotBuilder; + +public class DeleteCommandTest { + private Model model; + private Model expectedModel; + private Pair> pair1; + private Pair> pair2; + private Pair> pair3; + private Pair> pair4; + private CommandHistory commandHistory = new CommandHistory(); + + private SlotBuilder slotBuilder = new SlotBuilder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() throws Exception { + Clock.set("2019-01-14T10:00:00Z"); + + // Create typical planner + model = new ModelManager(); + pair1 = new Pair<>( + LocalDate.of(2019, 02, 01), + new Pair<>( + new Day( + DayOfWeek.FRIDAY, + "Week 3" + ), + slotBuilder.generateSlot(1) + ) + ); + pair2 = new Pair<>( + LocalDate.of(2019, 02, 02), + new Pair<>( + new Day( + DayOfWeek.SATURDAY, + "Week 3" + ), + slotBuilder.generateSlot(2) + ) + ); + pair3 = new Pair<>( + LocalDate.of(2019, 02, 03), + new Pair<>( + new Day( + DayOfWeek.SUNDAY, + "Week 3" + ), + slotBuilder.generateSlot(3) + ) + ); + pair4 = new Pair<>( + LocalDate.of(2019, 02, 04), + new Pair<>( + new Day( + DayOfWeek.MONDAY, + "Week 4" + ), + slotBuilder.generateSlot(3) + ) + ); + model.addSlot(LocalDate.of(2019, 02, 01), slotBuilder.generateSlot(1)); + model.addSlot(LocalDate.of(2019, 02, 02), slotBuilder.generateSlot(2)); + model.addSlot(LocalDate.of(2019, 02, 03), slotBuilder.generateSlot(3)); + model.addSlot(LocalDate.of(2019, 02, 04), slotBuilder.generateSlot(3)); + + Map> list = new HashMap<>(); + list.put(pair1.getKey(), pair1.getValue()); + list.put(pair2.getKey(), pair2.getValue()); + list.put(pair3.getKey(), pair3.getValue()); + list.put(pair4.getKey(), pair4.getValue()); + model.setLastShownList(list); + + expectedModel = new ModelManager(); + expectedModel.addSlot(LocalDate.of(2019, 02, 01), slotBuilder.generateSlot(1)); + expectedModel.addSlot(LocalDate.of(2019, 02, 02), slotBuilder.generateSlot(2)); + expectedModel.addSlot(LocalDate.of(2019, 02, 03), slotBuilder.generateSlot(3)); + expectedModel.addSlot(LocalDate.of(2019, 02, 04), slotBuilder.generateSlot(3)); + expectedModel.setLastShownList(model.getLastShownList()); + + } + + @Test + public void constructor_nullSlotRecursion_throwsNullPointerException() { + thrown.expect(NullPointerException.class); + new DeleteCommand(null); + } + + @Test + public void execute_validTag_success() { + Map> selectedSlots = new TreeMap<>(); + selectedSlots.put(pair3.getKey(), pair3.getValue()); + selectedSlots.put(pair4.getKey(), pair4.getValue()); + Set tags = pair4.getValue().getValue().getTags(); + DeleteCommand deleteCommand = new DeleteCommand(tags); + + String expectedMessage = String.format(MESSAGE_SUCCESS, + 2, Messages.craftSelectedMessage(tags), + Messages.craftSelectedMessage("Deleted Slots:", selectedSlots)); + + expectedModel.removeSlot(pair3); + expectedModel.removeSlot(pair4); + + assertCommandSuccess(deleteCommand, model, commandHistory, expectedMessage, expectedModel); + } + + @Test + public void execute_validIndex_success() { + Map> selectedSlots = new TreeMap<>(); + Pair> slot = model.getLastShownItem(1); + selectedSlots.put(slot.getKey(), slot.getValue()); + DeleteCommand deleteCommand = new DeleteCommand(1); + + String expectedMessage = String.format(MESSAGE_SUCCESS, + 1, Messages.craftSelectedMessage(1), + Messages.craftSelectedMessage("Deleted Slot:", selectedSlots)); + + expectedModel.removeSlot(slot); + + assertCommandSuccess(deleteCommand, model, commandHistory, expectedMessage, expectedModel); + } + + @Test + public void execute_InvalidTag_throwsCommandException() { + Set tags = pair4.getValue().getValue().getTags(); + DeleteCommand deleteCommand = new DeleteCommand(tags); + + String expectedMessage = String.format(MESSAGE_SUCCESS_NO_CHANGE, + Messages.craftSelectedMessage(tags)); + + // removed slots with the valid tags, so the exception will occur + model.removeSlot(pair3); + model.removeSlot(pair4); + + assertCommandFailure(deleteCommand, model, commandHistory, expectedMessage); + } + + @Test + public void execute_InvalidIndex_throwsCommandException() { + DeleteCommand deleteCommand = new DeleteCommand(5); + + String expectedMessage = Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX; + + assertCommandFailure(deleteCommand, model, commandHistory, expectedMessage); + } + + @Test + public void equals() { + DeleteCommand deleteFirstCommand = new DeleteCommand(1); + + // same object -> returns true + assertTrue(deleteFirstCommand.equals(deleteFirstCommand)); + + // same values -> returns true + DeleteCommand deleteFirstCommandCopy = new DeleteCommand(1); + assertTrue(deleteFirstCommand.equals(deleteFirstCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstCommand.equals(null)); + + // different command -> returns false + DeleteCommand deleteSecondCommand = new DeleteCommand(2); + assertFalse(deleteFirstCommand.equals(deleteSecondCommand)); + } +} diff --git a/test/java/planmysem/logic/Commands/EditCommandTest.java b/test/java/planmysem/logic/Commands/EditCommandTest.java new file mode 100644 index 000000000..e71be35f6 --- /dev/null +++ b/test/java/planmysem/logic/Commands/EditCommandTest.java @@ -0,0 +1,325 @@ +package planmysem.logic.Commands; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static planmysem.logic.Commands.CommandTestUtil.assertCommandFailure; +import static planmysem.logic.Commands.CommandTestUtil.assertCommandSuccess; +import static planmysem.logic.commands.EditCommand.MESSAGE_SUCCESS; +import static planmysem.logic.commands.EditCommand.MESSAGE_SUCCESS_NO_CHANGE; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javafx.util.Pair; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import planmysem.common.Clock; +import planmysem.common.Messages; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.EditCommand; +import planmysem.model.Model; +import planmysem.model.ModelManager; +import planmysem.model.semester.Day; +import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.slot.ReadOnlySlot; +import planmysem.testutil.SlotBuilder; + +public class EditCommandTest { + private Model model; + private Model expectedModel; + private Pair> pair1; + private Pair> pair2; + private Pair> pair3; + private Pair> pair4; + private CommandHistory commandHistory = new CommandHistory(); + + private SlotBuilder slotBuilder = new SlotBuilder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() throws Exception { + Clock.set("2019-01-14T10:00:00Z"); + + // Create typical planner + model = new ModelManager(); + pair1 = new Pair<>( + LocalDate.of(2019, 02, 01), + new Pair<>( + new Day( + DayOfWeek.FRIDAY, + "Week 3" + ), + slotBuilder.generateSlot(1) + ) + ); + pair2 = new Pair<>( + LocalDate.of(2019, 02, 02), + new Pair<>( + new Day( + DayOfWeek.SATURDAY, + "Week 3" + ), + slotBuilder.generateSlot(2) + ) + ); + pair3 = new Pair<>( + LocalDate.of(2019, 02, 03), + new Pair<>( + new Day( + DayOfWeek.SUNDAY, + "Week 3" + ), + slotBuilder.generateSlot(3) + ) + ); + pair4 = new Pair<>( + LocalDate.of(2019, 02, 04), + new Pair<>( + new Day( + DayOfWeek.MONDAY, + "Week 4" + ), + slotBuilder.generateSlot(3) + ) + ); + model.addSlot(LocalDate.of(2019, 02, 01), slotBuilder.generateSlot(1)); + model.addSlot(LocalDate.of(2019, 02, 02), slotBuilder.generateSlot(2)); + model.addSlot(LocalDate.of(2019, 02, 03), slotBuilder.generateSlot(3)); + model.addSlot(LocalDate.of(2019, 02, 04), slotBuilder.generateSlot(3)); + + Map> list = new HashMap<>(); + list.put(pair1.getKey(), pair1.getValue()); + list.put(pair2.getKey(), pair2.getValue()); + list.put(pair3.getKey(), pair3.getValue()); + list.put(pair4.getKey(), pair4.getValue()); + model.setLastShownList(list); + + expectedModel = new ModelManager(); + expectedModel.addSlot(LocalDate.of(2019, 02, 01), slotBuilder.generateSlot(1)); + expectedModel.addSlot(LocalDate.of(2019, 02, 02), slotBuilder.generateSlot(2)); + expectedModel.addSlot(LocalDate.of(2019, 02, 03), slotBuilder.generateSlot(3)); + expectedModel.addSlot(LocalDate.of(2019, 02, 04), slotBuilder.generateSlot(3)); + expectedModel.setLastShownList(model.getLastShownList()); + + } + + @Test + public void execute_validTag_success() { + Map> selectedSlots = new TreeMap<>(); + selectedSlots.put(pair3.getKey(), pair3.getValue()); + selectedSlots.put(pair4.getKey(), pair4.getValue()); + Set selectTags = pair3.getValue().getValue().getTags(); + + // values to edit + String name = "new name"; + String location = "new location"; + String description = "new description"; + LocalTime startTime = LocalTime.of(8, 0); + int duration = 60; + Set tags = new HashSet<>(Arrays.asList("tag1")); + + EditCommand editCommand = new EditCommand( + name, + startTime, + duration, + location, + description, + selectTags, + tags + ); + + String messageSelected = Messages.craftSelectedMessage(selectTags); + String messageSlots = editCommand.craftSuccessMessage(selectedSlots); + + String expectedMessage = String.format(MESSAGE_SUCCESS, selectedSlots.size(), + messageSelected, messageSlots); + + expectedModel.editSlot( + pair3.getKey(), + pair3.getValue().getValue(), + null, + startTime, + duration, + name, + location, + description, + tags + ); + + expectedModel.editSlot( + pair4.getKey(), + pair4.getValue().getValue(), + null, + startTime, + duration, + name, + location, + description, + tags + ); + + assertCommandSuccess(editCommand, model, commandHistory, expectedMessage, expectedModel); + } + + @Test + public void execute_InvalidTag_throwsCommandException() { + Set selectTags = new HashSet<>(Arrays.asList("tag does not exist")); + + // values to edit + String name = "new name"; + String location = "new location"; + String description = "new description"; + LocalTime startTime = LocalTime.of(8, 0); + int duration = 60; + Set tags = new HashSet<>(Arrays.asList("tag1")); + + EditCommand editCommand = new EditCommand( + name, + startTime, + duration, + location, + description, + selectTags, + tags + ); + + String expectedMessage = String.format(MESSAGE_SUCCESS_NO_CHANGE, + Messages.craftSelectedMessage(selectTags)); + + assertCommandFailure(editCommand, model, commandHistory, expectedMessage); + } + + @Test + public void execute_InvalidIndex_throwsCommandException() { + // values to edit + String name = "new name"; + String location = "new location"; + String description = "new description"; + LocalDate date = LocalDate.of(2019, 2, 2); + LocalTime startTime = LocalTime.of(8, 0); + int duration = 60; + Set tags = new HashSet<>(Arrays.asList("tag1")); + + EditCommand editCommand = new EditCommand( + 5, + name, + date, + startTime, + duration, + location, + description, + tags + ); + + String expectedMessage = Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX; + + assertCommandFailure(editCommand, model, commandHistory, expectedMessage); + } + + @Test + public void execute_validIndex_success() { + Map> selectedSlots = new TreeMap<>(); + Pair> slot = model.getLastShownItem(1); + selectedSlots.put(slot.getKey(), slot.getValue()); + + // values to edit + String name = "new name"; + String location = "new location"; + String description = "new description"; + LocalDate date = LocalDate.of(2019, 2, 2); + LocalTime startTime = LocalTime.of(8, 0); + int duration = 60; + Set tags = new HashSet<>(Arrays.asList("tag1")); + + EditCommand editCommand = new EditCommand( + 1, + "new name", + date, + startTime, + duration, + location, + description, + tags + ); + + String messageSelected = Messages.craftSelectedMessage(1); + String messageSlots = editCommand.craftSuccessMessage(selectedSlots); + + String expectedMessage = String.format(MESSAGE_SUCCESS, selectedSlots.size(), + messageSelected, messageSlots); + + expectedModel.editSlot( + pair1.getKey(), + pair1.getValue().getValue(), + date, + startTime, + duration, + name, + location, + description, + tags + ); + + assertCommandSuccess(editCommand, model, commandHistory, expectedMessage, expectedModel); + } + + @Test + public void equals() { + EditCommand editFirstCommand = new EditCommand( + 1, + "name", + LocalDate.of(2019, 2, 2), + LocalTime.of(8, 0), + 60, + "location", + "description", + new HashSet<>(Arrays.asList("tag1")) + ); + + // same object -> returns true + assertTrue(editFirstCommand.equals(editFirstCommand)); + + // same values -> returns true + EditCommand editFirstCommandCopy = new EditCommand( + 1, + "name", + LocalDate.of(2019, 2, 2), + LocalTime.of(8, 0), + 60, + "location", + "description", + new HashSet<>(Arrays.asList("tag1")) + ); + assertTrue(editFirstCommand.equals(editFirstCommandCopy)); + + // different types -> returns false + assertFalse(editFirstCommand.equals(1)); + + // null -> returns false + assertFalse(editFirstCommand.equals(null)); + + // different command -> returns false + EditCommand deleteSecondCommand = new EditCommand( + 2, + "name", + LocalDate.of(2019, 2, 2), + LocalTime.of(8, 0), + 60, + "location", + "description", + new HashSet<>(Arrays.asList("tag1")) + ); + assertFalse(editFirstCommand.equals(deleteSecondCommand)); + } +} diff --git a/test/java/planmysem/logic/Commands/ExitCommandTest.java b/test/java/planmysem/logic/Commands/ExitCommandTest.java new file mode 100644 index 000000000..bc43e97d1 --- /dev/null +++ b/test/java/planmysem/logic/Commands/ExitCommandTest.java @@ -0,0 +1,23 @@ +package planmysem.logic.Commands; + +import static planmysem.logic.Commands.CommandTestUtil.assertCommandSuccess; +import static planmysem.logic.commands.ExitCommand.MESSAGE_EXIT_ACKNOWEDGEMENT; + +import org.junit.Test; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.ExitCommand; +import planmysem.model.Model; +import planmysem.model.ModelManager; + +public class ExitCommandTest { + private Model model = new ModelManager(); + private Model expectedModel = new ModelManager(); + private CommandHistory commandHistory = new CommandHistory(); + + @Test + public void execute_exit_success() { + CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWEDGEMENT); + assertCommandSuccess(new ExitCommand(), model, commandHistory, expectedCommandResult, expectedModel); + } +} diff --git a/test/java/planmysem/logic/LogicManagerTest.java b/test/java/planmysem/logic/LogicManagerTest.java new file mode 100644 index 000000000..2c373ee50 --- /dev/null +++ b/test/java/planmysem/logic/LogicManagerTest.java @@ -0,0 +1,155 @@ +package planmysem.logic; + +import static org.junit.Assert.assertEquals; +import static planmysem.common.Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import planmysem.common.Clock; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.HistoryCommand; +import planmysem.logic.commands.ListCommand; +import planmysem.logic.commands.exceptions.CommandException; +import planmysem.logic.parser.exceptions.ParseException; +import planmysem.model.Model; +import planmysem.model.ModelManager; +import planmysem.storage.StorageFile; + + +public class LogicManagerTest { + private static final IOException DUMMY_IO_EXCEPTION = new IOException("dummy exception"); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private StorageFile storageFile; + private Model model; + private Logic logic; + + @Before + public void setUp() throws Exception { + Clock.set("2019-01-14T10:00:00Z"); + model = new ModelManager(); + storageFile = new StorageFile(temporaryFolder.newFile("testSaveFile.txt").getPath()); + logic = new LogicManager(storageFile, model); + } + + @Test + public void execute_commandExecutionError_throwsCommandException() { + String deleteCommand = "delete 3"; + assertCommandException(deleteCommand, MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); + assertHistoryCorrect(deleteCommand); + } + + @Test + public void execute_validCommand_success() { + String listCommand = ListCommand.COMMAND_WORD + " n/CS2113T"; + assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS_NONE, model); + assertHistoryCorrect(listCommand); + } +// +// @Test +// public void execute_storageThrowsIoException_throwsCommandException() throws Exception { +// // Setup LogicManager with JsonAddressBookIoExceptionThrowingStub +// JsonAddressBookStorage addressBookStorage = +// new JsonAddressBookIoExceptionThrowingStub(temporaryFolder.newFile().toPath()); +// JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.newFile().toPath()); +// StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); +// logic = new LogicManager(model, storage); +// +// // Execute add command +// String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY +// + ADDRESS_DESC_AMY; +// Person expectedPerson = new PersonBuilder(AMY).withTags().build(); +// ModelManager expectedModel = new ModelManager(); +// expectedModel.addPerson(expectedPerson); +// expectedModel.commitAddressBook(); +// String expectedMessage = LogicManager.FILE_OPS_ERROR_MESSAGE + DUMMY_IO_EXCEPTION; +// assertCommandBehavior(CommandException.class, addCommand, expectedMessage, expectedModel); +// assertHistoryCorrect(addCommand); +// } +// +// @Test +// public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException() { +// thrown.expect(UnsupportedOperationException.class); +// logic.getFilteredPersonList().remove(0); +// } + + /** + * Executes the command, confirms that no exceptions are thrown and that the result message is correct. + * Also confirms that {@code expectedModel} is as specified. + * @see #assertCommandBehavior(Class, String, String, Model) + */ + private void assertCommandSuccess(String inputCommand, String expectedMessage, Model expectedModel) { + assertCommandBehavior(null, inputCommand, expectedMessage, expectedModel); + } + + /** + * Executes the command, confirms that a ParseException is thrown and that the result message is correct. + * @see #assertCommandBehavior(Class, String, String, Model) + */ + private void assertParseException(String inputCommand, String expectedMessage) { + assertCommandFailure(inputCommand, ParseException.class, expectedMessage); + } + + /** + * Executes the command, confirms that a CommandException is thrown and that the result message is correct. + * @see #assertCommandBehavior(Class, String, String, Model) + */ + private void assertCommandException(String inputCommand, String expectedMessage) { + assertCommandFailure(inputCommand, CommandException.class, expectedMessage); + } + + /** + * Executes the command, confirms that the exception is thrown and that the result message is correct. + * @see #assertCommandBehavior(Class, String, String, Model) + */ + private void assertCommandFailure(String inputCommand, Class expectedException, String expectedMessage) { + Model expectedModel = new ModelManager(); + assertCommandBehavior(expectedException, inputCommand, expectedMessage, expectedModel); + } + + /** + * Executes the command, confirms that the result message is correct and that the expected exception is thrown, + * and also confirms that the following two parts of the LogicManager object's state are as expected:
+ * - the internal model manager data are same as those in the {@code expectedModel}
+ * - {@code expectedModel}'s planner was saved to the storage file. + */ + private void assertCommandBehavior(Class expectedException, String inputCommand, + String expectedMessage, Model expectedModel) { + + try { + CommandResult result = logic.execute(inputCommand); + assertEquals(expectedException, null); + assertEquals(expectedMessage, result.getFeedbackToUser()); + } catch (CommandException | ParseException e) { + assertEquals(expectedException, e.getClass()); + assertEquals(expectedMessage, e.getMessage()); + } + + assertEquals(expectedModel, model); + } + + /** + * Asserts that the result display shows all the {@code expectedCommands} upon the execution of + * {@code HistoryCommand}. + */ + private void assertHistoryCorrect(String... expectedCommands) { + try { + CommandResult result = logic.execute(HistoryCommand.COMMAND_WORD); + String expectedMessage = String.format( + HistoryCommand.MESSAGE_SUCCESS, String.join("\n", expectedCommands)); + assertEquals(expectedMessage, result.getFeedbackToUser()); + } catch (ParseException | CommandException e) { + throw new AssertionError("Parsing and execution of HistoryCommand.COMMAND_WORD should succeed.", e); + } + } +} diff --git a/test/java/planmysem/logic/LogicTest.java b/test/java/planmysem/logic/LogicTest.java deleted file mode 100644 index b9c9c3549..000000000 --- a/test/java/planmysem/logic/LogicTest.java +++ /dev/null @@ -1,679 +0,0 @@ -package planmysem.logic; - - -import static junit.framework.TestCase.assertEquals; -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_MULTIPLE_PARAMS; -import static planmysem.common.Messages.MESSAGE_INVALID_SLOT_DISPLAYED_INDEX; -import static planmysem.common.Messages.MESSAGE_INVALID_TIME; - -import java.time.Clock; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringJoiner; -import java.util.TreeMap; - -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.FindCommand; -import planmysem.commands.HelpCommand; -import planmysem.commands.ListCommand; -import planmysem.common.Messages; -import planmysem.common.Utils; -import planmysem.data.Planner; -import planmysem.data.recurrence.Recurrence; -import planmysem.data.semester.Day; -import planmysem.data.semester.ReadOnlyDay; -import planmysem.data.semester.Semester; -import planmysem.data.slot.ReadOnlySlot; -import planmysem.data.slot.Slot; -import planmysem.storage.StorageFile; - -public class LogicTest { - - /** - * See https://github.com/junit-team/junit4/wiki/rules#temporaryfolder-rule - */ - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private StorageFile storageFile; - private Planner planner; - private Logic logic; - - @Before - public void setup() throws Exception { - storageFile = new StorageFile(temporaryFolder.newFile("testSaveFile.txt").getPath()); - planner = createPlanner(); - storageFile.save(planner); - logic = new Logic(storageFile, planner); - Instant.now(Clock.fixed( - Instant.parse("2019-02-02T10:00:00Z"), - ZoneOffset.UTC)); - } - - private Planner createPlanner() { - return new Planner(Semester.generateSemester(LocalDate.of(2019, 1, 14))); - } - - @Test - 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(null, 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 { - Planner expectedPlanner = createPlanner(); - TestDataHelper helper = new TestDataHelper(); - planner.addSlot(LocalDate.of(2019, 2, 1), helper.generateSlot(1)); - planner.addSlot(LocalDate.of(2019, 2, 1), helper.generateSlot(2)); - planner.addSlot(LocalDate.of(2019, 2, 1), helper.generateSlot(3)); - - assertCommandBehavior("clear", - ClearCommand.MESSAGE_SUCCESS, - expectedPlanner, - false, - null); - } - - /** - * 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_by_date_successful() throws Exception { - // item to be added - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - LocalDate dateToBeAdded = LocalDate.of(2019, 2, 2); - - // expectation - Planner expectedPlanner = createPlanner(); - expectedPlanner.addSlot(dateToBeAdded, slotToBeAdded); - Map days = new TreeMap<>(); - days.put(dateToBeAdded, expectedPlanner.getDay(dateToBeAdded)); - - // execute command and verify result - assertCommandBehavior(helper.generateAddCommand(slotToBeAdded, dateToBeAdded, ""), - String.format(AddCommand.MESSAGE_SUCCESS, - 1, - AddCommand.craftSuccessMessage(days, slotToBeAdded)), - expectedPlanner, - false, - null); - } - - @Test - public void execute_add_by_day_successful() throws Exception { - // because adding by day takes the nearest day, the planner has to be changed to be in respects to the current system date. - // item to be added - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - int dayToBeAdded = LocalDate.now().getDayOfWeek().getValue(); - LocalDate dateToBeAdded = Utils.getNearestDayOfWeek(LocalDate.now(), dayToBeAdded); - - // expectation - Planner expectedPlanner = createPlanner(); - expectedPlanner.addSlot(dateToBeAdded, slotToBeAdded); - Map days = new TreeMap<>(); - days.put(dateToBeAdded, expectedPlanner.getDay(dateToBeAdded)); - - // execute command and verify result - assertCommandBehavior(helper.generateAddCommand(slotToBeAdded, dayToBeAdded, ""), - String.format(AddCommand.MESSAGE_SUCCESS, - 1, - AddCommand.craftSuccessMessage(days, slotToBeAdded)), - expectedPlanner, - false, - null); - } - - @Test - public void execute_add_by_date_multiple_successful() throws Exception { - // item to be added - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - LocalDate dateToBeAdded = LocalDate.of(2019, 2, 2); - - // expectation - Planner expectedPlanner = createPlanner(); - Recurrence recurrence = new Recurrence(new HashSet<>(Arrays.asList("normal")), dateToBeAdded); - Map days = new TreeMap<>(); - for (LocalDate date : recurrence.generateDates(expectedPlanner.getSemester())) { - expectedPlanner.addSlot(date, slotToBeAdded); - days.put(date, expectedPlanner.getDay(date)); - } - - - // execute command and verify result - assertCommandBehavior(helper.generateAddCommand(slotToBeAdded, dateToBeAdded, "r/normal"), - String.format(AddCommand.MESSAGE_SUCCESS, - days.size(), - AddCommand.craftSuccessMessage(days, slotToBeAdded)), - expectedPlanner, - false, - null); - } - - @Test - public void execute_add_by_day_multiple_successful() throws Exception { - // item to be added - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - int dayToBeAdded = LocalDate.of(2019, 2, 2).getDayOfWeek().getValue(); - LocalDate dateToBeAdded = Utils.getNearestDayOfWeek(LocalDate.now(), dayToBeAdded); - - // expectation - Planner expectedPlanner = createPlanner(); - Recurrence recurrence = new Recurrence(new HashSet<>(Arrays.asList("normal")), dayToBeAdded); - Map days = new TreeMap<>(); - for (LocalDate date : recurrence.generateDates(expectedPlanner.getSemester())) { - expectedPlanner.addSlot(date, slotToBeAdded); - days.put(date, expectedPlanner.getDay(date)); - } - - - // execute command and verify result - assertCommandBehavior(helper.generateAddCommand(slotToBeAdded, dayToBeAdded, "r/normal"), - String.format(AddCommand.MESSAGE_SUCCESS, - days.size(), - AddCommand.craftSuccessMessage(days, slotToBeAdded)), - expectedPlanner, - false, - null); - } - - @Test - public void execute_add_unsuccessful() throws Exception { - // item to be added - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - LocalDate dateToBeAdded = LocalDate.of(1999, 1, 1); - - // execute command and verify result - assertCommandBehavior(helper.generateAddCommand(slotToBeAdded, dateToBeAdded, ""), - AddCommand.MESSAGE_FAIL_OUT_OF_BOUNDS); - } - - /** - * Test edit 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 - public void execute_edit_successful() throws Exception { - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - LocalDate dateToBeAdded = LocalDate.of(2019, 2, 2); - planner.addSlot(dateToBeAdded, new Slot(slotToBeAdded)); - Map> selectedSlots = new TreeMap<>(); - selectedSlots.put(dateToBeAdded, - new Pair(planner.getDay(dateToBeAdded), new Slot(slotToBeAdded))); - - // setup expectations - Planner expectedPlanner = createPlanner(); - expectedPlanner.addSlot(dateToBeAdded, slotToBeAdded); - - // create tags - Set tags = new HashSet<>(); - tags.add("CS2113T"); - - Set newTags = new HashSet<>(); - newTags.add("CS2101"); - - expectedPlanner.editSlot(dateToBeAdded, slotToBeAdded, null, LocalTime.of(4, 0), 60, - "test", "testlo", "testdes", newTags); - - // Just to generate the crafted message in this case. - EditCommand ec = new EditCommand("test", LocalTime.of(4, 0), 60, - "testlo", "testdes", tags, newTags); - - // execute command and verify result - assertCommandBehavior("edit t/CS2113T nt/CS2101 nn/test nl/testlo ndes/testdes nst/04:00 net/60", - String.format(EditCommand.MESSAGE_SUCCESS, selectedSlots.size(), - Messages.craftSelectedMessage(tags), ec.craftSuccessMessage(selectedSlots)), - expectedPlanner, - false, - null); - } - - @Test - public void execute_edit_no_change_successful() throws Exception { - Set tags = new HashSet<>(); - tags.add("someTagThatDoesNotExist"); - - assertCommandBehavior("edit t/someTagThatDoesNotExist n/test", - String.format(EditCommand.MESSAGE_SUCCESS_NO_CHANGE, - Messages.craftSelectedMessage(tags))); - } - - @Test - public void execute_edit_invalid_slot_displayed_unsuccessful() throws Exception { - assertCommandBehavior("edit 100", MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); - } - -// @Test -// public void execute_edit_out_of_bound_unsuccessful() throws Exception { -// assertCommandBehavior("edit 100", MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); -// } - - /** - * 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); - } - - @Test - public void execute_delete_successful() throws Exception { - TestDataHelper helper = new TestDataHelper(); - Slot slotToBeAdded = helper.slotOne(); - LocalDate dateToBeAdded = LocalDate.of(2019, 2, 2); - planner.addSlot(dateToBeAdded, slotToBeAdded); - Map> selectedSlots = new TreeMap<>(); - selectedSlots.put(dateToBeAdded, - new Pair(planner.getDay(dateToBeAdded), slotToBeAdded)); - - // setup expectations - Planner expectedPlanner = createPlanner(); - expectedPlanner.addSlot(dateToBeAdded, slotToBeAdded); - expectedPlanner.getSemester().removeSlot(dateToBeAdded, slotToBeAdded); - - // execute command and verify result - assertCommandBehavior(helper.generateDeleteCommand(slotToBeAdded), - String.format(DeleteCommand.MESSAGE_SUCCESS, - 1, - Messages.craftSelectedMessage(slotToBeAdded.getTags()), - Messages.craftSelectedMessage("Deleted Slots:", selectedSlots)), expectedPlanner, - false, - null); - } - - @Test - public void execute_delete_no_change_successful() throws Exception { - TestDataHelper helper = new TestDataHelper(); - Set tags = new HashSet<>(); - tags.add("someTagThatDoesNotExist"); - - assertCommandBehavior(helper.generateDeleteCommand(tags), - String.format(DeleteCommand.MESSAGE_SUCCESS_NO_CHANGE, - Messages.craftSelectedMessage(tags))); - } - - @Test - public void execute_delete_invalid_slot_displayed_unsuccessful() throws Exception { - assertCommandBehavior("delete 100", MESSAGE_INVALID_SLOT_DISPLAYED_INDEX); - } - - /** - * Test find command - */ - - @Test - public void execute_find_invalidArgsFormat() throws Exception { - String expectedMessageSingle = String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE); - String expectedMessageMultipleParams = String.format(MESSAGE_INVALID_MULTIPLE_PARAMS, FindCommand.MESSAGE_USAGE); - assertCommandBehavior( - "find wrong args wrong args", expectedMessageSingle); - assertCommandBehavior( - "find n/CS2113T t/Tutorial", expectedMessageMultipleParams); - } - - @Test - public void execute_list_invalidArgsFormat() throws Exception { - String expectedMessageSingle = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE); - String expectedMessageMultipleParams = String.format(MESSAGE_INVALID_MULTIPLE_PARAMS, ListCommand.MESSAGE_USAGE); - assertCommandBehavior( - "list wrong args wrong args", expectedMessageSingle); - assertCommandBehavior( - "list n/CS2113T t/Tutorial", expectedMessageMultipleParams); - } - - /** - * 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, null); - } - - /** - * 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 { - String name = "CS2113T Tutorial"; - String location = "COM2 04-11"; - String description = "Topic: Sequence Diagram"; - LocalTime startTime = LocalTime.parse("08:00"); - LocalTime endTime = LocalTime.parse("09:00"); - Set tags = new HashSet<>(Arrays.asList( "CS2113T", "Tutorial")); - 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( - "slot " + seed, - "location " + Math.abs(seed), - "description " + Math.abs(seed), - LocalTime.parse("00:00"), - LocalTime.parse("00:00"), - new HashSet<>(Arrays.asList("tag" + Math.abs(seed), "tag" + Math.abs(seed + 1))) - ); - } - - /** Generates the correct add command based on the person given */ - String generateAddCommand(Slot s, LocalDate date, String recurrence) { - StringJoiner cmd = new StringJoiner(" "); - - cmd.add("add"); - - cmd.add("n/" + s.getName()); - cmd.add("d/" + Utils.parseDate(date)); - 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(String tag : tags){ - cmd.add("t/" + tag); - } - } - - cmd.add(recurrence); - - return cmd.toString(); - } - - /** Generates the correct add command based on the person given */ - String generateAddCommand(Slot s, int day, String recurrence) { - StringJoiner cmd = new StringJoiner(" "); - - cmd.add("add"); - - cmd.add("n/" + s.getName()); - cmd.add("d/" + day); - 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(String tag : tags){ - cmd.add("t/" + tag); - } - } - - cmd.add(recurrence); - - return cmd.toString(); - } - - /** Generates the correct delete command based on tags */ - String generateDeleteCommand(Set tags) { - StringJoiner cmd = new StringJoiner(" "); - - cmd.add("delete"); - - if (tags != null) { - for(String tag : tags){ - cmd.add("t/" + tag); - } - } - - return cmd.toString(); - } - - /** Generates the correct delete command based on the slot. */ - String generateDeleteCommand(Slot slot) { - StringJoiner cmd = new StringJoiner(" "); - - cmd.add("delete"); - - Set tags = slot.getTags(); - if (tags != null) { - for(String tag : tags){ - cmd.add("t/" + tag); - } - } - - return cmd.toString(); - } - - // public String recurrenceToString(Recurrence recurrence) { - // StringJoiner cmd = new StringJoiner(" "); - // if (recurrence.recess) { - // cmd.add("r/" + "recess"); - // } - // if (reading) { - // cmd.add("r/" + "reading"); - // } - // if (normal) { - // cmd.add("r/" + "normal"); - // } - // if (exam) { - // cmd.add("r/" + "exam"); - // } - // if (past) { - // cmd.add("r/" + "past"); - // } - // 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/logic/parser/AddCommandParserTest.java b/test/java/planmysem/logic/parser/AddCommandParserTest.java new file mode 100644 index 000000000..1b76612db --- /dev/null +++ b/test/java/planmysem/logic/parser/AddCommandParserTest.java @@ -0,0 +1,324 @@ +package planmysem.logic.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_TAG; +import static planmysem.common.Messages.MESSAGE_INVALID_TIME; +import static planmysem.logic.parser.CommandParserTestUtil.assertParseFailure; +import static planmysem.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Before; +import org.junit.Test; +import planmysem.common.Clock; +import planmysem.logic.commands.AddCommand; +import planmysem.model.recurrence.Recurrence; +import planmysem.model.slot.Slot; + +public class AddCommandParserTest { + private AddCommandParser parser = new AddCommandParser(); + + @Before + public void setup() { + Clock.set("2019-01-13T10:00:00Z"); + } + + @Test + public void parse_minimalFields_success() { + // Date represented in day format + assertParseSuccess(parser, + "n/CS2113T Tutorial d/mon st/08:00 et/09:00", + new AddCommand(new Slot( + "CS2113T Tutorial", + null, + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + null + ) + , new Recurrence( + null, + 1 + ))); + + // Date represented in date format + assertParseSuccess(parser, + "n/CS2113T Tutorial d/14-01-2019 st/08:00 et/09:00", + new AddCommand(new Slot( + "CS2113T Tutorial", + null, + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + null + ) + , new Recurrence( + null, + LocalDate.of(2019, 1, 14) + ))); + } + + @Test + public void parse_optionalFieldsMissing_success() { + assertParseSuccess(parser, + "n/CS2113T Tutorial d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial", + new AddCommand(new Slot( + "CS2113T Tutorial", + null, + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")) + ) + , new Recurrence( + null, + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram", + new AddCommand(new Slot( + "CS2113T Tutorial", + null, + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + ) + , new Recurrence( + null, + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial l/COM2 04-01 d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")) + ) + , new Recurrence( + null, + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial l/COM2 04-01 d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + ) + , new Recurrence( + null, + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial l/COM2 04-01 d/mon st/08:00 et/09:00 t/CS2113T t/Tutorial", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")) + ) + , new Recurrence( + null, + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial l/COM2 04-01 d/mon st/08:00 et/09:00", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + ) + , new Recurrence( + null, + 1 + ))); + } + + @Test + public void parse_AllFieldsMissing_success() { + assertParseSuccess(parser, + "n/CS2113T Tutorial d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial r/normal", + new AddCommand(new Slot( + "CS2113T Tutorial", + null, + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")) + ), + new Recurrence( + new HashSet<>(Arrays.asList("normal")), + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram r/exam", + new AddCommand(new Slot( + "CS2113T Tutorial", + null, + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + ) + , new Recurrence( + new HashSet<>(Arrays.asList("exam")), + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial r/reading r/exam l/COM2 04-01 d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")) + ) + , new Recurrence( + new HashSet<>(Arrays.asList("reading", "exam")), + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial l/COM2 04-01 d/mon st/08:00 et/09:00 r/normal r/exam des/Topic: Sequence Diagram", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + "Topic: Sequence Diagram", + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + ) + , new Recurrence( + new HashSet<>(Arrays.asList("normal", "exam")), + 1 + ))); + + assertParseSuccess(parser, + "n/CS2113T Tutorial l/COM2 04-01 d/mon r/reading r/recess st/08:00 et/09:00 t/CS2113T t/Tutorial", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")) + ) + , new Recurrence( + new HashSet<>(Arrays.asList("reading", "recess")), + 1 + ))); + + assertParseSuccess(parser, + "r/normal r/reading r/recess r/exam n/CS2113T Tutorial l/COM2 04-01 d/mon st/08:00 et/09:00", + new AddCommand(new Slot( + "CS2113T Tutorial", + "COM2 04-01", + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + new HashSet<>() + ) + , new Recurrence( + new HashSet<>(Arrays.asList("reading", "recess", "normal", "exam")), + 1 + ))); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); + + // missing name prefix + assertParseFailure(parser, + "add d/mon st/08:00 et/09:00", + expectedMessage); + + // missing date prefix + assertParseFailure(parser, + "add n/CS2113T Tutorial st/08:00 et/09:00", + expectedMessage); + + // missing start time prefix + assertParseFailure(parser, + "add n/CS2113T Tutorial d/mon et/09:00 ", + expectedMessage); + + // missing end time prefix + assertParseFailure(parser, + "add n/CS2113T Tutorial d/mon st/08:00", + expectedMessage); + + // all prefixes missing + assertParseFailure(parser, + "add", + expectedMessage); + } + + @Test + public void parse_invalidDate_failure() { + // invalid day + assertParseFailure(parser, + "add n/CS2113T Tutorial d/0 st/08:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial r/normal", + String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_DATE)); + + // invalid date + assertParseFailure(parser, + "add n/CS2113T Tutorial d/19999 st/08:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial r/normal", + String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_DATE)); + } + + @Test + public void parse_invalidTime_failure() { + // invalid start time + assertParseFailure(parser, + "add n/CS2113T Tutorial d/mon st/25:00 et/09:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial r/normal", + String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + + // invalid end time + assertParseFailure(parser, + "add n/CS2113T Tutorial d/mon st/08:00 et/25:00 des/Topic: Sequence Diagram t/CS2113T t/Tutorial r/normal", + String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + assertParseFailure(parser, + "add n/CS2113T Tutorial d/mon st/08:00 et/13:00AM des/Topic: Sequence Diagram t/CS2113T t/Tutorial r/normal", + String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TIME)); + } + + @Test + public void parse_invalidTag_failure() { + // invalid start time + assertParseFailure(parser, + "add n/CS2113T Tutorial d/mon st/08:00 et/09:00 des/Topic: Sequence Diagram t/ t/Tutorial r/normal", + String.format(MESSAGE_INVALID_COMMAND_FORMAT_ADDITIONAL, + AddCommand.MESSAGE_USAGE, MESSAGE_INVALID_TAG)); + } +} diff --git a/test/java/planmysem/logic/parser/CommandParserTestUtil.java b/test/java/planmysem/logic/parser/CommandParserTestUtil.java new file mode 100644 index 000000000..c319f89b2 --- /dev/null +++ b/test/java/planmysem/logic/parser/CommandParserTestUtil.java @@ -0,0 +1,38 @@ +package planmysem.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import planmysem.logic.commands.Command; +import planmysem.logic.parser.exceptions.ParseException; + +/** + * Contains helper methods for testing command parsers. + */ +public class CommandParserTestUtil { + + /** + * Asserts that the parsing of {@code userInput} by {@code parser} is successful and the command created + * equals to {@code expectedCommand}. + */ + public static void assertParseSuccess(Parser parser, String userInput, Command expectedCommand) { + try { + Command command = parser.parse(userInput); + assertEquals(expectedCommand, command); + } catch (ParseException pe) { + throw new IllegalArgumentException("Invalid userInput.", pe); + } + } + + /** + * Asserts that the parsing of {@code userInput} by {@code parser} is unsuccessful and the error message + * equals to {@code expectedMessage}. + */ + public static void assertParseFailure(Parser parser, String userInput, String expectedMessage) { + try { + parser.parse(userInput); + throw new AssertionError("The expected ParseException was not thrown."); + } catch (ParseException pe) { + assertEquals(expectedMessage, pe.getMessage()); + } + } +} diff --git a/test/java/planmysem/logic/parser/DeleteCommandParserTest.java b/test/java/planmysem/logic/parser/DeleteCommandParserTest.java new file mode 100644 index 000000000..3b179a29e --- /dev/null +++ b/test/java/planmysem/logic/parser/DeleteCommandParserTest.java @@ -0,0 +1,89 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static planmysem.logic.parser.CommandParserTestUtil.assertParseFailure; +import static planmysem.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Before; +import org.junit.Test; +import planmysem.common.Clock; +import planmysem.logic.commands.DeleteCommand; + +public class DeleteCommandParserTest { + private DeleteCommandParser parser = new DeleteCommandParser(); + + @Before + public void setup() { + Clock.set("2019-01-14T10:00:00Z"); + } + + @Test + public void parse_validTags_success() { + // single tag + assertParseSuccess(parser, + "t/CS2113T", + new DeleteCommand( + new HashSet<>(Arrays.asList("CS2113T") + ) + ) + ); + + // multiple tags + assertParseSuccess(parser, + "t/CS2113T t/Tutorial t/Hard", + new DeleteCommand( + new HashSet<>(Arrays.asList("CS2113T", "Tutorial", "Hard") + ) + ) + ); + } + + + @Test + public void parse_validIndex_success() { + assertParseSuccess(parser, + "1", + new DeleteCommand(1) + ); + + assertParseSuccess(parser, + "100", + new DeleteCommand(100) + ); + } + + @Test + public void parse_noIndexNoTag_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); + + assertParseFailure(parser, + "nt/CS2113T nt/Tutorial nt/Hard", + expectedMessage + ); + + assertParseFailure(parser, + "", + expectedMessage + ); + } + + @Test + public void parse_invalidIndex_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); + + assertParseFailure(parser, + "0", + expectedMessage + ); + + // multiple tags + assertParseFailure(parser, + "-1", + expectedMessage + ); + } + +} diff --git a/test/java/planmysem/logic/parser/EditCommandParserTest.java b/test/java/planmysem/logic/parser/EditCommandParserTest.java new file mode 100644 index 000000000..f1a91b9d4 --- /dev/null +++ b/test/java/planmysem/logic/parser/EditCommandParserTest.java @@ -0,0 +1,149 @@ +package planmysem.logic.parser; + +import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static planmysem.logic.parser.CommandParserTestUtil.assertParseFailure; +import static planmysem.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.time.LocalTime; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Before; +import org.junit.Test; +import planmysem.common.Clock; +import planmysem.logic.commands.EditCommand; + +public class EditCommandParserTest { + private EditCommandParser parser = new EditCommandParser(); + + @Before + public void setup() { + Clock.set("2019-01-13T10:00:00Z"); + } + + @Test + public void parse_validTags_success() { + // single tag + assertParseSuccess(parser, + "t/CS2113T nl/COM2 04-01", + new EditCommand( + null, + null, + -1, + "COM2 04-01", + null, + new HashSet<>(Arrays.asList("CS2113T")), + new HashSet<>() + ) + ); + + // multiple tags + assertParseSuccess(parser, + "t/CS2113T t/Tutorial nst/08:00 net/09:00 t/Hard", + new EditCommand( + null, + LocalTime.of(8,0), + 60, + null, + null, + new HashSet<>(Arrays.asList("CS2113T", "Tutorial", "Hard")), + new HashSet<>() + ) + ); + + // multiple tags all input + assertParseSuccess(parser, + "nn/CS2113T nst/08:00 net/75 ndes/So tough nt/new tag t/CS2113T t/Tutorial nl/COM2 04-01 t/Hard", + new EditCommand( + "CS2113T", + LocalTime.of(8, 0), + 75, + "COM2 04-01", + "So tough", + new HashSet<>(Arrays.asList("CS2113T", "Tutorial", "Hard")), + new HashSet<>(Arrays.asList("new tag")) + ) + ); + } + + @Test + public void parse_validIndex_success() { + assertParseSuccess(parser, + "1 nl/COM2 04-01", + new EditCommand( + 1, + null, + null, + null, + -1, + "COM2 04-01", + null, + new HashSet<>() + ) + ); + + assertParseSuccess(parser, + "100 nl/COM2 04-01", + new EditCommand( + 100, + null, + null, + null, + -1, + "COM2 04-01", + null, + new HashSet<>() + ) + ); + } + + @Test + public void parse_invalidIndex_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + + assertParseFailure(parser, + "0", + expectedMessage + ); + + assertParseFailure(parser, + "-1", + expectedMessage + ); + } + + @Test + public void parse_noIndexNoTag_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + + assertParseFailure(parser, + "nl/COM2 04-01", + expectedMessage + ); + + assertParseFailure(parser, + "", + expectedMessage + ); + } + + @Test + public void parse_InvalidStartTime_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + + assertParseFailure(parser, + "1 nst/25:00", + expectedMessage + ); + } + + @Test + public void parse_InvalidEndTime_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); + + assertParseFailure(parser, + "1 net/25:00", + expectedMessage + ); + } +} diff --git a/test/java/planmysem/logic/parser/HelpCommandTest.java b/test/java/planmysem/logic/parser/HelpCommandTest.java new file mode 100644 index 000000000..ea4ad0b41 --- /dev/null +++ b/test/java/planmysem/logic/parser/HelpCommandTest.java @@ -0,0 +1,23 @@ +package planmysem.logic.parser; + +import static planmysem.logic.Commands.CommandTestUtil.assertCommandSuccess; +import static planmysem.logic.commands.HelpCommand.MESSAGE_ALL_USAGES; + +import org.junit.Test; +import planmysem.logic.CommandHistory; +import planmysem.logic.commands.CommandResult; +import planmysem.logic.commands.HelpCommand; +import planmysem.model.Model; +import planmysem.model.ModelManager; + +public class HelpCommandTest { + private Model model = new ModelManager(); + private Model expectedModel = new ModelManager(); + private CommandHistory commandHistory = new CommandHistory(); + + @Test + public void execute_help_success() { + CommandResult expectedCommandResult = new CommandResult(MESSAGE_ALL_USAGES); + assertCommandSuccess(new HelpCommand(), model, commandHistory, expectedCommandResult, expectedModel); + } +} diff --git a/test/java/planmysem/logic/parser/ParserManagerTest.java b/test/java/planmysem/logic/parser/ParserManagerTest.java new file mode 100644 index 000000000..5f38ec9fe --- /dev/null +++ b/test/java/planmysem/logic/parser/ParserManagerTest.java @@ -0,0 +1,228 @@ +package planmysem.logic.parser; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import planmysem.logic.commands.AddCommand; +import planmysem.logic.commands.ClearCommand; +import planmysem.logic.commands.DeleteCommand; +import planmysem.logic.commands.EditCommand; +import planmysem.logic.commands.ExitCommand; +import planmysem.logic.commands.HelpCommand; +import planmysem.model.recurrence.Recurrence; +import planmysem.model.slot.Slot; + +public class ParserManagerTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final ParserManager parser = new ParserManager(); + + @Test + public void parseCommand_add_via_day() throws Exception { + AddCommand command = (AddCommand) parser.parseCommand( + AddCommand.COMMAND_WORD + " " + "n/CS2113T Tutorial d/mon st/08:00 et/09:00"); + assertEquals(new AddCommand(new Slot( + "CS2113T Tutorial", + null, + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + null + ) + , new Recurrence( + null, + 1 + )), command); + + AddCommand commandShort = (AddCommand) parser.parseCommand( + AddCommand.COMMAND_WORD_SHORT + " " + "n/CS2113T Tutorial d/mon st/08:00 et/09:00"); + assertEquals(new AddCommand(new Slot( + "CS2113T Tutorial", + null, + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + null + ) + , new Recurrence( + null, + 1 + )), commandShort); + } + + @Test + public void parseCommand_add_via_date() throws Exception { + AddCommand command = (AddCommand) parser.parseCommand( + AddCommand.COMMAND_WORD + " " + "n/CS2113T Tutorial d/21-01-2019 st/08:00 et/09:00"); + assertEquals(new AddCommand(new Slot( + "CS2113T Tutorial", + null, + null, + LocalTime.of(8, 0), + LocalTime.of(9, 0), + null + ) + , new Recurrence( + null, + LocalDate.of(2019, 1, 21) + )), command); + } + + @Test + public void parseCommand_delete_via_index() throws Exception { + DeleteCommand command = (DeleteCommand) parser.parseCommand( + DeleteCommand.COMMAND_WORD + " 1"); + assertEquals(new DeleteCommand(1), command); + + DeleteCommand commandAlt = (DeleteCommand) parser.parseCommand( + DeleteCommand.COMMAND_WORD_ALT + " 1"); + assertEquals(new DeleteCommand(1), commandAlt); + + DeleteCommand commandShort = (DeleteCommand) parser.parseCommand( + DeleteCommand.COMMAND_WORD_SHORT + " 1"); + assertEquals(new DeleteCommand(1), commandShort); + + + } + + @Test + public void parseCommand_delete_via_tags() throws Exception { + DeleteCommand command = (DeleteCommand) parser.parseCommand( + DeleteCommand.COMMAND_WORD + " t/CS2113T t/Tutorial"); + assertEquals(new DeleteCommand(new HashSet<>(Arrays.asList("CS2113T", "Tutorial"))), command); + } + + @Test + public void parseCommand_edit_via_index() throws Exception { + EditCommand command = (EditCommand) parser.parseCommand( + EditCommand.COMMAND_WORD + " " + "1 nl/COM2 04-01"); + assertEquals(new EditCommand( + 1, + null, + null, + null, + -1, + "COM2 04-01", + null, + new HashSet<>() + ), command); + + EditCommand commandShort = (EditCommand) parser.parseCommand( + EditCommand.COMMAND_WORD_SHORT + " " + "1 nl/COM2 04-01"); + assertEquals(new EditCommand( + 1, + null, + null, + null, + -1, + "COM2 04-01", + null, + new HashSet<>() + ), commandShort); + } + + @Test + public void parseCommand_edit_via_tags() throws Exception { + EditCommand command = (EditCommand) parser.parseCommand( + EditCommand.COMMAND_WORD + " " + "t/CS2113T nl/COM2 04-01"); + assertEquals(new EditCommand( + null, + null, + -1, + "COM2 04-01", + null, + new HashSet<>(Arrays.asList("CS2113T")), + new HashSet<>() + ), command); + } + + + @Test + public void parseCommand_help() throws Exception { + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); + } + + @Test + public void parseCommand_clear() throws Exception { + assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); + assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof ClearCommand); + } + + @Test + public void parseCommand_exit() throws Exception { + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); + } +// +// @Test +// public void parseCommand_find() throws Exception { +// List keywords = Arrays.asList("foo", "bar", "baz"); +// FindCommand command = (FindCommand) parser.parseCommand( +// FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); +// assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); +// } +// + +// +// @Test +// public void parseCommand_history() throws Exception { +// assertTrue(parser.parseCommand(HistoryCommand.COMMAND_WORD) instanceof HistoryCommand); +// assertTrue(parser.parseCommand(HistoryCommand.COMMAND_WORD + " 3") instanceof HistoryCommand); +// +// try { +// parser.parseCommand("histories"); +// throw new AssertionError("The expected ParseException was not thrown."); +// } catch (ParseException pe) { +// assertEquals(MESSAGE_UNKNOWN_COMMAND, pe.getMessage()); +// } +// } +// +// @Test +// public void parseCommand_list() throws Exception { +// assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); +// assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); +// } +// +// @Test +// public void parseCommand_select() throws Exception { +// SelectCommand command = (SelectCommand) parser.parseCommand( +// SelectCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); +// assertEquals(new SelectCommand(INDEX_FIRST_PERSON), command); +// } +// +// @Test +// public void parseCommand_redoCommandWord_returnsRedoCommand() throws Exception { +// assertTrue(parser.parseCommand(RedoCommand.COMMAND_WORD) instanceof RedoCommand); +// assertTrue(parser.parseCommand("redo 1") instanceof RedoCommand); +// } +// +// @Test +// public void parseCommand_undoCommandWord_returnsUndoCommand() throws Exception { +// assertTrue(parser.parseCommand(UndoCommand.COMMAND_WORD) instanceof UndoCommand); +// assertTrue(parser.parseCommand("undo 3") instanceof UndoCommand); +// } +// +// @Test +// public void parseCommand_unrecognisedInput_throwsParseException() throws Exception { +// thrown.expect(ParseException.class); +// thrown.expectMessage(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); +// parser.parseCommand(""); +// } +// +// @Test +// public void parseCommand_unknownCommand_throwsParseException() throws Exception { +// thrown.expect(ParseException.class); +// thrown.expectMessage(MESSAGE_UNKNOWN_COMMAND); +// parser.parseCommand("unknownCommand"); +// } +} diff --git a/test/java/planmysem/data/PlannerTest.java b/test/java/planmysem/model/PlannerTest.java similarity index 95% rename from test/java/planmysem/data/PlannerTest.java rename to test/java/planmysem/model/PlannerTest.java index 525ced206..937cd26dc 100644 --- a/test/java/planmysem/data/PlannerTest.java +++ b/test/java/planmysem/model/PlannerTest.java @@ -1,4 +1,4 @@ -package planmysem.data; +package planmysem.model; import static junit.framework.TestCase.assertEquals; @@ -12,8 +12,9 @@ import org.junit.Test; -import planmysem.data.semester.Day; -import planmysem.data.semester.Semester; +import planmysem.common.Clock; +import planmysem.model.semester.Day; +import planmysem.model.semester.Semester; public class PlannerTest { @@ -47,12 +48,12 @@ public void execute_generateSemester() { * Asserts that the generated and expected Semester contents are equal. */ private void assertSameSemester(Semester generatedSemester, Semester expectedSemester) { - //Confirm the state of data is as expected + //Confirm the state of model is as expected assertEquals(generatedSemester.hashCode(), expectedSemester.hashCode()); } /** - * A utility class to generate test data. + * A utility class to generate test model. */ public class TestDataHelper { @@ -65,7 +66,7 @@ public class TestDataHelper { */ Semester generateSemesterFromDate(LocalDate startDate, String acadSem) { String acadYear = null; - LocalDate endDate = LocalDate.now(); + LocalDate endDate = LocalDate.now(Clock.get()); int givenYear = startDate.getYear(); int weekOfStartDate = startDate.get(WeekFields.ISO.weekOfWeekBasedYear()); int noOfWeeks = 0; diff --git a/test/java/planmysem/parser/ParserTest.java b/test/java/planmysem/parser/ParserTest.java deleted file mode 100644 index 474e87fe3..000000000 --- a/test/java/planmysem/parser/ParserTest.java +++ /dev/null @@ -1,330 +0,0 @@ -package planmysem.parser; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static planmysem.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -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; - -public class ParserTest { - - private Parser parser; - - @Before - public void setup() { - parser = new Parser(); - } - - @Test - public void emptyInput_returnsIncorrect() { - final String[] emptyInputs = { "", " ", "\n \n" }; - final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE); - 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 - */ - - /** - * Asserts that parsing the given inputs will return IncorrectCommand with the given feedback message. - */ - private void parseAndAssertIncorrectWithMessage(String feedbackMessage, String... inputs) { - for (String input : inputs) { - final IncorrectCommand result = parseAndAssertCommandType(input, IncorrectCommand.class); - assertEquals(result.feedbackToUser, feedbackMessage); - } - } - - /** - * Utility method for parsing input and asserting the class/type of the returned command object. - * - * @param input to be parsed - * @param expectedCommandClass expected class of returned command - * @return the parsed command object - */ - @SuppressWarnings("unchecked") - private T parseAndAssertCommandType(String input, Class expectedCommandClass) { - final Command result = parser.parseCommand(input); - assertTrue(result.getClass().isAssignableFrom(expectedCommandClass)); - return (T) result; - } -} diff --git a/test/java/planmysem/storage/StorageFileTest.java b/test/java/planmysem/storage/StorageFileTest.java index 3706a6aea..9ab1b32c4 100644 --- a/test/java/planmysem/storage/StorageFileTest.java +++ b/test/java/planmysem/storage/StorageFileTest.java @@ -8,10 +8,10 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; -import planmysem.data.exception.IllegalValueException; +import planmysem.common.exceptions.IllegalValueException; public class StorageFileTest { - private static final String TEST_DATA_FOLDER = "test/data/StorageFileTest"; + private static final String TEST_DATA_FOLDER = "test/model/StorageFileTest"; @Rule public ExpectedException thrown = ExpectedException.none(); @@ -33,7 +33,7 @@ public void constructor_noTxtExtension_exceptionThrown() throws Exception { @Test public void load_invalidFormat_exceptionThrown() throws Exception { - // The file contains valid xml data, but does not match the Planner class + // The file contains valid xml model, but does not match the Planner class StorageFile storage = getStorage("InvalidData.txt"); thrown.expect(StorageFile.StorageOperationException.class); storage.load(); @@ -44,7 +44,7 @@ public void load_invalidFormat_exceptionThrown() throws Exception { // AddressBook actualAB = getStorage("ValidData.txt").load(); // AddressBook expectedAB = getTestAddressBook(); // - // // ensure loaded AddressBook is properly constructed with test data + // // ensure loaded AddressBook is properly constructed with test model // // TODO: overwrite equals method in AddressBook class and replace with equals method below // assertEquals(actualAB.getAllPersons(), expectedAB.getAllPersons()); // } diff --git a/test/java/planmysem/testutil/SlotBuilder.java b/test/java/planmysem/testutil/SlotBuilder.java new file mode 100644 index 000000000..b68d246c8 --- /dev/null +++ b/test/java/planmysem/testutil/SlotBuilder.java @@ -0,0 +1,142 @@ +package planmysem.testutil; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.StringJoiner; + +import planmysem.common.Utils; +import planmysem.model.recurrence.Recurrence; +import planmysem.model.slot.Slot; + +/** + * A utility class to generate test data. + */ +public class SlotBuilder{ + + public Slot slotOne() { + String name = "CS2113T Tutorial"; + String location = "COM2 04-11"; + String description = "Topic: Sequence Diagram"; + LocalTime startTime = LocalTime.parse("08:00"); + LocalTime endTime = LocalTime.parse("09:00"); + Set tags = new HashSet<>(Arrays.asList( "CS2113T", "Tutorial")); + return new Slot(name, location, description, startTime, endTime, tags); + } + + public Recurrence recurrenceOne() { + return new Recurrence( + new HashSet<>(Arrays.asList("CS2113T", "Tutorial")), + LocalDate.of(2019, 2, 1) + ); + } + + /** + * 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 + */ + public Slot generateSlot(int seed) throws Exception { + return new Slot( + "slot " + seed, + "location " + Math.abs(seed), + "description " + Math.abs(seed), + LocalTime.parse("00:00"), + LocalTime.parse("00:00"), + new HashSet<>(Arrays.asList("tag" + Math.abs(seed), "tag" + Math.abs(seed + 1))) + ); + } + + /** Generates the correct add command based on the person given */ + String generateAddCommand(Slot s, LocalDate date, String recurrence) { + StringJoiner cmd = new StringJoiner(" "); + + cmd.add("add"); + + cmd.add("n/" + s.getName()); + cmd.add("d/" + Utils.parseDate(date)); + 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(String tag : tags){ + cmd.add("t/" + tag); + } + } + + cmd.add(recurrence); + + return cmd.toString(); + } + + /** Generates the correct add command based on the person given */ + String generateAddCommand(Slot s, int day, String recurrence) { + StringJoiner cmd = new StringJoiner(" "); + + cmd.add("add"); + + cmd.add("n/" + s.getName()); + cmd.add("d/" + day); + 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(String tag : tags){ + cmd.add("t/" + tag); + } + } + + cmd.add(recurrence); + + return cmd.toString(); + } + + /** Generates the correct delete command based on tags */ + String generateDeleteCommand(Set tags) { + StringJoiner cmd = new StringJoiner(" "); + + cmd.add("delete"); + + if (tags != null) { + for(String tag : tags){ + cmd.add("t/" + tag); + } + } + + return cmd.toString(); + } + + /** Generates the correct delete command based on the slot. */ + String generateDeleteCommand(Slot slot) { + StringJoiner cmd = new StringJoiner(" "); + + cmd.add("delete"); + + Set tags = slot.getTags(); + if (tags != null) { + for(String tag : tags){ + cmd.add("t/" + tag); + } + } + + return cmd.toString(); + } +} \ No newline at end of file