diff --git a/.gitignore b/.gitignore index cfc40b7dd..199b8f1cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# data files +/data/ + +# build/libs files +/build/libs + +# text-ui-test data files +/text-ui-test/data/ +/text-ui-test/ACTUAL.txt + # IDEA files /.idea/ /out/ @@ -11,6 +21,4 @@ src/main/resources/docs/ # MacOS custom attributes files created by Finder .DS_Store *.iml -bin/ - -/text-ui-test/ACTUAL.txt \ No newline at end of file +bin/ \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 000000000..c74188174 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/build.gradle b/build.gradle index a6db99138..44dfbb89e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '5.1.0' } -group 'seedu.duke' +group 'jikan' version '0.1.0' repositories { @@ -21,11 +21,11 @@ test { } application { - mainClassName = "seedu.duke.Duke" + mainClassName = "jikan.Jikan" } shadowJar { - archiveBaseName = "duke" + archiveBaseName = "jikan" archiveVersion = "0.0.1" archiveClassifier = null archiveAppendix = null @@ -37,4 +37,11 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true +} + +jar { + manifest { + attributes 'Main-Class': 'jikan.Jikan' + } } \ No newline at end of file diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953e..a858ff949 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,10 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Name | Github Profile | Portfolio | Portfolio (web link) +|:----:|:--------------:|:---------:|:--------------------: +Ng Siu Hian | [Github](https://github.com/siuhian) | [Portfolio](./team/siuhian.md) | [Portfolio (web link)](https://ay1920s2-cs2113-t15-1.github.io/tp/team/siuhian.html) +Ananda Lye | [Github](https://github.com/ananda-lye) | [Portfolio](./team/ananda-lye.md) | [Portfolio (web link)](https://ay1920s2-cs2113-t15-1.github.io/tp/team/ananda-lye.html) +Beatrice Chan | [Github](https://github.com/btricec) | [Portfolio](./team/btricec.md) | [Portfolio (web link)](https://ay1920s2-cs2113-t15-1.github.io/tp/team/btricec.html) +Nigelle Leo | [Github](https://github.com/nigellenl) | [Portfolio](./team/nigellenl.md) | [Portfolio (web link)](https://ay1920s2-cs2113-t15-1.github.io/tp/team/nigellenl.html) +Riccardo Di Maio | [Github](https://github.com/rdimaio) | [Portfolio](./team/rdimaio.md) | [Portfolio (web link)](https://ay1920s2-cs2113-t15-1.github.io/tp/team/rdimaio.html) + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8b5ef5cb3..3417c8e20 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,34 +1,642 @@ -# Developer Guide +# Developer Guide -## Design & Implementation +- [1. Setting Up](#1-setting-up) +- [2. Design](#2-design) + - [2.1 High-Level Architecture](#21-high-level-architecture) + - [2.2 Class Diagram](#22-class-diagram) +- [3. Implementation](#3-implementation) + - [3.1 Start Feature](#31-start-feature) + - [3.1.1 Current Implementation](#311-current-implementation) + - [3.1.2 Additional Implementation](#312-additional-implementation) + - [3.1.3 Design Considerations](#313-design-considerations) + - [3.2 Clean Feature](#32-clean-feature) + - [3.2.1 Current Implementation](#321-current-implementation) + - [3.2.2 Additional Implementation](#322-additional-implementation) + - [3.2.3 Design Considerations](#323-design-considerations) + - [3.3 Storage feature](#33-storage-feature) + - [3.4 Storage handler](#34-storage-handler) + - [3.5 Edit feature](#35-edit-feature) + - [3.5.1 Current Implementation](#351-current-implementation) + - [3.5.2 Additional Implementations](#352-additional-implementations) + - [3.5.3 Design Considerations](#353-design-considerations) + - [Current Design](#current-design) + - [Possible Design](#possible-design) + - [3.6 Continue Feature](#36-continue-feature) + - [3.6.1 Current Implementation](#361-current-implementation) + - [3.6.2 Additional Implementations](#362-additional-implementations) + - [3.6.3 Design Considerations](#363-design-considerations) + - [3.7 List feature](#37-list-feature) + - [3.7.1 Current implementation](#371-current-implementation) + - [3.8 Find & Filter Features](#38-find--filter-features) + - [Find Feature](#find-feature) + - [Filter Feature](#filter-feature) + - [3.8.1 Design Considerations](#381-design-considerations) + - [3.8.2a Current Implementation for Find](#382a-current-implementation-for-find) + - [3.8.2b Current Implementation for Filter](#382b-current-implementation-for-filter) + - [3.8.3 Sequence Diagram](#383-sequence-diagram) + - [3.8.4 Additional features](#384-additional-features) + - [3.9 Graph Feature](#39-graph-feature) + - [3.9.1 Current Implementation](#391-current-implementation) + - [3.9.2 Additional features](#392-additional-features) +- [4. Appendix](#4-appendix) + - [4.1 Product Scope](#41-product-scope) + - [4.1.1 Target user profile](#411-target-user-profile) + - [4.1.2 Value proposition](#412-value-proposition) + - [4.2 User Stories](#42-user-stories) + - [4.3 Non-Functional Requirements](#43-non-functional-requirements) + - [4.4 Glossary](#44-glossary) + - [4.5 Instructions for Manual Testing](#45-instructions-for-manual-testing) + - [4.5.1 Launch and Shutdown](#451-launch-and-shutdown) + - [4.5.2 Listing activities](#452-listing-activities) + - [4.5.3 Continuing activities](#453-continuing-activities) + - [4.5.4 Graphing activities](#454-graphing-activities) + - [4.5.5 Setting tag goals](#455-setting-tag-goals) -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +## 1. Setting Up + * Prerequisites + * JDK 11 or above + * IntelliJ IDE + * Setting up the project in your computer + * Fork this repo, and clone the fork to your computer + * Open IntelliJ (if you are not in the welcome screen, click File > Close Project to close the existing project dialog first) + * Set up the correct JDK version for Gradle + * Click Configure > Project Defaults > Project Structure + * Click New…​ and find the directory of the JDK + * Click Import Project + * Locate the build.gradle file and select it. Click OK + * Click Open as Project + * Click OK to accept the default settings. + * Verifying the setup + * Run `jikan.jikan` and try a few commands + * Run the tests and ensure they all pass. +## 2. Design +The section provides a high-level explanation of how the Jikan software is designed. -## Product Scope -### Target user profile +### 2.1 High-Level Architecture +The users interact with the Jikan software which modifies the local storage data file. -{Describe the target user profile} +Within the Jikan software, there are 5 main components: +* **Parser Component** - Parses the user inputs and calls the relevant `Command` object to execute the desired +command. +* **Ui Component** - Prints to the user the output of the desired `Commands`. +* **Commands Component** - Contains all the `Commands` to be called by the `Parser` based on user inputs. +* **Activities Component** - Maintains the non-permanent state of all `Activities` in the `Activity List` to be accessed +by the executing `Commands`. +* **Storage Component** - Interacts with and modifies the local storage file, which contains the permanent (lasting +even after the program terminates) state of all activities. It retrieves this permanent state and populates the `Activity List` at the start of each session. -### Value proposition +![image_info](./pictures/Architecture_Diagram.png) -{Describe the value proposition: what problem does it solve?} -## User Stories +### 2.2 Class Diagram +The high-level class diagram describes the structure of the components + +![image_info](./pictures/Simplified_Class_Diagram.png) + +![image_info](./pictures/Commands.png) + +All the commands inherit from the abstract `Command` class. Each command has a protected `parameters` attribute from it's Parent class `command` and an overridden method `executeCommand` which is called in `main` to execute the relevant command. + +## 3. Implementation + +This section describes some noteworthy details on how certain features are implemented. + +### 3.1 Start Feature + +#### 3.1.1 Current Implementation + +![StartCD](./pictures/startCD2.png) + +With Jikan as the main entry point for our application, + +1. Jikan will receive user input and pass it to the Parser class to get the corresponding command. +2. The Parser class will initialise and return a Command class object based on the command in user input. +3. In this case, Parser will return a StartCommand class object to Jikan. +4. Then, Jikan will call the StartCommand#executeCommand method to start an activity. + +Additionally, StartCommand also implements the following operations: + +* **StartCommand#checkActivity** Checks if the activity already exists in the activity list. +* **StartCommand#checkTime** Checks if the allocated time provided is valid. +* **StartCommand#continueActivity** Continue on an existing activity. + +**checkActivity** + +![checkActivity](./pictures/checkactivityv2.png) + +The diagram above shows how the StartCommand#checkActivity function works. This function is used to check +if the activity to be started exists in the activity list. If the activity exists in the list, that activity will be +continued and this way the user cannot start duplicate activities. + +1. When checkActivity() is called, it will make a call to the ActivityList#findActivity method. +2. Once the findActivity() method finishes execution, it will return an integer index back to checkActivity(). +3. If the index is not equals to -1, the activity to be started exists in the activity list and continueActivity() will be called. +4. Else, the activity to be started is a brand new activity and addActivityToList() will be called. + +**checkTime** + +![checkTime](./pictures/checkTimev2.png) + +The diagram above shows how the StartCommand#checkTime function works. This function is used to check the validity of +the allocated time provided by the user input. If the allocated time is valid, the activity will be added to activity +list. + +1. When checkTime() is called, it will initialise two LocalTime objects called endTime and startTime respectively. +2. startTime will be initialised to time 00:00:00 while endTime will be calculated based on the user input to the start +command (i.e `start activity name /a HH:MM:SS /t tags`) +3. Then, the method Duration.between() will be used to get a Duration object that holds the time difference between startTime +and endTime. +4. If this Duration object is non zero (i.e user gave a valid non zero allocated time), then the activity will be added to the activity list +using the addActivity() method. + +**continueActivity** + +![continueActivity](./pictures/continueActivity.jpg) + +The diagram above shows how the StartCommand#continueActivity function works. This function is used when the current activity +to be started already exists in the activity list. Thus, this function will check with the user whether to continue on that activity +and prevent duplicate activities from being started. + +1. When continueActivity() is called, it will make a call to the Scanner object to read in the next line of user input. +2. If the user input is "yes", information about the activity (activity name, tags etc.) will be forwarded to parser and the parser +will update the activity list (i.e when continue is used, activity duration is added on and needs to be updated). +3. Else, if the user input is "no", continueActivity() will notify the parser to read in the next line of user input. + +#### 3.1.2 Additional Implementation + +1. `start` command have the ability to continue an activity if the activity to be started exists in activity list as discussed above. However, the second +start command's tags and allocated time parameters will not be captured if the activity originally did have tags or allocated time. + * `start activity 1` + * `start activity 1 /a HH:MM:SS /t tags` (this command will continue activity 1 but won't add the tags and allocated time to it) + + Thus, it would be best for `start` command to address this issue and allow the second `start` command to not only continue the +activity but also edit the fields of the activity. + +2. Allows two activities to start at the same time. As a user, sometimes the activity we are doing may be linked to another activity (i.e activities like +revising CS2106 and doing CS2106 Labs are similar as doing the labs can serve like a revision too). + + Thus, it would be good if more than one activity can be started at a particular time. + +#### 3.1.3 Design Considerations + +The current design is centred around the Parser Class as all the relevant activity information (activity startTime, endTime, name, tags, +allocated time) are stored inside Parser. + +Since Parser is a public class. There are some benefits to this design. +* All the command classes have access to activity information. +* Makes the classes more lightweight as there is no need for local variables to store activity informations. +* Reduces coupling between the commands as they interact through Parser. + +However, there are some drawbacks to this design too. +* Since all the activity information are public, every class in Jikan can access/modify activity information which is +undesirable. +* This creates a lot of dependencies between Commands and Parser which makes unit testing harder to implement. +* As more commands is created to accommodate new features , Parser will be overloaded with new variables and classes. + +### 3.2 Clean Feature + +#### 3.2.1 Current Implementation + +Jikan provides a `clean` command where users can automate the cleaning of done activities (i.e activities with duration > allocation) and logging data +at application startup. + +![CleanCD](./pictures/CleanCD.png) + +With Jikan as the main entry for our application, + +1. Upon startup, Jikan will initialise a LogCleaner and StorageCleaner object. +2. Jikan will call upon LogCleaner#autoClean() and StorageCleaner#autoClean() functions. +3. These two functions will check if the Storage and Log Cleaner are enabled respectively before cleaning. +4. Thus, by the time the user can interact with Jikan (i.e send commands to Jikan), the activity list and log files would already be cleaned. +5. Using the `clean` command, users would be able to manage the cleaner's behaviour (switching it on/off, set number of done activities/logging data to clean). + +The cleanup mechanism is stored internally as a StorageCleaner and LogCleaner class. + +These two classes have access to the data files of activity list and logs respectively and thus they are able to +directly manipulate the activity list and logging data. + +A status.txt file is initialised to keep track of the status (on/off) of the two cleaners and contains information on +the number of done activities/logging data for cleaning. + +Moreover, the CleanCommand also implements the following operation: + +* **CleanCommand#setStatus** Switch on/off the two cleaners respectively. +* **CleanCommand#setValue** Set a value for the number of done activities/logging data to be cleaned. +* Note: The two cleaners are independent, setting a value/status for one of the cleaner will not affect the other cleaner. + +**setStatus** + +![setStatus](./pictures/setStatusSD.png) + +The diagram above shows how CleanCommand#setStatus function works. This function is a generalized function that is used to +switch on or off the cleaners by checking the parameters to the `clean` command. Thus, based on the return value of getStatus() and +getCleaner(), there are four possible scenarios. + +1. When setStatus() is called, the method will call its own class method getStatus() to check what is the status to set to. +2. There are two valid return values for getStatus() method which is "on" and "off". The diagram shows the former. +3. Upon receiving a valid return value from getStatus() which is "on" in the diagram, the setStatus() method will self invoke another +of its own class method getCleaner(). +4. The return result of the getCleaner() together with getStatus() will then be used to determine which cleaner are we setting and what is +the status to set to. +5. In other words, result of getCleaner() is used to determine whether are we calling StorageCleaner#setStatus or LogCleaner#setStatus while +the result of getStatus() determines the parameter to setStatus(). (e.g "on" will call setStatus("true") while "off" will call setStatus("false")). + +**setValue** + +The diagram of setValue is omitted as it is similar to setStatus diagram. This function is a generalized function that is used to +set a value for the number of done activities or the number of lines of logging data to be cleaned for the two cleaners respectively. + +1. When setValue() is called, the method will call its own class method getNumber() that will return an integer value corresponding to the number +to set to. +2. Upon receiving a valid return value (non negative), the setValue() method will self invoke another of its own class method getCleaner(). +3. The return result of the getCleaner() together with getNumber() will then be used to determine which cleaner are we setting and what is +the value to set to. +4. In other words, result of getCleaner() is used to determine whether are we calling StorageCleaner#setNumberOfActivitiesToClean or LogCleaner#setNumberOfLogsToClean +while the result of getNumber determines the parameter to these two functions. + +Note that steps 2-4 of setValue() are similar to steps 3-5 of setStatus(). + +On the other hand, the Storage/Log Cleaner class implements the following core operation of `clean` command. + +* **Cleaner#autoClean** This operation is called whenever Jikan is executed. Cleaning will only be done to the activity list/logging data if +the two cleaners are enabled respectively. + +**autoClean** + +![autoClean](./pictures/ACSD.png) + +The diagram above shows how Cleaner#autoClean function works. This function is called whenever Jikan executes Jikan#main and is used to +perform cleaning of the activity list and logging data if Storage Cleaner and Log Cleaner are enabled respectively. The number of done activities and +lines of logging data to clean is set to 5 at default if user did not specify a value for both cleaners. + +1. When main() is called, Jikan will first initialise both the StorageCleaner and LogCleaner object using StorageCleaner() and +LogCleaner(). +2. Once both objects are initialised, Jikan will first call storageAutoClean() method of the StorageCleaner class. +3. This method will invoke another method under the StorageCleaner class called checkStatus() which will return a boolean toClean variable. +4. If toClean == true, the storageAutoClean() method will proceed and clean up the activity list before returning control back to main(). +5. Else, the storageAutoClean() will not do any clean up and will immediately return control back to main(). +6. Steps 2 to 5 will then be repeated when Jikan call logAutoClean() method of the LogCleaner class. + +#### 3.2.2 Additional Implementation + +1. Currently, the data that is cleaned up by this command is sent to a recycled folder similar to how Windows recycle bin works. + + Thus, it would be good to have a feature to restore the data deleted in the event the user wishes to recover some of the activities/logs. + + On a similar note, it would also be good to have a permanent delete feature built into the recycled folder so that items that are too old (> 6 months old) will + deleted away for good. + +2. The automated cleaning does not have a lot of flexibility as the current implementation only cleans up done activities starting from the oldest. + + Thus, it would be good if the `clean` command is expanded to allow users more freedom in specifying what activities to clean. + + * `clean /n 3 /t CS2113` does cleaning on the 3 oldest done activities with CS2113 tag. + * `clean /n 5 /i 1/4/2020 3/4/2020` does cleaning on the 5 oldest done activities with dates between 1 April 2020 and 3 April 2020. + + +#### 3.2.3 Design Considerations + +The current design uses the abstract cleaner class to create dedicated cleaners (i.e Storage and Log Cleaners) to perform +cleaning for various data files (e.g activity list data file, logging data file). + +There are some benefits to this design. +* Creating an abstract class reduces the amount of repetitive code as common methods between cleaners are abstracted out. +* Abstract classes produce a more OOP solution as different cleaners will handle different parts of the data. + +However there are drawbacks to this design too. +* There are some very similar methods with key differences that cannot be abstracted out (for e.g different parameters, different printing). +* This causes the CleanCommand class to have similar and repetitive methods to handle this difference. (for e.g setStorageCleanerOn(), setLogCleanerOn() etc). + +### 3.3 Storage feature +The Storage class represents the back-end of Jikan, handling the creation, saving and loading of data. +Jikan uses a `.csv` file to store its data, formatted in the following way: + +`entry-name, start-time, end-time, duration, tags` + +All tags are saved in the same cell, separated by a white space; this design decision was taken to make sure that each entry occupies the same number of cells regardless of each entry’s number of tags. The tags are then separately parsed when the data is loaded. + +Each Storage objects contains the path to the data file (`Storage.dataFilePath`), the File object representing the data file (`Storage.dataFile`), and an activityList populated with the data from the data file (`Storage.activityList`). Storage optionally supports multiple data files at the same time, allowing implementation of features like multiple sessions and multiple user profiles. + +Storage provides the following functions: +- Constructing a Storage object via `Storage(String dataFilePath)`, which takes in the path to the desired data file (or the path where the user wants to create the data file) as a String object. +- Creating a data file via `createDataFile`. +- Writing to a data file via `writeToFile`. This function takes a single string as parameter and writes it to the data file. It is recommended to only pass single-line strings to keep the file nicely formatted. +Loading a pre-existing data file via `loadFile`. If a data file already exists for the provided data file path, the function will return `true`; if the specified data file did not previously exist, this function will call the `createDataFile` method and returns `false`. The return value is useful so that the application knows whether or not this is the first session with a specific data file or if data already exists. +- Creating an ActivityList via `createActivityList`. This function calls `loadFile()` to check whether the file already existed or not; if the data file previously existed, it will construct an ActivityList object by passing the data from the data file to it, and return this populated ActivityList object; if the data file did not previously exist, it will return an empty activityList object. + +### 3.4 Storage handler +The StorageHandler class functions as a support to the main Storage class, allowing the Jikan application to manipulate the stored data file. Its main provided functions are: +- Removing an entry from the data file via `removeLine`. This function takes in the number of the line to remove. +- Replacing an entry in the data file via `replaceLine`. This function takes in the number of the line to replace, along with the String object that needs to be written to the data file in place of the replaced line. + +### 3.5 Edit feature +The edit feature allows the user to make changes to activities that have been saved in the activity list. This is to allow the user to rectify any mistakes that may have been made during the initial recording of the activity. + +#### 3.5.1 Current Implementation +The following sequence diagram shows how the edit feature works. +The current implementation of the edit feature allows the user to edit the activity name as well as its allocated time. +The following sequence diagram shows how the edit feature works for editing the activity name. The diagram for the editing of allocated time is omitted as the sequence is relatively similar. +![image_info](./pictures/EditSequenceDiagram.png) +The current implementation of the edit feature allows the user to edit only the name and allocated time parameter of the activity. When the user wants to edit an activity using the edit command, a new EditCommand object is created. The `executeCommand()` method of the EditCommand object is called and the specified parameters are updated accordingly. + +The order of method calls to edit the activity details is as follows if the specified activity exists (meaning `index != -1`) else an exception is thrown: +1. The `updateName()` method of the ActivityList class is called, with the user-specified parameters of the activity index and new activity name +2. The `get()` method is self-invoked by the ActivityList class to obtain the activity at the given index +3. The `setName()` method of the Activity class is called to edit the activity name to the user-specified name +4. The activity is updated with its new name in the activityList. +5. The `fieldChangeUpdateFile()` method of the StorageHandler class is called to update the data file with the new activity name. + + +#### 3.5.2 Additional Implementations +The current implementation of the edit feature only allows the user to edit the activity name and allocated time. Hence, additional implementations of the edit feature could allow the user to edit other parameters of the activity such as the tags and the start and end dates. + +This will require the implementation of more update methods in the ActivityList class to allow for the changes to be updated in the activityList after it has been edited. Additionally, there may be more updates required if the tags were to be edited due to the tag goals feature. + +The flowchart below shows the flow of activities if the feature of editing tags were to be implemented. +![image_info](./pictures/EditTagFlowChart.png) + +#### 3.5.3 Design Considerations +##### Current Design +The user is able to edit only the name and allocated time of the activity, which are user input data. + +**Pros:** +* The user is able to correct any mistake made during the recording of the activity. +* The user is able to adjust their allocated time for the activity based on their needs. +* Ensures that the record of activities is accurate and consistent in order for more efficient analysis of the time spent. + +**Cons:** +* The user is only able to edit 2 parameters of the activity, which may be restrictive for them. + +##### Possible Design +The user is able to edit any parameters of the activity, including tags, start and end date/time. + +**Pros:** +* The user has more flexibility in modifying the record of activities based on their needs. + +**Cons:** +* By allowing the user to edit the date and time, there may be potential inaccuracies in the record of activities, defeating the purpose of the time tracking program. +* By allowing the user to edit the tags, the tag goals command may become more complicated due to the need to keep track of the presence of the tags. + +### 3.6 Continue Feature +The continue feature allows the user to continue a previously ended activity. + +#### 3.6.1 Current Implementation +![Continue command sequence diagram](./pictures/continue.png) + +**Continuing an activity:** +* When the user enters the command to continue an activity, a *ContinueCommand* object is created in *Parser*. The method `executeCommand()` of the *ContinueCommand* object is then called. +* `executeCommand` checks if the given activity name exists in the activityList by calling `findActivity()` (if it doesn’t an exception is thrown, omitted in the sequence diagram above) +* It then gets the `name` and `tags` of the activity to be continued and saves it to a public static variable of *Parser* object. +* It also gets the current time and saves it to a public static variable of *Parser* object. + + ![End command sequence diagram](./pictures/end.png) + + **Ending a continued activity:** +* When the user wants to end the continued activity, an *EndCommand* object is created in *Parser.* The method `executeCommand()` of the *ContinueCommand* object is then called and it in turn executes the `saveActivity()` method of the *ActivityList* class. +* `saveActivity()` gets the current time and saves it to a public static variable of *Parser* object. +* Then the elapsed time is calculated using the `between()` method of *Duration* class. +* The elapsed time is added with the previous duration of the activity to get the `newDuration` using the `plus()` method of Duration class. +* `updateDuration()` method is called to update the `duration` attribute of the continued activity in the `activityList` as well as the `data.csv` file. + +#### 3.6.2 Additional Implementations +As users can only have activities with unique names, when a user wants to start an activity which already exists in the activityList, they will be given the option to continue the stated activity. +![decision flowchart](./pictures/continue_flowchart.PNG) + +#### 3.6.3 Design Considerations + +**Execution:** + * Continue by activity name (current implementation) + * **Cons:** Activity names have to be unique. + * **Pros:** More versatile, resistant to changes in the activity list + * Continue by activity index + * **Cons:** need to add an additional index field to the Activity class, + index is not fixed, changes when an activity is deleted + * **Pros:** Can reuse activity names. + +Although the current implementation of the continue feature disallows users to have multiple activities with the same name, we felt that the versatility of this choice outweighed the cons. Firstly because if the activityList got too big, it would be hard for the user to get the index of the task they wanted to continue. Also, the index would constantly be changing when changes are made to the list. + +### 3.7 List feature +This feature is used to list activities within a range specified by the user. +If no parameter is passed to the `list` command, then all the stored activities will be displayed. +By passing a single date, the command returns all activities within that date. +By passing two dates, the command returns all activities that took place within the two dates. +(for an activity to be included in the range, both its start and end time must be within the specified time range). +The user can also provide a verbal command, such as `day`, `week`, or `month`, which +will return all the activities for that day, week or month respectively. +Additionally, the user can specify a specific week of month by including a date +(e.g. `list month 2020-03-01` returns all the activities in March 2020.) + +#### 3.7.1 Current implementation +* List all activities: `list` + * List today's activities: `list day` or `list daily` + * List this week's activities: `list week` or `list weekly` + * List a specific week's activities by day: `list week DATE` or `list weekly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List this month's activities: `list month` or `list monthly` + * List a specific month's activities by day: `list month DATE` or `list monthly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List a specific day's activities: `list DATE`, where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List activities within a time frame: `list DATE1 DATE2`, where both `DATE1` and `DATE2` are + in either `yyyy-MM-dd` or `dd/MM/yyyy` format + +### 3.8 Find & Filter Features + +#### Find Feature +This command accepts keyword(s) and searches either the entire activity list or the last shown list for activities with +names containing each keyword. + +#### Filter Feature +This feature accepts space-separated keyword(s) to search either the entire list or the last shown list +for activities with tags matching each keyword. The keywords should be an exact-match with the tag names. + + +#### 3.8.1 Design Considerations +As the `find` and `filter` commands are important for the user to analyse and eventually graph time-spent on each +activity. The user should be allowed to query for all useful combinations of activities in the activity list. +This entails: +* The user should be allowed to match for multiple keywords at once. +* The user should be allowed to exclude certain activities by limiting his search to a previously shown list as + opposed to the entire activity list. + (chaining `list`, `find`, and `filter` commands). + + +#### 3.8.2a Current Implementation for Find +* This feature is called by the user when the `find` command is entered into the command line. +The string following the command are the parameters: + * `-s` flag indicates that the searching should be limited to activities previously shown to the user. + * The remaining parameters are a string of keywords separated by ` / `. +* The Parser will create a FindCommand object. +* The FindCommand will invoke its own `executeCommand()` method. + * The Parser's `lastShownList` will be cleared. + * Then it will loop through `activityList` to find activities with names that contain the keyword. + * If one is found, it will be added to `lastShownList`. + * `printResults()` of the Ui will be called: + * If `lastShownList` is not empty, it will print the matching activities. + * Else, it will respond to the user that there are no tasks which match the given keyword. + + +#### 3.8.2b Current Implementation for Filter +* This feature is called by the user when the `filter` command is entered into the command line. The space separated strings following the command are the keywords to match activity tags with. +* The Parser will create a FilterCommand object. +* The FindCommand will invoke its own `executeCommand()` method. +* The Parser's `lastShownList` will be cleared. +* For each keyword: + * Then it will loop through `activityList` to find activities with tags that contain the keyword. + * If one is found, it will be added to `lastShownList`. + * `printResults()` method of the Ui will be called + * If `lastShownList` is not empty, it will print the matching activities. + * Else, it will respond to the user that there are no tasks which match the given keyword. + +#### 3.8.3 Sequence Diagram +The following illustrates the execution sequence of a general use case. + +Note: Due to the sequence similarities between `find` and + `filter`, the sequence diagram for `filter` is omitted. + +![image_info](./pictures/Find_Sequence_Diagram.png) + +![image_info](./pictures/Find_Reference_Frame.PNG) + + +#### 3.8.4 Additional features +`find` and `filter` command supports the limiting of searches to activities in the last shown list. This +is done in 2 ways: +* The `-s` flag following the command (eg. `find -s keyword`) +* The `;` delimiter for a combination of `find` and `filter` in a single input (eg. `find KEYWORD ; filter TAGNAME`) + + ![Chaining_Activity_Diagram](./pictures/Chaining_Activity_Diagram.png) + +### 3.9 Graph Feature +This feature gives the user a visual representation of their activity duration and activity goals. +Graph can be used along with `list`, `find` and `filter` to sieve out the data to be graphed. + + +#### 3.9.1 Current Implementation +![graph seq diagram](./pictures/graph_seqDiag.png) +* This feature is called by the user when the `graph` command is entered into the command line. The user will then have to specify what he would like to graph (goals progress bar / tag duration / activity duration). +* The Parser will create a GraphCommand object. +* The GraphCommand will invoke its own `executeCommand()` method. + +**Graph allocations** +This displays the progress bar for the duration with respect to allocated time of activities in the `lastShownList`. +* If the user indicated `targets`, Ui calss will be called to execute graphTargets. + +**Graph tags** +This displays a bar graph of the cumulative duration of the tags for each activity in the `lastShownList`. +E.g. if 3 activities in the `lastshownlist` are tagged `CS2113`, the durations of these 3 activities are added up and associated with the tag `CS2113` in the graph. +* If the user indicated `tags`, `GraphCommand` will call it's own `graphTags` method. +* A HashMap (`tags`) of tags to duration is created. +* `graphTags` iterates through every activity in `lastshownlist` and in each loop, `extractTags` is called. +* `extractTags` loops through the tags of that activity. Tag is added to the `tags` if it is not found. Else, the duration of the activity is added to the corresponding tag in `tags`. +* `tags` and `interval` (how many minutes each point in the graph represents) is passed to the method printTagGraphs in Ui to print the graph. + +**Graph activities** +This displays a bar graph of the durations of each activity in the `lastShownList`. +* If the user indicated `activities`, `GraphCommand` will call it's own `graphDuration` method. +* `graphDuration` calls `printActivityGraph` of the Ui class and passes the `interval` parameter, which is how many minutes each point in the graph represents. + + +#### 3.9.2 Additional features +As graph gets it's data based on the `lastShownList`, users can pair the `graph` command with `find`, `filter`, and `list` to sieve out the activities to be graphed. + + +## 4. Appendix +### 4.1 Product Scope +#### 4.1.1 Target user profile + +* University students with poor time management skills who are struggling to allocate time efficiently for + the numerous deadlines/tasks. +* Users who are reasonably comfortable using CLI apps. + +#### 4.1.2 Value proposition + +Allow users to record their daily activities and track their time usage in a user-friendly manner. + +### 4.2 User Stories |Version| As a ... | I want to ... | So that I can ...| |--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +|v1.0|user|start a new activity|track the time spent on the activity| +|v1.0|user|end an activity|track the total time i spent on the activity| +|v1.0|user|abort an activity|record a different activity| +|v1.0|user|store the completed activities in a list|analyse how I spent my time| +|v2.0|user|delete an activity|remove activities that I do not want to track| +|v2.0|user|search by activity name|view similar activities| +|v2.0|user|filter activities by tags|view activities of the same category| +|v2.0|user|view the activities by date|see how much time I have spent on different activities| +|v2.0|user|continue my activities at another time|do other things between activities| +|v2.0|user|edit past activities|keep a more accurate record of activities| +|v2.0|user|automate the deletion of old activities|keep a more concise log of activities| + + +### 4.3 Non-Functional Requirements +* The program should be usable by a novice who has never used a time management application. +* The program should work on most mainstream OSes. +* The program should be portable to other systems. + +### 4.4 Glossary + +* *Mainstream OSes:* Windows, MacOS, Linux + +### 4.5 Instructions for Manual Testing + +#### 4.5.1 Launch and Shutdown + 1. Download the jar file, tag.csv file and data.csv file. + 2. Copy the files into an empty folder. + 3. Create a folder named `data` and put the data.csv file into this folder. + 4. Within the `data` folder, create a `tag` folder and put the tag.csv file into this folder. + 4. Ensure the folder `data` and `jikan.jar` are in the same folder. + 5. Open command prompt and navigate to the folder. Run the jar file using `java -jar jikan.jar` + + It is important to include the data.csv file to have data for testing! + +#### 4.5.2 Listing activities + Test case: `list month april` + + Expected: A list of activities completed in the month of April should be shown. + + Test case: `list 25/03/2020` + + Expected: A list of activities completed on 25th March 2020 should be shown. + +#### 4.5.3 Continuing activities + Test case: `continue lab 4 ex2` + + Expected: Message "lab 4 ex2 was continued" will be displayed. + + Test case: `start lab 4 ex2` + + Expected: Option to continue will be given. If 'yes' is typed, activity will be continued. + + +#### 4.5.4 Graphing activities +Test case: (to be done in succession) + +`find tutorial` then `graph 10` + +Expected: List of activities that contain 'tutorial' will be shown. +Then a chart of the duration of these activities will be shown. + +Test case: (to be done in succession) + +`list week` then `graph tags` + +Expected: List of activities completed this week will be shown. +Then a chart of the duration of the tags of these activities will be shown. -## Non-Functional Requirements +#### 4.5.5 Setting tag goals +Test case: `goal core /g 24:00:00` -{Give non-functional requirements} +Expected: Message "The goal for core has been added!" will be displayed. -## Glossary +Test case: `goal core /g 22:00:00` -* *glossary item* - Definition +Expected: Message "The goal for this tag already exists, do you want to update the goal?" will be displayed. +* If 'yes' is entered, the goal will be updated and the message "The goal for core was updated" will be displayed. +* If 'no' is entered, the message "Okay then, what else can I do for you?" will be displayed and the program will wait for user's next command. -## Instructions for Manual Testing +Test case: `goal` -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +Expected: List of tags and their associated goals will be displayed. + diff --git a/docs/README.md b/docs/README.md index 5fda977aa..e873beccf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ -# Duke +# Jikan -{Give product intro here} +**Jikan** is a CLI time-tracker built in Java that aims to help manage tasks and projects. Users can set tags and goals for their entries, ultimately being able to keep track of what's left to do and maintain an overview of how time was spent. Useful links: -* [User Guide](UserGuide.md) -* [Developer Guide](UserGuide.md) -* [About Us](AboutUs.md) +* [User Guide](UserGuide.md) | [Web version](https://ay1920s2-cs2113-t15-1.github.io/tp/UserGuide.html) +* [Developer Guide](DeveloperGuide.md) | [Web version](https://ay1920s2-cs2113-t15-1.github.io/tp/DeveloperGuide.html) +* [About Us](AboutUs.md) | [Web version](https://ay1920s2-cs2113-t15-1.github.io/tp/AboutUs.html) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 81da234ff..49863178c 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,422 @@ -# User Guide +# User Guide -## Introduction +- [1. Target user profile](#1-target-user-profile) +- [2. Introduction](#2-introduction) +- [3. Quick Start](#3-quick-start) +- [4. Features and usage](#4-features-and-usage) +- [5. Basic Commands](#5-basic-commands) + - [5.1 Starting an activity: `start`](#51-starting-an-activity-start) + - [5.2 Continuing an activity: `continue`](#52-continuing-an-activity-continue) + - [5.3 Ending an activity: `end`](#53-ending-an-activity-end) + - [5.4 Aborting an activity: `abort`](#54-aborting-an-activity-abort) + - [5.5 Delete an activity: `delete`](#55-delete-an-activity-delete) + - [5.6 Listing activities: `list`](#56-listing-activities-list) + - [5.7 Editing an activity: `edit`](#57-editing-an-activity-edit) +- [6. Finding and Filtering](#6-finding-and-filtering) + - [6.1 Finding Activities by Name: `find`](#61-finding-activities-by-name-find) + - [6.2 Filtering Activities by Tags: `filter`](#62-filtering-activities-by-tags-filter) + - [6.3 Chaining Finds & Filters: `-s`](#63-chaining-finds--filters--s) + - [6.3.1 Single Input Chaining: `;`](#631-single-input-chaining) +- [7. Graphs](#7-graphs) + - [7.1 Activity time graph: `graph activities`](#71-activity-time-graph-graph-activities) + - [7.2 Tags time graph: `graph tags`](#72-tags-time-graph-graph-tags) + - [7.3 Activity allocation graph: `graph allocations`](#73-activity-allocation-graph-graph-allocations) + - [7.4 Chaining `list`, `find` and `filter` with `graph` command:](#74-chaining-list-find-and-filter-with-graph-command) +- [8. Tag Goals](#8-tag-goals) + - [8.1 Set goal: `goal TAG_NAME /g DURATION`](#81-set-goal-goal-tagname-g-duration) + - [8.2 Delete goal: `goal TAG_NAME /d`](#82-delete-goal-goal-tagname-d) + - [8.3 View goals: `goal`](#83-view-goals-goal) +- [9. Automated Cleaning](#9-automated-cleaning) + - [9.1 Activate cleaning: `clean on`](#91-activate-cleaning-clean-on) + - [9.2 Deactivate cleaning: `clean off`](#92-deactivate-cleaning-clean-off) + - [9.3 Set the number of activities to clean: `clean /n`](#93-set-the-number-of-activities-to-clean-clean-n) + - [9.4 Automated Cleaning for Logs:](#94-automated-cleaning-for-logs) + - [9.5 Activate log cleaning: `clean log on`](#95-activate-log-cleaning-clean-log-on) + - [9.6 Deactivate log cleaning: `clean log off`](#96-deactivate-log-cleaning-clean-log-off) + - [9.7 Set the number of logs to clean: `clean log /n`](#97-set-the-number-of-logs-to-clean-clean-log-n) +- [10. Command Guide](#10-command-guide) + +## 1. Target user profile -{Give a product intro} +University students with poor time management skills who are struggling to allocate time efficiently for +the numerous deadlines/tasks. -## Quick Start +## 2. Introduction -{Give steps to get started quickly} +Jikan is a CLI time management tool that allows you to track the amount of time that you spend on different activities so as to ensure +that time is allocated more efficiently to the different activities (i.e spending too much/too little time on an activity) . +This user guide will show you how to use the program effectively. +## 3. Quick Start 1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the jar and tag.csv file of the latest version of `Jikan` from [here](https://github.com/AY1920S2-CS2113-T15-1/tp/releases). +3. Create an empty folder and put the Jikan.jar file inside. +4. Within the folder, create a separate folder named `tag` and place the `tag.csv` file inside the tag folder. +5. Open command prompt and navigate to the folder. Run the jar file using `java -jar jikan.jar`. -## Features +## 4. Features and usage +Jikan lets you record how much time you spend on various activities so that you can easily see what took up the most time today / this week / this month. +(In the example below, we use the example of a student tracking his/her schoolwork, but Jikan can be used for more than just that!) -{Give detailed description of each feature} +To start, record your first activity using the `start ACTIVITY_NAME` command. -### Adding a to-do: `todo` -Adds a to-do item to the list of to-dos. +Add some tags to your activities to group similar activities together using `/t`. Tags help you group activities of the same type together, +in this example, we use the tags feature to label activities according to their module code. +**(Note that each activity can only store two tags at maximum.)** -Format: `todo n/TODO_NAME d/DEADLINE` +Add allocated time to your activities using `/a`. This allows users to set aside how much time they would like to spend on an activity and +keep track on whether they are spending too much or too little time for that particular activity. -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +When you are done with the activity, or want to move onto something else, tell Jikan to `end` and the Activity time will be recorded and saved to your list. -Example of usage: +You can view all your activities using the `list` command. Or view all your activities over a period of time by using `list` with extra parameters. For example `list week` will return a list of all activities this current week, as shown below. -`todo n/Write the rest of the User Guide d/next week` +![list week](./pictures/list_week.PNG) -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +The list still looks very cluttered, we can reduce it further! Want to find everything you did for CS2113 this week? Filter out the activities you want to see using the `find` or `filter` command. This is our list after filtering out all our activities tagged as `2113`. (the -s flag tells Jikan to search our last shown list, i.e. the list of activities this week in this case) -## FAQ +![Continue command sequence diagram](./pictures/filter.PNG) -**Q**: How do I transfer my data to another computer? +To easily see what took up the most of your time out of all the 2113 activities, use the `graph` command to view a chart of your activities. -**A**: Well, write the User Guide in active voice anyway. +![Continue command sequence diagram](./pictures/graph.png) -## Command Summary +Curious about what module took up the most time this week? We can use the `graph tags` command on our weekly activity list to find out. -{Give a 'cheat sheet' of commands here} +![Continue command sequence diagram](./pictures/graphtags.PNG) -* Add to-do `todo n/TODO_NAME d/DEADLINE` +Evidently, it was CS2105. + +Not done with an activity and want to continue on it? Use the `continue` command to continue recording time for a previously started activity. + +Finally, when you're done and want to close the app, simply say `bye` and Jikan will exit. + +This is just a quick overview of what Jikan can do for you. For more details on each individual command, read the command guide below. + +## 5. Basic Commands +### 5.1 Starting an activity: `start` +**Usage:** Starts recording the time for a new activity. + +**Format:** `start ACTIVITY_NAME /a ALLOCATED_TIME /t TAGS` + * `ACTIVITY_NAME` can contains spaces and must be less than 25 characters. +* `ACTIVITY_NAME` must also be unique (should the user start an already existing activity, the option to `continue` will be given). +* `ALLOCATED_TIME` should be of the format [HH:MM:SS] and cannot exceed 23:59:59. +* `TAGS` must be single spaced separated and a maximum of 2 tags can be stored. +* `ALLOCATED_TIME` and `TAGS` are optional. + +**Example:** +`start assignment /a 01:30:00 /t CS1010` +`start GER1000 quiz /t GER GEmod` +`start revision` + +**Discouraged Names:** + * The following strings are used as parameters for other commands, and hence should be avoided in the `ACTIVITY_NAME` and `TAGS` as it may interfere with Jikan running smoothly: + * `/`,`;`, `/a`, `/t`,`/a`,`-s`,`/en`,`/ea`, `/d`, `/n`, `/g` + +### 5.2 Continuing an activity: `continue` +**Usage:** Continues recording the time of an activity that you have previously started. + +**Format:** `continue ACTIVITY_NAME` +* `ACTIVITY_NAME` must be an existing activity in the activity list. + +**Example:** +`continue revision` + +### 5.3 Ending an activity: `end` +**Usage:** Stops recording the time for an ongoing activity and stores it into the activity list. + +**Format:** `end` +* An activity must be started or continued before it can be ended. + +### 5.4 Aborting an activity: `abort` +**Usage:** Aborts the current activity and does not save it to the activity list. + +**Format:** `abort` +* An activity must be started or continued before it can be ended. + +### 5.5 Delete an activity: `delete` +**Usage:** Deletes an activity in the activity list. + +**Format:** `delete ACTIVITY_NAME` + +### 5.6 Listing activities: `list` +**Usage:** Displays a list of the completed activities. + +**Format:** `list TIME_PERIOD` +* If no `TIME_PERIOD` is given, all activities will be listed. +* `TIME_PERIOD` can be `day` or `week` +* To list activities in a specific month of the current year, use `list month MONTH_NAME` where `MONTH_NAME` must be spelled out in full (i.e. January and not Jan). +* Otherwise, `TIME_PERIOD` should be of the format [dd/MM/yyyy] or [yyyy-MM-dd] +* `TIME_PERIOD` can either be a specific date or over a range. + +**Example:** +* `list` lists all activities. +* `list month april` lists all activities in April. +* `list week` or `list weekly` lists all activities in the current week. +* `list week 01/01/2020` lists all activities in the week of 01/01/2020. +* `list day` or `list daily` lists all activities in the current day. +* `list yesterday` lists all activities completed the day before. +* `list 01/01/2020` or `list 2020-01-01` lists all activities completed on 1 Jan 2020. +* `list 01/01/2020 20/02/2020` lists all activities than fall within 1 Jan 2020 and 20 Feb 2020. + +### 5.7 Editing an activity: `edit` +**Usage:** Edits the name or allocated time of an activity in the activity list. + +**Format** +* `edit ACTIVITY_NAME /en NEW_NAME` +* `edit ACTIVITY_NAME /ea NEW_ALLOCATED_TIME` + * `NEW_ALLOCATED_TIME` should be in the format [HH:MM:SS] + +**Example:** +`edit CS1010 assignment /en CS1010 assignment 2` Activity name is edited to `CS1010 assignment 2` +`edit CS1010 assignment /ea 10:00:00` Allocated time for activity is edited to `10:00:00` + +## 6. Finding and Filtering +By using `find` and `filter` commands, the user can reduce clutter and zoom-in to specific activities containing certain keywords or tags. The sub-query flag `-s` allows chaining any combination of `find` and `filter` commands to further reduce clutter. These features are particularly useful when the visualisation of time spent with minimal clutter is required. + +### 6.1 Finding Activities by Name: `find` +**Usage:** Users can request for a sub-list of activities that has names which contain any of the given keywords. If there are more than one keyword, each keyword should be separated with ` / `. + +**Format:** +* `find KEYWORD` +* `find KEYWORD1 / KEYWORD2 / KEYWORD3` + +### 6.2 Filtering Activities by Tags: `filter` +**Usage:** Users can request for a sub-list of activities that has specific tags. Each tag should be space separated. + +**Format:** +* `filter TAGNAME` +* `filter TAGNAME1 TAGNAME2` + +### 6.3 Chaining Finds & Filters: `-s` +**Usage:** Users can provide the `find` and `filter` command on the last shown list (also compatible after a `list` +command) by providing the `-s` flag after each `find` or `filter` command. + +**Format:** +* `find -s KEYWORD` +* `filter -s TAGNAME` +* `filter -s TAGNAME1 TAGNAME2` +* `find -s KEYWORD1 / KEYWORD2 / KEYWORD3` + +**Example:** +If we want to find all CS2106 tutorials, we can first use `filter 2106` to filter out all activities tagged `2106`, then use the find command with the flag, `find -s Tutorial` to get a list of all 2106 Tutorials. + +![chain graph activities](./pictures/filter-find_chain.PNG) + +#### 6.3.1 Single Input Chaining: `;` +**Usage:** Users can achieve the same outcome as multiple `-s` chaining with a single input. This is done by separating +`find` and `filter` commands with ` ; `. + +**Examples:** +* `filter TAGNAME ; find KEYWORD1 ; find KEYWORD2` +* `filter -s TAGNAME ; find KEYWORD1 ; find KEYWORD2` + +Note: `-s` is only relevant in the first command of the entire input string, as subsequent commands are automatically chained. + +## 7. Graphs +By using the following commands, users can get a visual representation of the time spent on each activity and their current progress. +The 3 types of graphs are : + * *Activity time graph* - Total time spent on each activity: `graph activities SCALE` + * *Tags time graph* - Total time spent on each tag: `graph tags SCALE` + * *Activity allocation graph* - Progress of each activity in relation to its allocated time: `graph allocations` + +Tip: Use `find`, `filter` and `list` commands to reduce clutter before graphing as the graphs are based on the last shown list of activities. + +### 7.1 Activity time graph: `graph activities` +**Usage:** View a comparison of the absolute time spent on each activity in the last shown list. +The parameter `SCALE` refers to the number of minutes represented by each point on the graph. + +Note: As the units of `SCALE` is minutes, if your activity is less than a minute, graph function will not show anything. + +**Format:** `graph activities SCALE` + +**Example:** +`graph activities 10` + +### 7.2 Tags time graph: `graph tags` +**Usage:** View a comparison of the absolute time spent on each tag in the last shown list. + +![graph tags](./pictures/graphtags_example.PNG) + +For example, if we `graph tags 1` for the activity list above, we will get the following graph: + +![graph tags](./pictures/graphtags_example2.png) + +`activity 1` and `activity 2` are both tagged `tag1` and have a duration of 5 mins. +`activity 3` and `activity 4` are both tagged `tag2` and have a duration of 2 and 3 mins respectively. +Adding up the durations for each tag, we get 10 mins for `tag1` and 5 mins for `tag2`. As we chose the graph to have a scale of 1 min, there are (10 asterisk representing 1 min each) for `tag1` and 5 asterisks for `tag2` in the graph. + +As tags can be used to group activities of a similar nature together (i.e. same module), this feature can be used to easily see what type of activity took up the most time. + +**Format:** `graph tags SCALE` + +**Example:** +`graph tags 1` + +### 7.3 Activity allocation graph: `graph allocations` +**Usage:** View the progress of activities to see how much time was spent on the activity relative to the allocated time. + +Note: Only activities with an `ALLOCATED_TIME` will be shown. + +![graph_allocations](./pictures/listforgraphallocations.png) + +For example, if we `graph allocations` for the activity list above, we will get the following graph: + +![graph_allocations](./pictures/graphallocations.png) + +`activity 3` and `activity 5` does not have an allocated time, thus they do not appear in the graph. +The percentage shown in the graph represents the activity's progress relative to their allocated time. (`activity 4` have a duration of 2 seconds while its allocated time was 5 seconds, 2/5 * 100% = 40%. Thus the progress of `activity 4` is 40% +as shown in the graph) + +**Format:** `graph allocations` + +### 7.4 Chaining `list`, `find` and `filter` with `graph` command: +Using `list`, `find` and `filter` commands you can sieve out the information you wish to be graphed. + +**Graph Activities Example:** + +![chain graph activities](./pictures/filter-graph_chain.PNG) + +`filter 2113` gives all activities tagged `2113`, then we can use `graph activities 5` to view a graph of the duration for each activity. + +**Graph Tags Example:** + +![chain graph tags](./pictures/list-graphtags_chain.PNG) + +`list 25/03/2020` gives all activities completed on 25th March 2020, then we can use `graph tags 5` to view the graph of the tags. Each asterisk represents 5 minutes, as indicated by the `SCALE` parameter of the graph command. + +**Graph Allocations Example:** + +![chain graph tags](./pictures/find-allocations_chain.PNG) + +`find Lab` gives us all `Lab` activities, then we can use `graph allocations` to view the progress bar of each of the activities to see how much time was spent on the activity relative to the time that was allocated. + +## 8. Tag Goals + +By using the `goal` command, users can set specific goals for how long they would like to spend on activities under a certain tags as well as view the amount of time they have spent in total for those activities as compared to their goal. + +### 8.1 Set goal: `goal TAG_NAME /g DURATION` +**Usage:** Sets a duration goal for a tag + +**Format:** `goal TAG_NAME /g DURATION` +* The duration should be in the format [HH:MM:SS] + +**Example:** `goal core /g 24:00:00` a goal of `24:00:00` is added for the tag `core` + +### 8.2 Delete goal: `goal TAG_NAME /d` +**Usage:** Deletes the duration goal set for the tag. + +**Format:** `goal TAG_NAME /d` + +### 8.3 View goals: `goal` +**Usage:** Displays the tags with their goals, actual time spent on activities with these tags and the difference between the 2 timings. + +**Format:** `goal` + +![goal display](./pictures/GoalDisplay.PNG) + +## 9. Automated Cleaning + +Jikan provides a `clean` command where users can automate the cleaning of activities from the activity list at application startup. + +### 9.1 Activate cleaning: `clean on` +**Usage:** Switch on automated cleaning. + +**Format:** `clean on` + +### 9.2 Deactivate cleaning: `clean off` +**Usage:** Switch off automated cleaning. + +**Format:** `clean off` + +### 9.3 Set the number of activities to clean: `clean /n` +**Usage:** Set a number of activities to clean. + +**Format:** `clean /n NUMBER` + +Note: Once cleaning is switched on, the automated cleaning persists (i.e cleaning will be done at every application startup) until it is switched off. + +**Example:** + +![CleanExample](./pictures/cleanlist.png) + +Taking a look at this cluttered activity list, we can see that there are some activities which are done (i.e duration > allocation). +Thus, to reduce clutter, we would like to get rid of these done activities. + +However, since the list is so huge, it would be troublesome to use the delete function as users will have to manually navigate through +the list to identify the done activities and delete them. + +This is where the `clean` command would be useful. See that activity 6, 7 and 10 are done. + +![CleanExample](./pictures/cleanEg.png) + +By using the `clean` command. Users can choose how much of these done activities to clean, for the example here, the number is set to 2. + +![CleanExample](./pictures/afterClean.png) + +Upon the next startup, the automated cleaning will do its work and clean the 2 oldest done activities (i.e oldest here is based on date). + +Note that since the user specified to clean only 2 activities, only activity 6 and 7 are cleaned and activity 10 remains in the activity list. + +### 9.4 Automated Cleaning for Logs: + +Jikan also provides cleaning for log file which are used to record important information during program execution. This feature will be useful +to users who are running this application on systems with limited hardware (small storage space). + +### 9.5 Activate log cleaning: `clean log on` +**Usage:** Switch on automated cleaning. + +**Format:** `clean log on` + +### 9.6 Deactivate log cleaning: `clean log off` +**Usage:** Switch off automated cleaning. + +**Format:** `clean log off` + +### 9.7 Set the number of logs to clean: `clean log /n` +**Usage:** Set number of lines of logs to clean. + +**Format:** `clean log /n NUMBER` + +## 10. Command Guide + +* Start an activity: `start ACTVITY_NAME` + * optional: `start ACTIVITY_NAME /a ALLOCATED_TIME /t TAGS` +* Abort an activity: `abort` +* Stop an activity: `end` +* Continue an activity: `continue ACTIVITY_NAME` +* List all activities: `list` + * List today's activities: `list day` or `list daily` or `list today` + * List yesterday's activities: `list yesterday` + * List this week's activities: `list week` or `list weekly` + * List a specific week's activities by day: `list week DATE` or `list weekly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List this month's activities: `list month` or `list monthly` + * List a specific month's activities by day: `list month MONTH_NAME` where `MONTH_NAME` must be spelled out in full + * List a specific day's activities: `list DATE`, where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List activities within a time frame: `list DATE1 DATE2`, where both `DATE1` and `DATE2` are + in either `yyyy-MM-dd` or `dd/MM/yyyy` format +* Edit an activity: `edit ACTIVITY_NAME [flag]` + * Edit activity name: `edit ACTIVITY_NAME /en NEW_NAME` + * Edit activity allocated time: `edit ACTIVITY_NAME /ea NEW_ALLOCATED_TIME` +* Delete an activity: `delete ACTIVITY_NAME` +* Find activities with keyword: `find KEYWORD` + * optional: `find KEYWORD1 / KEYWORD2` for multiple keywords + * optional: `find -s KEYWORD` for more specific find +* Filter activities by tags: `filter TAG_NAME` + * optional: `filter TAG1 TAG2` for multiple tags + * optional: `filter -s TAG_NAME` for more specific filter +* Set a goal for tags: `goal TAG_NAME /g DURATION` +* Delete a goal for tags: `goal TAG_NAME /d` +* View goals for tags: `goal` +* Display graph by tags: `graph tags INTERVAL` +* Display graph by duration: `graph activities INTERVAL` +* Display graph by targets: `graph targets` +* Clean data files: `clean [command]` + * Activate auto data cleaner: `clean on` + * Activate auto log cleaner: `clean log on` + * Deactivate auto data cleaner: `clean off` + * Deactivate auto log cleaner: `clean log off` + * Specify number of files to clean for data: `clean /n NUMBER` + * Specify number of files to clean for logs: `clean log /n NUMBER` +* Terminate the program: `bye` diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..c4192631f --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/docs/pictures/ACSD.png b/docs/pictures/ACSD.png new file mode 100644 index 000000000..f36ed5024 Binary files /dev/null and b/docs/pictures/ACSD.png differ diff --git a/docs/pictures/Architecture_Diagram.png b/docs/pictures/Architecture_Diagram.png new file mode 100644 index 000000000..25aa43105 Binary files /dev/null and b/docs/pictures/Architecture_Diagram.png differ diff --git a/docs/pictures/Chaining_Activity_Diagram.png b/docs/pictures/Chaining_Activity_Diagram.png new file mode 100644 index 000000000..6605273c3 Binary files /dev/null and b/docs/pictures/Chaining_Activity_Diagram.png differ diff --git a/docs/pictures/ClassDiagram.png b/docs/pictures/ClassDiagram.png new file mode 100644 index 000000000..ff6d64917 Binary files /dev/null and b/docs/pictures/ClassDiagram.png differ diff --git a/docs/pictures/CleanCD.png b/docs/pictures/CleanCD.png new file mode 100644 index 000000000..790125ead Binary files /dev/null and b/docs/pictures/CleanCD.png differ diff --git a/docs/pictures/Commands.png b/docs/pictures/Commands.png new file mode 100644 index 000000000..59f5a2a43 Binary files /dev/null and b/docs/pictures/Commands.png differ diff --git a/docs/pictures/EditSequenceDiagram.png b/docs/pictures/EditSequenceDiagram.png new file mode 100644 index 000000000..88e6722a2 Binary files /dev/null and b/docs/pictures/EditSequenceDiagram.png differ diff --git a/docs/pictures/EditTagFlowChart.png b/docs/pictures/EditTagFlowChart.png new file mode 100644 index 000000000..84b0e46f9 Binary files /dev/null and b/docs/pictures/EditTagFlowChart.png differ diff --git a/docs/pictures/Find_Reference_Frame.PNG b/docs/pictures/Find_Reference_Frame.PNG new file mode 100644 index 000000000..1527b9872 Binary files /dev/null and b/docs/pictures/Find_Reference_Frame.PNG differ diff --git a/docs/pictures/Find_Sequence_Diagram.png b/docs/pictures/Find_Sequence_Diagram.png new file mode 100644 index 000000000..c55df054b Binary files /dev/null and b/docs/pictures/Find_Sequence_Diagram.png differ diff --git a/docs/pictures/FlowchartinitCleaner.png b/docs/pictures/FlowchartinitCleaner.png new file mode 100644 index 000000000..f90a23d70 Binary files /dev/null and b/docs/pictures/FlowchartinitCleaner.png differ diff --git a/docs/pictures/FlowchartsetStatus.png b/docs/pictures/FlowchartsetStatus.png new file mode 100644 index 000000000..ee4de31ec Binary files /dev/null and b/docs/pictures/FlowchartsetStatus.png differ diff --git a/docs/pictures/GoalDisplay.PNG b/docs/pictures/GoalDisplay.PNG new file mode 100644 index 000000000..a84dcc3b9 Binary files /dev/null and b/docs/pictures/GoalDisplay.PNG differ diff --git a/docs/pictures/GraphTargets.png b/docs/pictures/GraphTargets.png new file mode 100644 index 000000000..1d8f60b29 Binary files /dev/null and b/docs/pictures/GraphTargets.png differ diff --git a/docs/pictures/SDautoClean.png b/docs/pictures/SDautoClean.png new file mode 100644 index 000000000..d1ed7b5df Binary files /dev/null and b/docs/pictures/SDautoClean.png differ diff --git a/docs/pictures/Simplified_Class_Diagram.png b/docs/pictures/Simplified_Class_Diagram.png new file mode 100644 index 000000000..21f418454 Binary files /dev/null and b/docs/pictures/Simplified_Class_Diagram.png differ diff --git a/docs/pictures/StartCD.png b/docs/pictures/StartCD.png new file mode 100644 index 000000000..97539d709 Binary files /dev/null and b/docs/pictures/StartCD.png differ diff --git a/docs/pictures/afterClean.png b/docs/pictures/afterClean.png new file mode 100644 index 000000000..020f87a21 Binary files /dev/null and b/docs/pictures/afterClean.png differ diff --git a/docs/pictures/checkActivity.png b/docs/pictures/checkActivity.png new file mode 100644 index 000000000..3f08eec99 Binary files /dev/null and b/docs/pictures/checkActivity.png differ diff --git a/docs/pictures/checkTimev2.png b/docs/pictures/checkTimev2.png new file mode 100644 index 000000000..591218645 Binary files /dev/null and b/docs/pictures/checkTimev2.png differ diff --git a/docs/pictures/checkactivityv2.png b/docs/pictures/checkactivityv2.png new file mode 100644 index 000000000..f79d8706f Binary files /dev/null and b/docs/pictures/checkactivityv2.png differ diff --git a/docs/pictures/checktime.png b/docs/pictures/checktime.png new file mode 100644 index 000000000..b3350f2fe Binary files /dev/null and b/docs/pictures/checktime.png differ diff --git a/docs/pictures/cleanEg.png b/docs/pictures/cleanEg.png new file mode 100644 index 000000000..f217801e2 Binary files /dev/null and b/docs/pictures/cleanEg.png differ diff --git a/docs/pictures/cleanlist.png b/docs/pictures/cleanlist.png new file mode 100644 index 000000000..24368c7c6 Binary files /dev/null and b/docs/pictures/cleanlist.png differ diff --git a/docs/pictures/continue.png b/docs/pictures/continue.png new file mode 100644 index 000000000..92e47988d Binary files /dev/null and b/docs/pictures/continue.png differ diff --git a/docs/pictures/continueActivity.jpg b/docs/pictures/continueActivity.jpg new file mode 100644 index 000000000..3e9e4057f Binary files /dev/null and b/docs/pictures/continueActivity.jpg differ diff --git a/docs/pictures/continue_flowchart.PNG b/docs/pictures/continue_flowchart.PNG new file mode 100644 index 000000000..f9ace2d30 Binary files /dev/null and b/docs/pictures/continue_flowchart.PNG differ diff --git a/docs/pictures/end.png b/docs/pictures/end.png new file mode 100644 index 000000000..f8091a467 Binary files /dev/null and b/docs/pictures/end.png differ diff --git a/docs/pictures/filter-find_chain.PNG b/docs/pictures/filter-find_chain.PNG new file mode 100644 index 000000000..e1f5bb226 Binary files /dev/null and b/docs/pictures/filter-find_chain.PNG differ diff --git a/docs/pictures/filter-graph_chain.PNG b/docs/pictures/filter-graph_chain.PNG new file mode 100644 index 000000000..f9557f1ba Binary files /dev/null and b/docs/pictures/filter-graph_chain.PNG differ diff --git a/docs/pictures/filter.PNG b/docs/pictures/filter.PNG new file mode 100644 index 000000000..fd5f690cb Binary files /dev/null and b/docs/pictures/filter.PNG differ diff --git a/docs/pictures/filter_eg.PNG b/docs/pictures/filter_eg.PNG new file mode 100644 index 000000000..b4975f9cf Binary files /dev/null and b/docs/pictures/filter_eg.PNG differ diff --git a/docs/pictures/find-allocations_chain.PNG b/docs/pictures/find-allocations_chain.PNG new file mode 100644 index 000000000..77e6b031b Binary files /dev/null and b/docs/pictures/find-allocations_chain.PNG differ diff --git a/docs/pictures/find_eg.PNG b/docs/pictures/find_eg.PNG new file mode 100644 index 000000000..5c62c21d6 Binary files /dev/null and b/docs/pictures/find_eg.PNG differ diff --git a/docs/pictures/graph.png b/docs/pictures/graph.png new file mode 100644 index 000000000..411960106 Binary files /dev/null and b/docs/pictures/graph.png differ diff --git a/docs/pictures/graph_core.PNG b/docs/pictures/graph_core.PNG new file mode 100644 index 000000000..6a92597cf Binary files /dev/null and b/docs/pictures/graph_core.PNG differ diff --git a/docs/pictures/graph_seqDiag.png b/docs/pictures/graph_seqDiag.png new file mode 100644 index 000000000..b0e6fddfd Binary files /dev/null and b/docs/pictures/graph_seqDiag.png differ diff --git a/docs/pictures/graphallocations.png b/docs/pictures/graphallocations.png new file mode 100644 index 000000000..d3bca715a Binary files /dev/null and b/docs/pictures/graphallocations.png differ diff --git a/docs/pictures/graphtags.PNG b/docs/pictures/graphtags.PNG new file mode 100644 index 000000000..73d661181 Binary files /dev/null and b/docs/pictures/graphtags.PNG differ diff --git a/docs/pictures/graphtags_example.PNG b/docs/pictures/graphtags_example.PNG new file mode 100644 index 000000000..999d13623 Binary files /dev/null and b/docs/pictures/graphtags_example.PNG differ diff --git a/docs/pictures/graphtags_example2.png b/docs/pictures/graphtags_example2.png new file mode 100644 index 000000000..efb5b522f Binary files /dev/null and b/docs/pictures/graphtags_example2.png differ diff --git a/docs/pictures/list-graphtags_chain.PNG b/docs/pictures/list-graphtags_chain.PNG new file mode 100644 index 000000000..0243ed75c Binary files /dev/null and b/docs/pictures/list-graphtags_chain.PNG differ diff --git a/docs/pictures/list.PNG b/docs/pictures/list.PNG new file mode 100644 index 000000000..b24d09d71 Binary files /dev/null and b/docs/pictures/list.PNG differ diff --git a/docs/pictures/list_core.PNG b/docs/pictures/list_core.PNG new file mode 100644 index 000000000..c7e519936 Binary files /dev/null and b/docs/pictures/list_core.PNG differ diff --git a/docs/pictures/list_filter_chain.PNG b/docs/pictures/list_filter_chain.PNG new file mode 100644 index 000000000..2df5a832b Binary files /dev/null and b/docs/pictures/list_filter_chain.PNG differ diff --git a/docs/pictures/list_week.PNG b/docs/pictures/list_week.PNG new file mode 100644 index 000000000..981b02736 Binary files /dev/null and b/docs/pictures/list_week.PNG differ diff --git a/docs/pictures/listforgraphallocations.png b/docs/pictures/listforgraphallocations.png new file mode 100644 index 000000000..547eb1abb Binary files /dev/null and b/docs/pictures/listforgraphallocations.png differ diff --git a/docs/pictures/setStatusSD.png b/docs/pictures/setStatusSD.png new file mode 100644 index 000000000..f96105fa9 Binary files /dev/null and b/docs/pictures/setStatusSD.png differ diff --git a/docs/pictures/startCD2.png b/docs/pictures/startCD2.png new file mode 100644 index 000000000..dd8896edc Binary files /dev/null and b/docs/pictures/startCD2.png differ diff --git a/docs/team/ananda-lye.md b/docs/team/ananda-lye.md new file mode 100644 index 000000000..446b5f064 --- /dev/null +++ b/docs/team/ananda-lye.md @@ -0,0 +1,166 @@ +# Project Portfolio Page (PPP) +## Project overview +**Jikan** is a CLI time-tracker built in Java that aims to help manage tasks and projects. +Users can set tags and goals for their entries, +ultimately being able to keep track of what's left to do and maintain an overview of how time was spent. + +## Summary of contributions +### Code contributed +[Link to tP Code Dashboard](https://nus-cs2113-ay1920s2.github.io/tp-dashboard/#search=ananda-lye) + +### Enhancements implemented +* Find and Filter Activities + * Developed the `find` and `filter` commands for users to view a sub-list of activities, matching for name and + tag keywords respectively. + * Both commands allow for multiple keywords to be matched with `find` accepting `/` separated keywords and `filter` + accepting space-separated keywords. Activities which match either keyword will be included in the sub-list. + * Both commands can be limited to only searching activities in the last shown list by including the `-s` flag. + This is particularly helpful when used before graphing functions to omit undesired activities. + * This can also be included as a single-line user input, separating commands by `;` which reduces the + number of user inputs and printing calls required to achieve the same results. + +* Last Shown List Implementation + * Proposed and implemented the last shown list which is used in `list`, `find`, `filter` and `graph` commands. + * This proposal sets the direction for how the Jikan software is used, as all analysis done by the user revolves + around this functionality (combining `list` by date range, `find` and `filter` chaining, to allow `graph` to be + clutter-free). + +* Activity Progress and Ui + * Proposed and implemented the progress percentage based user messages, allowing for the progress bars in `end` and + `graph` commands. + + +### Contributions to documentation +* Implemented and de-conflicted the high-level flow of the User Guide, distinguishing from Basic to Advanced features for +improved format standardisation. +* Provided instructions and examples for `find` and `filter` commands and their multiple variations. + +### Contributions to the DG +* Drafted write-ups and diagrams in the design section, mainly the overall architecture diagram and class diagram. +* Drafted the `find` and `filter` sections, including the sequence diagram, design considerations, and proposed features. + +### Contributions to team-based tasks +* Generated ideas with the team on the set of features for the Jikan application. +* Made use of the issue tracker extensively to track enhancement and bugs found. +* Developed jUnit tests for `find`, `filter` and `list` commands. + +### Review/mentoring contributions +* Provided feedback to teammates before and after implementation to ensure that everyone is on the same page. + +### Contributions beyond the project team +* Provided feedback to the developer guide of another team. + * [Reviewing of DG on Week 11](https://github.com/nus-cs2113-AY1920S2/tp/pull/14) + +* Reported bugs in other team's product in PE dry run. + * [PED](https://github.com/ananda-lye/ped/issues) + +## Contributions to the User Guide (Extracts) + +### Finding Activities by Name: `find` +**Usage:** Users can request for a sub-list of activities that has names which contain any of the given keywords. If there are more than one keyword, each keyword should be separated with ` / `. + +**Format:** +* `find KEYWORD` +* `find KEYWORD1 / KEYWORD2 / KEYWORD3` + +### Filtering Activities by Tags: `filter` +**Usage:** Users can request for a sub-list of activities that has specific tags. Each tag should be space separated. + +**Format:** +* `filter TAGNAME` +* `filter TAGNAME1 TAGNAME2` + +### Chaining Finds & Filters: `-s` +**Usage:** Users can provide the `find` and `filter` command on the last shown list (also compatible after a `list` +command) by providing the `-s` flag after each `find` or `filter` command. + +**Format:** +* `find -s KEYWORD` +* `filter -s TAGNAME` +* `filter -s TAGNAME1 TAGNAME2` +* `find -s KEYWORD1 / KEYWORD2 / KEYWORD3` + +**Example:** +If we want to find all CS2106 tutorials, we can first use `filter 2106` to filter out all activities tagged `2106`, then use the find command with the flag, `find -s Tutorial` to get a list of all 2106 Tutorials. + +#### Single Input Chaining: `;` +**Usage:** Users can achieve the same outcome as multiple `-s` chaining with a single input. This is done by separating +`find` and `filter` commands with ` ; `. + +**Examples:** +* `filter TAGNAME ; find KEYWORD1 ; find KEYWORD2` +* `filter -s TAGNAME ; find KEYWORD1 ; find KEYWORD2` + +Note: `-s` is only relevant in the first command of the entire input string, as subsequent commands are automatically chained. + +## Contributions to the Developer Guide (Extracts) +## 2. Design +The section provides a high-level explanation of how the Jikan software is designed. + +### 2.1 High-Level Architecture +The users interact with the Jikan software which modifies the local storage data file. + +Within the Jikan software, there are 5 main components: +* **Parser Component** - Parses the user inputs and calls the relevant `Command` object to execute the desired +command. +* **Ui Component** - Prints to the user the output of the desired `Commands`. +* **Commands Component** - Contains all the `Commands` to be called by the `Parser` based on user inputs. +* **Activities Component** - Maintains the non-permanent state of all `Activities` in the `Activity List` to be accessed +by the executing `Commands`. +* **Storage Component** - Interacts with and modifies the local storage file, which contains the permanent (lasting +even after the program terminates) state of all activities. It retrieves this permanent state and populates the `Activity List` at the start of each session. + +### 3.8 Find & Filter Features + +#### Find Feature +This command accepts keyword(s) and searches either the entire activity list or the last shown list for activities with +names containing each keyword. + +#### Filter Feature +This feature accepts space-separated keyword(s) to search either the entire list or the last shown list +for activities with tags matching each keyword. The keywords should be an exact-match with the tag names. + + +#### 3.8.1 Design Considerations +As the `find` and `filter` commands are important for the user to analyse and eventually graph time-spent on each +activity. The user should be allowed to query for all useful combinations of activities in the activity list. +This entails: +* The user should be allowed to match for multiple keywords at once. +* The user should be allowed to exclude certain activities by limiting his search to a previously shown list as + opposed to the entire activity list. + (chaining `list`, `find`, and `filter` commands). + + +#### 3.8.2a Current Implementation for Find +* This feature is called by the user when the `find` command is entered into the command line. +The string following the command are the parameters: + * `-s` flag indicates that the searching should be limited to activities previously shown to the user. + * The remaining parameters are a string of keywords separated by ` / `. +* The Parser will create a FindCommand object. +* The FindCommand will invoke its own `executeCommand()` method. + * The Parser's `lastShownList` will be cleared. + * Then it will loop through `activityList` to find activities with names that contain the keyword. + * If one is found, it will be added to `lastShownList`. + * `printResults()` of the Ui will be called: + * If `lastShownList` is not empty, it will print the matching activities. + * Else, it will respond to the user that there are no tasks which match the given keyword. + + +#### 3.8.2b Current Implementation for Filter +* This feature is called by the user when the `filter` command is entered into the command line. The space separated strings following the command are the keywords to match activity tags with. +* The Parser will create a FilterCommand object. +* The FindCommand will invoke its own `executeCommand()` method. +* The Parser's `lastShownList` will be cleared. +* For each keyword: + * Then it will loop through `activityList` to find activities with tags that contain the keyword. + * If one is found, it will be added to `lastShownList`. + * `printResults()` method of the Ui will be called + * If `lastShownList` is not empty, it will print the matching activities. + * Else, it will respond to the user that there are no tasks which match the given keyword. + +#### 3.8.4 Additional features +`find` and `filter` command supports the limiting of searches to activities in the last shown list. This +is done in 2 ways: +* The `-s` flag following the command (eg. `find -s keyword`) +* The `;` delimiter for a combination of `find` and `filter` in a single input (eg. `find KEYWORD ; filter TAGNAME`) + diff --git a/docs/team/btricec.md b/docs/team/btricec.md new file mode 100644 index 000000000..fc217874d --- /dev/null +++ b/docs/team/btricec.md @@ -0,0 +1,203 @@ + +# Project Portfolio Page (PPP) +## Project overview +**Jikan** is a CLI time-tracker built in Java that aims to help manage tasks and projects. Users can set tags and goals for their entries, ultimately being able to keep track of what's left to do and maintain an overview of how time was spent. + +## Summary of contributions +### Code contributed +[Link to tP Code Dashboard](https://nus-cs2113-ay1920s2.github.io/tp-dashboard/#search=btricec) + +### Enhancements implemented +* Starting and ending activities + * Implemented the basic `start` and `end` commands. + * `start` allows the user to start an activity and add tags to that activity. + * `end` ends the started activity and saves it to the activity list. + * `abort` stops the current activity and does not save it. + * Implemented an additional feature that gives the user the option to end an activity when the user exits the app after starting an activity. + +* Continue + * Allows the user to continue an activity in the activity list. + * Implemented an additional feature that gives the user the option to continue an activity if they used the `start` command to start an activity already in the activity list. + +* Graph activities and tags + * `graph activities` displays a graph of the duration of each activity based on the `lastShownList` + * `graph tags` calculates the duration of activity tags in the `lastShownList` and displays a graph of the cumulative duration for each tag. + +* Delete + * Allows the user to delete an activity from the activity list. + +### Contributions to documentation +* Did the *Usage* section to give an overall example of how Jikan can be used in logical flow of commands, providing examples of expected outputs. +* Provided syntax and usage examples for some commands (namely, start, end, continue, abort and delete) + +### Contributions to the DG +* Drew command class diagram +* Explained the implementation of the `continue` command (under section 3.5) using Sequence Diagrams +* Explained the implementation of the `graph` command (under section 3.9) using Sequence Diagrams +* Gave instructions for manual testing and included sample test cases for `list`, `continue` and `graph` commands. + +### Contributions to team-based tasks +* Set up the issue tracker with relevant labels and milestones. +* Made extensive use of the issue tracker to manage enhancements and bugs. +* Released v1.0 and v2.0 of Jikan, and also provided a set of sample data for v2.0 for testing. +* Did more extensive refactoring for commands, moving the command execution to the command class itself. + +### Review contributions +* Contributed to ideation and implementation discussion of new features offline. +* Although I did not explicitly review PRs on github, I actively tried to help with text-ui issues which were a big problem for our group. (text-ui file had to be updated every day to reflect the current date for it to pass) + + +## Contributions to the User Guide (extracts) + +## Usage +Jikan lets you record how much time you spend on various activities so that you can easily see what took up the most time today / this week / this month. +(In the example below, we use the example of a student tracking his/her schoolwork, but Jikan can be used for more than just that!) + +To start, record your first activity using the `start ACTIVITY_NAME` command. + +Add some tags to your activities to group similar activities together using `/t`. Tags help you group activities of the same type together, +in this example, we use the tags feature to label activities according to their module code. +**(Note that each activity can only store two tags at maximum.)** + +Add allocated time to your activities using `/a`. This allows users to set aside how much time they would like to spend on an activity and +keep track on whether they are spending too much or too little time for that particular activity. + +When you are done with the activity, or want to move onto something else, tell Jikan to `end` and the Activity time will be recorded and saved to your list. + +You can view all your activities using the `list` command. Or view all your activities over a period of time by using `list` with extra parameters. For example `list week` will return a list of all activities this current week, as shown below. + +![list week](./pictures/list_week.PNG) + +The list still looks very cluttered, we can reduce it further! Want to find everything you did for CS2113 this week? Filter out the activities you want to see using the `find` or `filter` command. This is our list after filtering out all our activities tagged as `2113`. (the -s flag tells Jikan to search our last shown list, i.e. the list of activities this week in this case) + +![Continue command sequence diagram](./pictures/filter.PNG) + +To easily see what took up the most of your time out of all the 2113 activities, use the `graph` command to view a chart of your activities. + +![Continue command sequence diagram](./pictures/graph.png) + +Curious about what module took up the most time this week? We can use the `graph tags` command on our weekly activity list to find out. + +![Continue command sequence diagram](./pictures/graphtags.PNG) + +Evidently, it was CS2105. + +Not done with an activity and want to continue on it? Use the `continue` command to continue recording time for a previously started activity. + +Finally, when you're done and want to close the app, simply say `bye` and Jikan will exit. + +This is just a quick overview of what Jikan can do for you. For more details on each individual command, read the command guide below. + +### Starting an activity: `start` +**Usage:** Starts recording the time for a new activity. + +**Format:** `start ACTIVITY_NAME /a ALLOCATED_TIME /t TAGS` + * `ACTIVITY_NAME` can contains spaces and must be less than 25 characters. +* `ACTIVITY_NAME` must also be unique (should the user start an already existing activity, the option to `continue` will be given). +* `ALLOCATED_TIME` should be of the format [HH:MM:SS] and cannot exceed 23:59:59. +* `TAGS` must be single spaced separated and a maximum of 2 tags can be stored. +* `ALLOCATED_TIME` and `TAGS` are optional. + +**Example:** +`start assignment /a 01:30:00 /t CS1010` +`start GER1000 quiz /t GER GEmod` +`start revision` + +### Continuing an activity: `continue` +**Usage:** Continues recording the time of an activity that you have previously started. + +**Format:** `continue ACTIVITY_NAME` +* `ACTIVITY_NAME` must be an existing activity in the activity list. + +**Example:** +`continue revision` + +### Ending an activity: `end` +**Usage:** Stops recording the time for an ongoing activity and stores it into the activity list. + +**Format:** `end` +* An activity must be started or continued before it can be ended. + +### Aborting an activity: `abort` +**Usage:** Aborts the current activity and does not save it to the activity list. + +**Format:** `abort` +* An activity must be started or continued before it can be ended. + +### Delete an activity: `delete` +**Usage:** Deletes an activity in the activity list. + +**Format:** `delete ACTIVITY_NAME` + +## Contributions to the Developer Guide (extracts) +### 3.5 Continue Feature +The continue feature allows the user to continue a previously ended activity. + +#### 3.5.1 Current Implementation +(diagrams are omitted) +**Continuing an activity:** + +- When the user enters the command to continue an activity, a _ContinueCommand_ object is created in _Parser_. The method `executeCommand()` of the _ContinueCommand_ object is then called. +- `executeCommand` checks if the given activity name exists in the activityList by calling `findActivity()` (if it doesn’t an exception is thrown, omitted in the sequence diagram above) +- It then gets the `name` and `tags` of the activity to be continued and saves it to a public static variable of _Parser_ object. +- It also gets the current time and saves it to a public static variable of _Parser_ object. + +**Ending a continued activity:** + +- When the user wants to end the continued activity, an _EndCommand_ object is created in _Parser._ The method `executeCommand()` of the _ContinueCommand_ object is then called and it in turn executes the `saveActivity()` method of the _ActivityList_ class. +- `saveActivity()` gets the current time and saves it to a public static variable of _Parser_ object. +- Then the elapsed time is calculated using the `between()` method of _Duration_ class. +- The elapsed time is added with the previous duration of the activity to get the `newDuration` using the `plus()` method of Duration class. +- `updateDuration()` method is called to update the `duration` attribute of the continued activity in the `activityList` as well as the `data.csv` file. + +#### 3.5.2 Design Considerations + +**Execution:** + +- Continue by activity name (current implementation) + - **Cons:** Activity names have to be unique. + - **Pros:** More versatile, resistant to changes in the activity list +- Continue by activity index + - **Cons:** need to add an additional index field to the Activity class, index is not fixed, changes when an activity is deleted + - **Pros:** Can reuse activity names. + +Although the current implementation of the continue feature disallows users to have multiple activities with the same name, we felt that the versatility of this choice outweighed the cons. Firstly because if the activityList got too big, it would be hard for the user to get the index of the task they wanted to continue. Also, the index would constantly be changing when changes are made to the list. + +#### 3.5.3 Additional Features + +As users can only have activities with unique names, when a user wants to start an activity which already exists in the activityList, they will be given the option to continue the stated activity. + +### 3.9 Graph Feature + +This feature gives the user a visual representation of their activity duration and activity goals. +Graph can be used along with `list`, `find` and `filter` to sieve out the data to be graphed. + +#### 3.9.1 Current Implementation +(diagrams are omitted) +- This feature is called by the user when the `graph` command is entered into the command line. The user will then have to specify what he would like to graph (goals progress bar / tag duration / activity duration). +- The Parser will create a GraphCommand object. +- The GraphCommand will invoke its own `executeCommand()` method. + +**Graph targets** +This displays the progress bar for the duration with respect to allocated time of activities in the `lastShownList`. + +- If the user indicated `targets`, Ui calss will be called to execute graphTargets. + +**Graph tags** +This displays a bar graph of the cumulative duration of the tags for each activity in the `lastShownList`. E.g. if 3 activities in the `lastshownlist` are tagged `CS2113`, the durations of these 3 activities are added up and associated with the tag `CS2113` in the graph. + +- If the user indicated `tags`, `GraphCommand` will call it's own `graphTags` method. +- A HashMap (`tags`) of tags to duration is created. +- `graphTags` iterates through every activity in `lastshownlist` and in each loop, `extractTags` is called. +- `extractTags` loops through the tags of that activity. Tag is added to the `tags` if it is not found. Else, the duration of the activity is added to the corresponding tag in `tags`. +- `tags` and `interval` (how many minutes each point in the graph represents) is passed to the method printTagGraphs in Ui to print the graph. + +**Graph activities** +This displays a bar graph of the durations of each activity in the `lastShownList`. + +- If the user indicated `activities`, `GraphCommand` will call it's own `graphDuration` method. +- `graphDuration` calls `printActivityGraph` of the Ui class and passes the `interval` parameter, which is how many minutes each point in the graph represents. + +#### 3.9.2 Additional features + +As graph gets it's data based on the `lastShownList`, users can pair the `graph` command with `find`, `filter`, and `list` to sieve out the activities to be graphed. \ No newline at end of file diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b..000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/nigellenl.md b/docs/team/nigellenl.md new file mode 100644 index 000000000..3fcd15c5e --- /dev/null +++ b/docs/team/nigellenl.md @@ -0,0 +1,132 @@ +# Project Portfolio Page (PPP) +## Project overview +**Jikan** is a CLI time-tracker built in Java that aims to help manage tasks and projects. Users can set tags and goals for their entries, ultimately being able to keep track of what's left to do and maintain an overview of how time was spent. + +## Summary of contributions +### Code contributed +[Link to tP Code Dashboard](https://nus-cs2113-ay1920s2.github.io/tp-dashboard/#search=nigellenl) + +### Enhancements implemented +* Edit activities + * Ensures that the activity record is accurate by giving the user the ability to update certain parameters of the activities that have been recorded. + * Allow the user to edit the name of existing activities in the activity list. + * Allow the user to edit the allocated time of existing activities in the activity list. + * `edit ACTIVITY_NAME /en NEW_NAME` allows the user to edit the activity name. + * `edit ACTIVITY_NAME /ea NEW_ALLOCATED_TIME` allows the user to edit the allocated time. + +* Goal setting + * Allow the user to set and delete goals based on existing tags. + * `goal TAG_NAME /g GOAL_TIME` allows the user to set the goal for that tag. + * `goal TAG_NAME /d` allows the user to delete the goal set for that tag. + +* View goals + * Allow the user to view the goals easily in a table format. + * `goal` displays all the goals that have been set together with the actual time spent on the activities under the tags and the amount of time left to meet the goal. + +### Contributions to documentation +* Provided syntax and usage examples for some commands (edit, set goal, view goal). +* Added command summary with command name and syntax in the User Guide. + +### Contributions to the DG +* Added setting up instructions. +* Explained the implementation of the `edit` command (under section 3.4) using Sequence Diagram. +* Gave instructions for manual testing and included sample test cases for `goal` command. +* Added the user stories for the program. +* Added the non-functional requirements for the program. + +### Contributions to team-based tasks +* Made extensive use of the issue tracker to manage enhancements and bugs. +* Did refactoring for commands by creating command classes. +* Generate table of content for user guide. + +### Review contributions +* Discussed with team on how to further enhance existing features (e.g. allowing user to edit allocated time) + +## Contributions to the User Guide (extracts) +### Editing an activity: `edit` +**Usage:** Edits the name or allocated time of an activity in the activity list. + +**Format** +* `edit ACTIVITY_NAME /en NEW_NAME` +* `edit ACTIVITY_NAME /ea NEW_ALLOCATED_TIME` + * `NEW_ALLOCATED_TIME` should be in the format [HH:MM:SS] + +**Example:** +`edit CS1010 assignment /en CS1010 assignment 2` Activity name is edited to `CS1010 assignment 2` +`edit CS1010 assignment /ea 10:00:00` Allocated time for activity is edited to `10:00:00` + +### Tag Goals + +By using the `goal` command, users can set specific goals for how long they would like to spend on activities under a certain tags as well as view the amount of time they have spent in total for those activities as compared to their goal. + +### Set goal: `goal TAG_NAME /g DURATION` +**Usage:** Sets a duration goal for a tag + +**Format:** `goal TAG_NAME /g DURATION` +* The duration should be in the format [HH:MM:SS] + +**Example:** `goal core /g 24:00:00` a goal of `24:00:00` is added for the tag `core` + +### Delete goal: `goal TAG_NAME /d` +**Usage:** Deletes the duration goal set for the tag. + +**Format:** `goal TAG_NAME /d` + +### View goals: `goal` +**Usage:** Displays the tags with their goals, actual time spent on activities with these tags and the difference between the 2 timings. + +**Format:** `goal` + +## Contributions to the Developer Guide (extracts) + +### 3.5 Edit feature +The edit feature allows the user to make changes to activities that have been saved in the activity list. This is to allow the user to rectify any mistakes that may have been made during the initial recording of the activity. + +#### 3.5.1 Current Implementation +The following sequence diagram shows how the edit feature works. +The current implementation of the edit feature allows the user to edit the activity name as well as its allocated time. +The following sequence diagram shows how the edit feature works for editing the activity name. The diagram for the editing of allocated time is omitted as the sequence is relatively similar. + +_[The sequence diagram has been omitted in this section]_ + +The current implementation of the edit feature allows the user to edit only the name and allocated time parameter of the activity. When the user wants to edit an activity using the edit command, a new EditCommand object is created. The `executeCommand()` method of the EditCommand object is called and the specified parameters are updated accordingly. + +The order of method calls to edit the activity details is as follows if the specified activity exists (meaning `index != -1`) else an exception is thrown: +1. The `updateName()` method of the ActivityList class is called, with the user-specified parameters of the activity index and new activity name +2. The `get()` method is self-invoked by the ActivityList class to obtain the activity at the given index +3. The `setName()` method of the Activity class is called to edit the activity name to the user-specified name +4. The activity is updated with its new name in the activityList. +5. The `fieldChangeUpdateFile()` method of the StorageHandler class is called to update the data file with the new activity name. + + +#### 3.5.2 Additional Implementations +The current implementation of the edit feature only allows the user to edit the activity name and allocated time. Hence, additional implementations of the edit feature could allow the user to edit other parameters of the activity such as the tags and the start and end dates. + +This will require the implementation of more update methods in the ActivityList class to allow for the changes to be updated in the activityList after it has been edited. Additionally, there may be more updates required if the tags were to be edited due to the tag goals feature. + +The flowchart below shows the flow of activities if the feature of editing tags were to be implemented. + +_[The flowchart diagram has been omitted in this section]_ + + +#### 3.5.3 Design Considerations +##### Current Design +The user is able to edit only the name and allocated time of the activity, which are user input data. + +**Pros:** +* The user is able to correct any mistake made during the recording of the activity. +* The user is able to adjust their allocated time for the activity based on their needs. +* Ensures that the record of activities is accurate and consistent in order for more efficient analysis of the time spent. + +**Cons:** +* The user is only able to edit 2 parameters of the activity, which may be restrictive for them. + +##### Possible Design +The user is able to edit any parameters of the activity, including tags, start and end date/time. + +**Pros:** +* The user has more flexibility in modifying the record of activities based on their needs. + +**Cons:** +* By allowing the user to edit the date and time, there may be potential inaccuracies in the record of activities, defeating the purpose of the time tracking program. +* By allowing the user to edit the tags, the tag goals command may become more complicated due to the need to keep track of the presence of the tags. diff --git a/docs/team/rdimaio.md b/docs/team/rdimaio.md new file mode 100644 index 000000000..3f796e4f1 --- /dev/null +++ b/docs/team/rdimaio.md @@ -0,0 +1,139 @@ +# Project Portfolio Page (PPP) +## Project overview +**Jikan** is a CLI time-tracker built in Java that +aims to help manage tasks and projects. +Users can set tags and goals for their entries, +ultimately being able to keep track of what's left to do +and maintain an overview of how time was spent. + +## Summary of contributions +### Code contributed +[Link to tP Code Dashboard](https://nus-cs2113-ay1920s2.github.io/tp-dashboard/#=undefined&search=rdimaio) + +### Enhancements implemented +* Storage + * Storage handles saving, creating and accessing files. + This is a necessary feature, given the nature of Jikan as a timetracker + that needs to retain information across multiple sessions. +* StorageHandler + * Higher-level interface to Storage objects; + StorageHandler provides functions to retrieve and manipulate data + for the rest of the application. +* List + * The `list` command allows the user to list all the activities for a specified + date or range of dates. In many cases, the user does not want to specify + the exact dates; that is why the user can simply ask to `list day`, `list week`, + `list month april` etc. to list entries belonging in the specified time frame. + +### Contributions to documentation +* Provided information on how to use the `list` command, including +all the possible parameters (`day`, `week`, `month`) + +### Contributions to the DG +* Provided information on the `Storage` object, +including how to use it for multiple concurring data files +* Provided information on the `StorageHandler` object, +specifically which functions can be used to write/edit/remove data +* Provided information on the possibilities of the `list` command + +### Contributions to team-based tasks +* Generating tables of content automatically for documentation +* Made extensive use of the issue tracker + * Labelled each PR and issue with severity and type + * Linked PRs to close their respective issues automatically + +### Review contributions +* Provided comments on possible ways to solve issues, +e.g. [#162](https://github.com/AY1920S2-CS2113-T15-1/tp/issues/162) + +### Contributions beyond the project team + +* Community + * Reported bugs: + * ["Submit post-lecture quiz" missing from Week 12 Admin info section](https://github.com/nus-cs2113-AY1920S2/forum/issues/96) + * [Arrow pointing the wrong way in the new UML video?](https://github.com/nus-cs2113-AY1920S2/forum/issues/75) + * Helped other students: + * [Problem when importing contacts project to Intellij](https://github.com/nus-cs2113-AY1920S2/forum/issues/23#issuecomment-581352650) + * [Markdown syntax](https://github.com/nus-cs2113-AY1920S2/forum/issues/88#issuecomment-603329337) + +## Contributions to the User Guide (extracts) + +### Listing activities: `list` +**Usage:** Displays a list of the completed activities. + +**Format:** `list TIME_PERIOD` +* If no `TIME_PERIOD` is given, all activities will be listed. +* `TIME_PERIOD` can be `day` or `week` +* To list activities in a specific month of the current year, use `list month MONTH_NAME` where `MONTH_NAME` must be spelled out in full (i.e. January and not Jan). +* Otherwise, `TIME_PERIOD` should be of the format [dd/MM/yyyy] or [yyyy-MM-dd] +* `TIME_PERIOD` can either be a specific date or over a range. + +**Example:** +`list` List all activities. +`list month april` Lists all activities in April. +`list week` or `list weekly` List all activities in the current week. +`list day` or `list daily` List all activities in the current day. +`list 01/01/2020` or `list 2020-01-01` List all activities on 1 Jan 2020. +`list 01/01/2020 20/02/2020` List all activities than fall within 1 Jan 2020 and 20 Feb 2020. + + +### Command Guide + +* List all activities: `list` + * List today's activities: `list day` or `list daily` + * List this week's activities: `list week` or `list weekly` + * List a specific week's activities by day: `list week DATE` or `list weekly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List this month's activities: `list month` or `list monthly` + * List a specific month's activities by day: `list month DATE` or `list monthly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List a specific day's activities: `list DATE`, where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List activities within a time frame: `list DATE1 DATE2`, where both `DATE1` and `DATE2` are + in either `yyyy-MM-dd` or `dd/MM/yyyy` format + +## Contributions to the Developer Guide (extracts) +### 3.2 Storage feature +The Storage class represents the back-end of Jikan, handling the creation, saving and loading of data. +Jikan uses a `.csv` file to store its data, formatted in the following way: + +`entry-name, start-time, end-time, duration, tags` + +All tags are saved in the same cell, separated by a white space; this design decision was taken to make sure that each entry occupies the same number of cells regardless of each entry’s number of tags. The tags are then separately parsed when the data is loaded. + +Each Storage objects contains the path to the data file (`Storage.dataFilePath`), the File object representing the data file (`Storage.dataFile`), and an activityList populated with the data from the data file (`Storage.activityList`). Storage optionally supports multiple data files at the same time, allowing implementation of features like multiple sessions and multiple user profiles. + +Storage provides the following functions: +- Constructing a Storage object via `Storage(String dataFilePath)`, which takes in the path to the desired data file (or the path where the user wants to create the data file) as a String object. +- Creating a data file via `createDataFile`. +- Writing to a data file via `writeToFile`. This function takes a single string as parameter and writes it to the data file. It is recommended to only pass single-line strings to keep the file nicely formatted. +Loading a pre-existing data file via `loadFile`. If a data file already exists for the provided data file path, the function will return `true`; if the specified data file did not previously exist, this function will call the `createDataFile` method and returns `false`. The return value is useful so that the application knows whether or not this is the first session with a specific data file or if data already exists. +- Creating an ActivityList via `createActivityList`. This function calls `loadFile()` to check whether the file already existed or not; if the data file previously existed, it will construct an ActivityList object by passing the data from the data file to it, and return this populated ActivityList object; if the data file did not previously exist, it will return an empty activityList object. + +### 3.3 Storage handler +The StorageHandler class functions as a support to the main Storage class, allowing the Jikan application to manipulate the stored data file. Its main provided functions are: +- Removing an entry from the data file via `removeLine`. This function takes in the number of the line to remove. +- Replacing an entry in the data file via `replaceLine`. This function takes in the number of the line to replace, along with the String object that needs to be written to the data file in place of the replaced line. + +### 3.6 List feature +This feature is used to list activities within a range specified by the user. +If no parameter is passed to the `list` command, then all the stored activities will be displayed. +By passing a single date, the command returns all activities within that date. +By passing two dates, the command returns all activities that took place within the two dates. +(for an activity to be included in the range, both its start and end time must be within the specified time range). +The user can also provide a verbal command, such as `day`, `week`, or `month`, which +will return all the activities for that day, week or month respectively. +Additionally, the user can specify a specific week of month by including a date +(e.g. `list month 2020-03-01` returns all the activities in March 2020.) + +#### 3.6.1 Current implementation +* List all activities: `list` + * List today's activities: `list day` or `list daily` + * List this week's activities: `list week` or `list weekly` + * List a specific week's activities by day: `list week DATE` or `list weekly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List this month's activities: `list month` or `list monthly` + * List a specific month's activities by day: `list month DATE` or `list monthly DATE`, + where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List a specific day's activities: `list DATE`, where `DATE` is in either `yyyy-MM-dd` or `dd/MM/yyyy` format + * List activities within a time frame: `list DATE1 DATE2`, where both `DATE1` and `DATE2` are + in either `yyyy-MM-dd` or `dd/MM/yyyy` format diff --git a/docs/team/siuhian.md b/docs/team/siuhian.md new file mode 100644 index 000000000..00f94529f --- /dev/null +++ b/docs/team/siuhian.md @@ -0,0 +1,355 @@ +# Project Portfolio Page (PPP) +## Project overview +**Jikan** is a CLI time-tracker built in Java that aims to help manage tasks and projects. +Users can set tags and goals for their entries, ultimately being able to keep track of what's left to do and maintain an overview of how time was spent. + +## Summary of contributions +### Code contributed +[Link to tP Code Dashboard](https://nus-cs2113-ay1920s2.github.io/tp-dashboard/#search=siuhian) + +### Enhancements implemented +* Start activities + * Added new feature on top of the existing `start` command. + * `start` command now allows users to allocate a certain amount of time for that activity using the /a flag. + * Fix bugs in the `start` command with inputs from the peer testing results. + * Added more flexibility to the `start` command, `start cs2113 /a 10:30:00 /t tP` now works the same way as + `start cs2113 /t tP /a 10:30:00` meaning the positions of /a and /t flags can now be used interchangeably. + * Added some more test cases to the Junit for StartCommand. + +* User interface + * Sourced and implemented the existing Jikan logo that greets the user upon execution. + * Created a standard template to print lines to stdout. This feature is used to print error, acknowledgement and + reply messages from Jikan to the user. + * Implemented the `bye` command which makes use of Ui to exit from the application. + +* Automated cleaning + * Built a automated cleaner that performs a batch delete on data and log files upon application execution. + * This batch delete on data works by deleting a number of completed activities (specified by user) starting from the oldest + completed activity (e.g completed activities are activities that have met its allocated time). + * Implemented `clean` command which allows the user to switch on/off the automated cleaner. + * Allows the user to specify how much completed activities/logs to delete for each round of automated cleaning. This + is done through the /n flag in the clean command. + * Did a Junit for CleanCommand to further test and improve the reliability of `clean` command. + +* Graph allocations + * Provides the user with a graphical representation on the progress of all the activities. + * The function is provided by the `graph allocations` command. + +### Contributions to documentation +* Provided syntax and usage examples for these commands. (`clean`,`graph allocations` and `start`). +* Edited the command summary and usage section to ensure consistency with the features under my implementation. + +### Contributions to the DG +* Explained the implementation of the start feature (Under Section 3.1) using a mixture of sequence diagrams with class diagrams. +* Explained the implementation of the clean feature (Under Section 3.2) using a mixture of sequence diagram with class diagrams. + +### Contributions to team-based tasks +* Generated ideas with the team on the set of features for the Jikan application. +* Made use of the issue tracker extensively to track enhancement and bugs found. +* Enabled assertions in the build.gradle file. +* Created the project repository and set up the team organization. +* Helped create the Parser and Ui class to make the code more OOP. +* Documented the target user profile for UG and DG. +* Helped out in resolving the text-ui-issues (date sensitive need to update everyday) that caused many commits to fail the checks. +* Fixed the bug that made our PPP links broken. + +### Review/mentoring contributions +* Provided feedback to peers on how certain features can be improved (e.g progress message for activities, + store tag goals in separate data file so as to not overload the main data file for activities). + +### Contributions beyond the project team +* Provided feedback to the developer guide of another team. + * [Reviewing of DG on Week 11](https://github.com/nus-cs2113-AY1920S2/tp/pull/7) +* Reported bugs in other team's product in PE dry run. + * [PED](https://github.com/siuhian/ped/issues) + +### Contributions to the User Guide (Extracts) + +### Activity allocation graph: `graph allocations` +**Usage:** View the progress of activities to see how much time was spent on the activity relative to the allocated time. + +Note: Only activities with an `ALLOCATED_TIME` will be shown. + +(Diagram omitted) + +For example, if we `graph allocations` for the activity list above, we will get the following graph: + +(Diagram omitted) + +`activity 3` and `activity 5` does not have an allocated time, thus they do not appear in the graph. +The percentage shown in the graph represents the activity's progress relative to their allocated time. (`activity 4` have a duration of 2 seconds while its allocated time was 5 seconds, 2/5 * 100% = 40%. Thus the progress of `activity 4` is 40% +as shown in the graph) + +**Format:** +`graph allocations` + +## Automated Cleaning + +Jikan provides a `clean` command where users can automate the cleaning of activities from the activity list at application startup. + +### Activate cleaning: `clean on` +**Usage:** Switch on automated cleaning. + +**Format:** `clean on` + +### Deactivate cleaning: `clean off` +**Usage:** Switch off automated cleaning. + +**Format:** `clean off` + +### Set the number of activities to clean: `clean /n` +**Usage:** Set a number of activities to clean. + +**Format:** `clean /n NUMBER` + +Note: Once cleaning is switched on, the automated cleaning persists (i.e cleaning will be done at every application startup) until it is switched off. + +**Example:** + +(Diagram omitted) + +Taking a look at this cluttered activity list, we can see that there are some activities which are done (i.e duration > allocation). +Thus, to reduce clutter, we would like to get rid of these done activities. + +However, since the list is so huge, it would be troublesome to use the delete function as users will have to manually navigate through +the list to identify the done activities and delete them. + +This is where the `clean` command would be useful. See that activity 6, 7 and 10 are done. + +(Diagram omitted) + +By using the `clean` command. Users can choose how much of these done activities to clean, for the example here, the number is set to 2. + +(Diagram omitted) + +Upon the next startup, the automated cleaning will do its work and clean the 2 oldest done activities (i.e oldest here is based on date). + +Note that since the user specified to clean only 2 activities, only activity 6 and 7 are cleaned and activity 10 remains in the activity list. + +### Automated Cleaning for Logs: + +Jikan also provides cleaning for log file which are used to record important information during program execution. This feature will be useful +to users who are running this application on systems with limited hardware (small storage space). + +### Activate log cleaning: `clean log on` +**Usage:** Switch on automated cleaning. + +**Format:** `clean log on` + +### Deactivate log cleaning: `clean log off` +**Usage:** Switch off automated cleaning. + +**Format:** `clean log off` + +### Set the number of logs to clean: `clean log /n` +**Usage:** Set number of lines of logs to clean. + +**Format:** `clean log /n NUMBER` + +### Contributions to the Developer Guide (Extracts) + +### 3.1 Start Feature + +#### 3.1.1 Current Implementation + +(Diagram omitted) + +With Jikan as the main entry point for our application, + +1. Jikan will receive user input and pass it to the Parser class to get the corresponding command. +2. The Parser class will initialise and return a Command class object based on the command in user input. +3. In this case, Parser will return a StartCommand class object to Jikan. +4. Then, Jikan will call the StartCommand#executeCommand method to start an activity. + +Additionally, StartCommand also implements the following operations: + +* **StartCommand#checkActivity** Checks if the activity already exists in the activity list. +* **StartCommand#checkTime** Checks if the allocated time provided is valid. +* **StartCommand#continueActivity** Continue on an existing activity. + +**checkActivity** + +(Diagram omitted) + +The diagram above shows how the StartCommand#checkActivity function works. This function is used to check +if the activity to be started exists in the activity list. If the activity exists in the list, that activity will be +continued and this way the user cannot start duplicate activities. + +1. When checkActivity() is called, it will make a call to the ActivityList#findActivity method. +2. Once the findActivity() method finishes execution, it will return an integer index back to checkActivity(). +3. If the index is not equals to -1, the activity to be started exists in the activity list and continueActivity() will be called. +4. Else, the activity to be started is a brand new activity and addActivityToList() will be called. + +**checkTime** + +(Diagram omitted) + +The diagram above shows how the StartCommand#checkTime function works. This function is used to check the validity of +the allocated time provided by the user input. If the allocated time is valid, the activity will be added to activity +list. + +1. When checkTime() is called, it will initialise two LocalTime objects called endTime and startTime respectively. +2. startTime will be initialised to time 00:00:00 while endTime will be calculated based on the user input to the start +command (i.e `start activity name /a HH:MM:SS /t tags`) +3. Then, the method Duration.between() will be used to get a Duration object that holds the time difference between startTime +and endTime. +4. If this Duration object is non zero (i.e user gave a valid non zero allocated time), then the activity will be added to the activity list +using the addActivity() method. + +**continueActivity** + +(Diagram omitted) + +The diagram above shows how the StartCommand#continueActivity function works. This function is used when the current activity +to be started already exists in the activity list. Thus, this function will check with the user whether to continue on that activity +and prevent duplicate activities from being started. + +1. When continueActivity() is called, it will make a call to the Scanner object to read in the next line of user input. +2. If the user input is "yes", information about the activity (activity name, tags etc.) will be forwarded to parser and the parser +will update the activity list (i.e when continue is used, activity duration is added on and needs to be updated). +3. Else, if the user input is "no", continueActivity() will notify the parser to read in the next line of user input. + +#### 3.1.2 Additional Implementation + +1. `start` command have the ability to continue an activity if the activity to be started exists in activity list as discussed above. However, the second +start command's tags and allocated time parameters will not be captured if the activity originally did have tags or allocated time. + * `start activity 1` + * `start activity 1 /a HH:MM:SS /t tags` (this command will continue activity 1 but won't add the tags and allocated time to it) + + Thus, it would be best for `start` command to address this issue and allow the second `start` command to not only continue the +activity but also edit the fields of the activity. + +2. Allows two activities to start at the same time. As a user, sometimes the activity we are doing may be linked to another activity (i.e activities like +revising CS2106 and doing CS2106 Labs are similar as doing the labs can serve like a revision too). + + Thus, it would be good if more than one activity can be started at a particular time. + +#### 3.1.3 Design Considerations + +The current design is centred around the Parser Class as all the relevant activity information (activity startTime, endTime, name, tags, +allocated time) are stored inside Parser. + +Since Parser is a public class. There are some benefits to this design. +* All the command classes have access to activity information. +* Makes the classes more lightweight as there is no need for local variables to store activity informations. +* Reduces coupling between the commands as they interact through Parser. + +However, there are some drawbacks to this design too. +* Since all the activity information are public, every class in Jikan can access/modify activity information which is +undesirable. +* This creates a lot of dependencies between Commands and Parser which makes unit testing harder to implement. +* As more commands is created to accommodate new features , Parser will be overloaded with new variables and classes. + +### 3.2 Clean Feature + +#### 3.2.1 Current Implementation + +Jikan provides a `clean` command where users can automate the cleaning of done activities (i.e activities with duration > allocation) and logging data +at application startup. + +(Diagram omitted) + +With Jikan as the main entry for our application, + +1. Upon startup, Jikan will initialise a LogCleaner and StorageCleaner object. +2. Jikan will call upon LogCleaner#autoClean() and StorageCleaner#autoClean() functions. +3. These two functions will check if the Storage and Log Cleaner are enabled respectively before cleaning. +4. Thus, by the time the user can interact with Jikan (i.e send commands to Jikan), the activity list and log files would already be cleaned. +5. Using the `clean` command, users would be able to manage the cleaner's behaviour (switching it on/off, set number of done activities/logging data to clean). + +The cleanup mechanism is stored internally as a StorageCleaner and LogCleaner class. + +These two classes have access to the data files of activity list and logs respectively and thus they are able to +directly manipulate the activity list and logging data. + +A status.txt file is initialised to keep track of the status (on/off) of the two cleaners and contains information on +the number of done activities/logging data for cleaning. + +Moreover, the CleanCommand also implements the following operation: + +* **CleanCommand#setStatus** Switch on/off the two cleaners respectively. +* **CleanCommand#setValue** Set a value for the number of done activities/logging data to be cleaned. +* Note: The two cleaners are independent, setting a value/status for one of the cleaner will not affect the other cleaner. + +**setStatus** + +(Diagram omitted) + +The diagram above shows how CleanCommand#setStatus function works. This function is a generalized function that is used to +switch on or off the cleaners by checking the parameters to the `clean` command. Thus, based on the return value of getStatus() and +getCleaner(), there are four possible scenarios. + +1. When setStatus() is called, the method will call its own class method getStatus() to check what is the status to set to. +2. There are two valid return values for getStatus() method which is "on" and "off". The diagram shows the former. +3. Upon receiving a valid return value from getStatus() which is "on" in the diagram, the setStatus() method will self invoke another +of its own class method getCleaner(). +4. The return result of the getCleaner() together with getStatus() will then be used to determine which cleaner are we setting and what is +the status to set to. +5. In other words, result of getCleaner() is used to determine whether are we calling StorageCleaner#setStatus or LogCleaner#setStatus while +the result of getStatus() determines the parameter to setStatus(). (e.g "on" will call setStatus("true") while "off" will call setStatus("false")). + +**setValue** + +The diagram of setValue is omitted as it is similar to setStatus diagram. This function is a generalized function that is used to +set a value for the number of done activities or the number of lines of logging data to be cleaned for the two cleaners respectively. + +1. When setValue() is called, the method will call its own class method getNumber() that will return an integer value corresponding to the number +to set to. +2. Upon receiving a valid return value (non negative), the setValue() method will self invoke another of its own class method getCleaner(). +3. The return result of the getCleaner() together with getNumber() will then be used to determine which cleaner are we setting and what is +the value to set to. +4. In other words, result of getCleaner() is used to determine whether are we calling StorageCleaner#setNumberOfActivitiesToClean or LogCleaner#setNumberOfLogsToClean +while the result of getNumber determines the parameter to these two functions. + +Note that steps 2-4 of setValue() are similar to steps 3-5 of setStatus(). + +On the other hand, the Storage/Log Cleaner class implements the following core operation of `clean` command. + +* **Cleaner#autoClean** This operation is called whenever Jikan is executed. Cleaning will only be done to the activity list/logging data if +the two cleaners are enabled respectively. + +**autoClean** + +(Diagram omitted) + +The diagram above shows how Cleaner#autoClean function works. This function is called whenever Jikan executes Jikan#main and is used to +perform cleaning of the activity list and logging data if Storage Cleaner and Log Cleaner are enabled respectively. The number of done activities and +lines of logging data to clean is set to 5 at default if user did not specify a value for both cleaners. + +1. When main() is called, Jikan will first initialise both the StorageCleaner and LogCleaner object using StorageCleaner() and +LogCleaner(). +2. Once both objects are initialised, Jikan will first call storageAutoClean() method of the StorageCleaner class. +3. This method will invoke another method under the StorageCleaner class called checkStatus() which will return a boolean toClean variable. +4. If toClean == true, the storageAutoClean() method will proceed and clean up the activity list before returning control back to main(). +5. Else, the storageAutoClean() will not do any clean up and will immediately return control back to main(). +6. Steps 2 to 5 will then be repeated when Jikan call logAutoClean() method of the LogCleaner class. + +#### 3.2.2 Additional Implementation + +1. Currently, the data that is cleaned up by this command is sent to a recycled folder similar to how Windows recycle bin works. + + Thus, it would be good to have a feature to restore the data deleted in the event the user wishes to recover some of the activities/logs. + + On a similar note, it would also be good to have a permanent delete feature built into the recycled folder so that items that are too old (> 6 months old) will + deleted away for good. + +2. The automated cleaning does not have a lot of flexibility as the current implementation only cleans up done activities starting from the oldest. + + Thus, it would be good if the `clean` command is expanded to allow users more freedom in specifying what activities to clean. + + * `clean /n 3 /t CS2113` does cleaning on the 3 oldest done activities with CS2113 tag. + * `clean /n 5 /i 1/4/2020 3/4/2020` does cleaning on the 5 oldest done activities with dates between 1 April 2020 and 3 April 2020. + + +#### 3.2.3 Design Considerations + +The current design uses the abstract cleaner class to create dedicated cleaners (i.e Storage and Log Cleaners) to perform +cleaning for various data files (e.g activity list data file, logging data file). + +There are some benefits to this design. +* Creating an abstract class reduces the amount of repetitive code as common methods between cleaners are abstracted out. +* Abstract classes produce a more OOP solution as different cleaners will handle different parts of the data. + +However there are drawbacks to this design too. +* There are some very similar methods with key differences that cannot be abstracted out (for e.g different parameters, different printing). +* This causes the CleanCommand class to have similar and repetitive methods to handle this difference. (for e.g setStorageCleanerOn(), setLogCleanerOn() etc). \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 000000000..13c7d5786 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: jikan.Jikan + diff --git a/src/main/java/jikan/Jikan.java b/src/main/java/jikan/Jikan.java new file mode 100644 index 000000000..749aa43a7 --- /dev/null +++ b/src/main/java/jikan/Jikan.java @@ -0,0 +1,92 @@ +package jikan; + +import jikan.activity.ActivityList; +import jikan.command.ByeCommand; +import jikan.command.Command; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.cleaner.LogCleaner; +import jikan.parser.Parser; +import jikan.storage.Storage; +import jikan.cleaner.StorageCleaner; +import jikan.ui.Ui; + +import java.io.IOException; +import java.util.Scanner; + +import static java.lang.System.exit; + +/** + * Represents the Jikan time tracker. + */ +public class Jikan { + /** Constant file path of data file. */ + private static final String DATA_FILE_PATH = "data/data.csv"; + + private static final String TAG_FILE_PATH = "data/tag/tag.csv"; + + /** Storage object for data file. */ + private static Storage storage; + + private static Storage tagStorage; + + /** Activity list to store current tasks in. */ + private static ActivityList activityList; + + public static ActivityList lastShownList = new ActivityList(); + + /** Ui to handle printing. */ + private static Ui ui = new Ui(); + + /** Parser to parse commands. */ + private static Parser parser = new Parser(); + + /** CLeaner to delete entries in data.csv when it gets too long */ + private static StorageCleaner storageCleaner; + + private static LogCleaner logCleaner = new LogCleaner(); + + public static final Scanner in = new Scanner(System.in); + + /** + * Main entry-point for the Jikan application. + */ + public static void main(String[] args) { + ui.printGreeting(); + storage = new Storage(DATA_FILE_PATH); + tagStorage = new Storage(TAG_FILE_PATH); + storageCleaner = new StorageCleaner(storage); + try { + storageCleaner.storageAutoClean(); + logCleaner.logAutoClean(); + activityList = storage.createActivityList(); + } catch (IOException e) { + Ui.printDivider("Error while preparing application.\n" + + "If any data files are open, please close them and try again."); + exit(0); + } + + lastShownList.activities.addAll(activityList.activities); + parser.cleaner = storageCleaner; + parser.logcleaner = logCleaner; + parser.tagStorage = tagStorage; + + while (true) { + try { + Command command = parser.parseUserCommands(in); + if (command == null) { + continue; + } + if (ByeCommand.isExit(command)) { + command.executeCommand(activityList); + break; + } + command.executeCommand(activityList); + // This block should theoretically never be entered (if command is empty, it just continues) + // However, you never know.. + } catch (EmptyNameException | ExtraParametersException e) { + Ui.printDivider("Error parsing command. Please try again."); + } + } + } +} diff --git a/src/main/java/jikan/activity/Activity.java b/src/main/java/jikan/activity/Activity.java new file mode 100644 index 000000000..dab89da05 --- /dev/null +++ b/src/main/java/jikan/activity/Activity.java @@ -0,0 +1,155 @@ +package jikan.activity; + +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Iterator; +import java.util.Set; + +/** + * Represents an activity entry with a name and total time spent. + */ + +public class Activity { + private String name; + private Set tags; + private LocalDateTime startTime; + private LocalDateTime endTime; + private Duration duration; + private Duration allocatedTime; + private LocalDate date; + + public static final int MAX_PERCENT = 100; + + /** + * Constructor for a new activity entry. + * @param name represents the name of the activity + * @param startTime the time that the activity first started + * @param tags activity tags + * @param endTime the time that the activity ended + */ + public Activity(String name, LocalDateTime startTime, LocalDateTime endTime, Duration duration, + Set tags, Duration allocatedTime) throws InvalidTimeFrameException, NameTooLongException { + + if (endTime.isBefore(startTime)) { + throw new InvalidTimeFrameException(); + } + + if (name.strip().length() <= 25) { + this.name = name.strip(); + } else { + throw new NameTooLongException(); + } + this.startTime = startTime; + this.tags = tags; + this.endTime = endTime; + this.duration = duration; + this.date = endTime.toLocalDate(); + this.allocatedTime = allocatedTime; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name.strip(); + } + + public Set getTags() { + return tags; + } + + public String getTagsAsString() { + return String.join(",", tags); + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public LocalDate getDate() { + return date; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public Duration getAllocatedTime() { + return this.allocatedTime; + } + + public void setAllocatedTime(Duration allocatedTime) { + this.allocatedTime = allocatedTime; + } + + /** + * Gets the percentage completed of the allocated time. + * @return percent completed + */ + public double getProgressPercent() { + double percent = ((double)this.duration.toMillis() / this.allocatedTime.toMillis()) * MAX_PERCENT; + return Math.min(percent, 100); + } + + /** + * Returns true if the Activity's date is within the date range specified (inclusive). + * + * @param startDate Start date of range + * @param endDate End date of range + * @return True if Activity is within date range; false otherwise + */ + public boolean isWithinDateFrame(LocalDate startDate, LocalDate endDate) { + if (!this.date.isBefore(startDate) && !this.date.isAfter(endDate)) { + return true; + } + return false; + } + + /** + * Converts the jikan.activity.Activity object to data representation to be stored in a data file. + * File format: + * name, startTime, endTime + * + * @return String representing the Task object in comma-separated data format. + */ + public String toData() { + + // Convert tags to a single space-separated + String tagString = ""; + tagString = tagsToString(tagString); + + String dataLine = (this.name + "," + this.startTime + "," + this.endTime + "," + + this.duration.toString() + "," + this.allocatedTime + "," + tagString); + return dataLine; + } + + private String tagsToString(String tagString) { + Iterator i = this.tags.iterator(); + + while (i.hasNext()) { + tagString += i.next() + " "; + } + return tagString; + } +} \ No newline at end of file diff --git a/src/main/java/jikan/activity/ActivityList.java b/src/main/java/jikan/activity/ActivityList.java new file mode 100644 index 000000000..ed159bc19 --- /dev/null +++ b/src/main/java/jikan/activity/ActivityList.java @@ -0,0 +1,252 @@ +package jikan.activity; + +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.parser.Parser; +import jikan.storage.Storage; +import jikan.storage.StorageHandler; +import jikan.ui.Ui; + +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Scanner; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.io.File; +import java.io.FileNotFoundException; + +/** + * Represents the list of activities. + */ +public class ActivityList { + public ArrayList activities; + public Storage storage; // Storage the list was loaded from + public StorageHandler storageHandler; + + /** + * Constructor for a new activity list. + */ + public ActivityList() { + this.activities = new ArrayList<>(); + } + + /** + * Constructor for a new activity list to be saved to a file. + * @param storage the storage object to use. + */ + public ActivityList(Storage storage) { + assert storage != null : "Input Storage must not be a null pointer"; + this.activities = new ArrayList<>(); + this.storage = storage; + this.storageHandler = new StorageHandler(storage); + } + + /** + * Loads activityList from data file. + * @param storage the storage object to use. + * @param dataFile the datafile to be read from. + */ + public ActivityList(Storage storage, File dataFile) { + assert storage != null : "Input Storage must not be a null pointer"; + this.activities = new ArrayList<>(); + this.storage = storage; + this.storageHandler = new StorageHandler(storage); + populateTaskList(dataFile); + } + + public Activity get(int i) { + return activities.get(i); + } + + /** + * Adds activity to activity list and stores it in the data file. + * @param activity Activity to add. + */ + public void add(Activity activity) { + activities.add(activity); + String dataLine = activity.toData(); + updateFile(dataLine); + } + + /** + * Updates the duration of an activity. + * @param duration The new duration. + * @param endTime Thew new end time. + * @param index Index of the activity to be updated. + */ + public void updateDuration(Duration duration, LocalDateTime endTime, int index) { + activities.get(index).setDuration(duration); + activities.get(index).setEndTime(endTime); + fieldChangeUpdateFile(); + } + + /** + * Searches for an activity in activityList by name. + * @param name Name of the activity to search for. + * @return Index of activity with that name. + */ + public int findActivity(String name) { + for (int i = 0; i < activities.size(); i++) { + if (activities.get(i).getName().equals(name)) { + return i; + } + } + return -1; + } + + /** + * Updates data file with new task. + * + * @param dataLine Line to write to file. + */ + private void updateFile(String dataLine) { + try { + storage.writeToFile(dataLine); + } catch (IOException e) { + Ui.printDivider("Error saving task to data file.\n" + + "Your changes have not been saved in the data file.\n" + + "If the data file is open, please close it, restart the app and try again."); + } + } + + public void updateName(int index, String newName) { + activities.get(index).setName(newName); + fieldChangeUpdateFile(); + } + + public void updateAlloc(int index, Duration newAllocTime) { + activities.get(index).setAllocatedTime(newAllocTime); + fieldChangeUpdateFile(); + } + + public void delete(int index) { + activities.remove(index); + deleteUpdateFile(index); + } + + /** + * Deletes the line in the file. + * @param index the index of the line in the file. + */ + public void deleteUpdateFile(int index) { + try { + storageHandler.removeLine(index, storage); + } catch (IOException e) { + Ui.printDivider("Error while deleting activity from data file.\n" + + "Your changes have not been saved in the data file.\n" + + "If the data file is open, please close it, restart the app and try again."); + } + } + + private void fieldChangeUpdateFile() { + try { + storageHandler.updateField(activities, storage); + } catch (IOException e) { + Ui.printDivider("Error while updating activity from data file.\n" + + "Your changes have not been saved in the data file.\n" + + "If the data file is open, please close it, restart the app and try again."); + } + } + + public int getSize() { + return activities.size(); + } + + /** + * Saves a new activity to the list of activities. + * @throws InvalidTimeFrameException if start time is before end time + */ + public void saveActivity() throws InvalidTimeFrameException, NameTooLongException { + if (Parser.continuedIndex != -1) { + Ui.printDivider(Parser.activityName + " was ended."); + Parser.endTime = LocalDateTime.now(); + Duration duration = Duration.between(Parser.startTime, Parser.endTime); + Duration oldDuration = this.get(Parser.continuedIndex).getDuration(); + Duration newDuration = duration.plus(oldDuration); + Duration allocatedTime = this.get(Parser.continuedIndex).getAllocatedTime(); + this.updateDuration(newDuration, Parser.endTime, Parser.continuedIndex); + + if (allocatedTime != Duration.parse("PT0S")) { + Ui.printProgressMessage(this.get(Parser.continuedIndex).getProgressPercent()); + } + Parser.continuedIndex = -1; + Parser.resetInfo(); + assert (Parser.tags == null); + assert (Parser.activityName == null); + assert (Parser.startTime == null); + + } else { + Ui.printDivider(Parser.activityName + " was ended."); + Parser.endTime = LocalDateTime.now(); + Duration duration = Duration.between(Parser.startTime, Parser.endTime); + Activity newActivity = new Activity(Parser.activityName, Parser.startTime, + Parser.endTime, duration, Parser.tags, Parser.allocatedTime); + this.add(newActivity); + + if (newActivity.getAllocatedTime() != Duration.parse("PT0S")) { + Ui.printProgressMessage(newActivity.getProgressPercent()); + } + // reset activity info + Parser.resetInfo(); + assert (Parser.tags == null); + assert (Parser.activityName == null); + assert (Parser.startTime == null); + } + } + + + /** + * Populates task list from file. + * + * @param dataFile Data file to populate from. + */ + private void populateTaskList(File dataFile) { + try { + Scanner dataScanner = new Scanner(dataFile); + while (dataScanner.hasNext()) { + parseDataLine(dataScanner.nextLine()); + } + } catch (FileNotFoundException e) { + Ui.printDivider("Error: data file not found. Could not load into the current session's task list."); + } catch (InvalidTimeFrameException e) { + Ui.printDivider("Error: Invalid time frame."); + } catch (NameTooLongException e) { + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + } + + /** + * Parses the current line in the data file to an jikan.activity.Activity object. + * + * @param s String to parse. + */ + private void parseDataLine(String s) throws InvalidTimeFrameException, NameTooLongException { + + if (!s.isEmpty()) { + List strings = Arrays.asList(s.split(",")); + String[] tagStrings; + Set tags = new HashSet(); + + // if there are tags + if (strings.size() > 5) { + // remove square brackets surrounding tags + tagStrings = strings.get(5).split(" "); + for (String i : tagStrings) { + tags.add(i); + } + } + + Activity e; + LocalDateTime startTime = LocalDateTime.parse(strings.get(1)); + LocalDateTime endTime = LocalDateTime.parse(strings.get(2)); + Duration duration = Duration.parse(strings.get(3)); + Duration allocatedTime = Duration.parse(strings.get(4)); + e = new Activity(strings.get(0), startTime, endTime, duration, tags, allocatedTime); + activities.add(e); + } + } +} diff --git a/src/main/java/jikan/cleaner/Cleaner.java b/src/main/java/jikan/cleaner/Cleaner.java new file mode 100644 index 000000000..70b63c814 --- /dev/null +++ b/src/main/java/jikan/cleaner/Cleaner.java @@ -0,0 +1,132 @@ +package jikan.cleaner; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Scanner; + +public abstract class Cleaner { + protected String statusFilePath; + protected String dataFilePath; + protected File status; + protected File recycledData; + /** toClean acts as a switch to switch on/off the cleaner. */ + public boolean toClean; + private static final int DEFAULT_LINES_TO_CLEAN = 5; + + /** + * Initialise a data file containing the deleted logs. + */ + protected void initialiseDataFile() { + try { + loadFile(recycledData); + } catch (IOException e) { + System.out.println("Error loading/creating recycled file"); + } + } + + /** + * Activates/De-activates the auto clean up by checking the status file. + * @return the number of lines of data to automatically clean. + */ + protected int initialiseCleaner() { + try { + return getDataFromStatusFile(); + } catch (IOException e) { + System.out.println("Error loading/creating cleaning file."); + return -1; + } catch (NumberFormatException e) { + System.out.println("Error with loading status file. Please delete status file."); + return -1; + } + } + + /** + * Scans the status file if it exists to initialise toClean and get data on + * the number of lines to clean. + * @return the number of lines of data to automatically clean. + * @throws IOException if status file could not be loaded/created. + */ + private int getDataFromStatusFile() throws IOException, NumberFormatException { + if (loadCleaner(status)) { + Scanner sc = new Scanner(status); + String status = sc.nextLine(); + int value = Integer.parseInt(status); + assert value == 0 || value == 1; + if (value == 1) { + this.toClean = true; + } else { + this.toClean = false; + } + String line = sc.nextLine(); + return Integer.parseInt(line); + } else { + FileWriter fw = new FileWriter(status); + fw.write("0" + "\n"); + fw.write("5" + "\n"); + fw.close(); + return DEFAULT_LINES_TO_CLEAN; + } + } + + /** + * Loads the status file and checks if it exists or not. + * @param file status file. + * @return true if the file exists and false otherwise. + * @throws IOException if there is an error with the creation/loading of the status file. + */ + protected boolean loadCleaner(File file) throws IOException { + if (!file.exists()) { + createFile(file); + return false; + } else { + return true; + } + } + + /** + * Loads the data file that contains deleted logs. + * @param file data file with the deleted logs. + * @throws IOException if there is an error with the creation/loading of the data file. + */ + protected void loadFile(File file) throws IOException { + if (!file.exists()) { + createFile(file); + } + } + + /** + * Creates a new file if the specified file cannot be found in the given path. + * @param file the file to be created if it does not exist. + * @throws IOException if there is an error with the creation of the file. + */ + protected void createFile(File file) throws IOException { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + + + /** + * Method to activate/de-activate the auto cleanup. + * @param status a boolean specifying whether the cleaner should be activated or not. + * @param number an integer specifying the number of lines of data to automatically clean. + * @throws IOException if there is an error with reading/writing to the status file. + */ + public void setStatus(boolean status, int number) throws IOException { + this.toClean = status; + File dataFile = new File(statusFilePath); + if (!dataFile.exists()) { + dataFile.createNewFile(); + } + BufferedWriter writer = new BufferedWriter(new FileWriter(dataFile)); + if (this.toClean) { + writer.write("1" + "\n"); + } else { + writer.write("0" + "\n"); + } + writer.write(number + "\n"); + writer.close(); + } + +} diff --git a/src/main/java/jikan/cleaner/LogCleaner.java b/src/main/java/jikan/cleaner/LogCleaner.java new file mode 100644 index 000000000..18932a537 --- /dev/null +++ b/src/main/java/jikan/cleaner/LogCleaner.java @@ -0,0 +1,115 @@ +package jikan.cleaner; + +import jikan.log.Log; +import jikan.ui.Ui; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + + +/** + * A log cleaner class that does automated cleaning + * for log files under the user's request. + */ +public class LogCleaner extends Cleaner { + + private static final String LOG_FILE_PATH = "data/LogRecord.txt"; + public int numberOfLogsToClean; + + + /** + * Constructor for the log cleaner. + */ + public LogCleaner() { + super.statusFilePath = "data/recycled/logStatus.txt"; + super.status = new File(statusFilePath); + super.dataFilePath = "data/recycled/logData.txt"; + super.recycledData = new File(dataFilePath); + initialiseDataFile(); + int value = initialiseCleaner(); + if (value != -1) { + this.numberOfLogsToClean = value; + } else { + Log.makeInfoLog("Problem initialising cleaner"); + Ui.printDivider("There is a problem initialising cleaner, please remove the status file"); + } + } + + /** + * Method to set a value for the number of logs to clean. + * @param value an integer representing the number to clean. + * @throws IOException if there is an error with reading/writing to the status file. + */ + public void setNumberOfLogsToClean(int value) throws IOException { + boolean status = this.toClean; + File dataFile = new File(statusFilePath); + this.numberOfLogsToClean = value; + if (!dataFile.exists()) { + dataFile.createNewFile(); + } + BufferedWriter writer = new BufferedWriter(new FileWriter((dataFile))); + if (status) { + writer.write("1" + "\n"); + } else { + writer.write("0" + "\n"); + } + writer.write(value + "\n"); + writer.close(); + } + + /** + * Method to clear up the live log file and move them to the recycled log file. + * @throws IOException if there is an error with reading/writing to the live log file and + * recycled log file. + */ + public void logAutoClean() throws IOException { + List logsForRecycling = new ArrayList<>(); + List logsLeftInData = new ArrayList<>(); + if (this.toClean) { + File liveData = recycleLog(logsForRecycling, logsLeftInData); + BufferedWriter recycledDataWriter = new BufferedWriter(new FileWriter(recycledData)); + for (String line : logsForRecycling) { + recycledDataWriter.write(line + "\n"); + } + recycledDataWriter.close(); + BufferedWriter liveDataWriter = new BufferedWriter(new FileWriter(liveData)); + for (String line : logsLeftInData) { + liveDataWriter.write(line + "\n"); + } + liveDataWriter.close(); + } + } + + /** + * Method to clean up log file and move them to the recycled folder. + * @param logsForRecycling an array list consisting of logs to be written to recycled folder. + * @param logsLeftInData an array list consisting of the logs left after clean up. + * @return a log file that holds all the logging at run time. + * @throws FileNotFoundException if file could not be found at the filepath. + */ + private File recycleLog(List logsForRecycling, List logsLeftInData) throws IOException { + File liveData = new File(LOG_FILE_PATH); + Scanner recycledDataScanner = new Scanner(recycledData); + Scanner liveDataScanner = new Scanner(liveData); + while (recycledDataScanner.hasNext()) { + String line = recycledDataScanner.nextLine(); + logsForRecycling.add(line); + } + while (numberOfLogsToClean != 0 && liveDataScanner.hasNext()) { + String line = liveDataScanner.nextLine(); + logsForRecycling.add(line); + numberOfLogsToClean -= 1; + } + while (liveDataScanner.hasNext()) { + String line = liveDataScanner.nextLine(); + logsLeftInData.add(line); + } + return liveData; + } +} diff --git a/src/main/java/jikan/cleaner/StorageCleaner.java b/src/main/java/jikan/cleaner/StorageCleaner.java new file mode 100644 index 000000000..43e20368b --- /dev/null +++ b/src/main/java/jikan/cleaner/StorageCleaner.java @@ -0,0 +1,146 @@ +package jikan.cleaner; + +import jikan.log.Log; +import jikan.storage.Storage; +import jikan.ui.Ui; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * A storage cleaner class that does automated cleaning + * for data files under the user's request. + */ +public class StorageCleaner extends Cleaner { + + public int numberOfActivitiesToClean; + public Storage storage; + + /** + * Constructor for the storage cleaner. + * @param storage an object that holds the data on the list of activities. + */ + public StorageCleaner(Storage storage) { + this.storage = storage; + super.statusFilePath = "data/recycled/status.txt"; + super.status = new File(statusFilePath); + super.dataFilePath = "data/recycled/data.csv"; + super.recycledData = new File(dataFilePath); + initialiseDataFile(); + int value = initialiseCleaner(); + if (value != -1) { + this.numberOfActivitiesToClean = value; + } else { + Log.makeInfoLog("Problem initialising cleaner"); + Ui.printDivider("There is a problem initialising cleaner, please remove the status file"); + } + } + + /** + * Method to set a value for the number of activities to clean. + * @param value an integer representing the number to clean. + * @throws IOException if there is an error with reading/writing to the status file. + */ + public void setNumberOfActivitiesToClean(int value) throws IOException { + boolean status = this.toClean; + File dataFile = new File(statusFilePath); + this.numberOfActivitiesToClean = value; + if (!dataFile.exists()) { + dataFile.createNewFile(); + } + BufferedWriter writer = new BufferedWriter(new FileWriter(dataFile)); + if (status) { + writer.write("1" + "\n"); + } else { + writer.write("0" + "\n"); + } + writer.write(value + "\n"); + writer.close(); + } + + /** + * Method to clear up the live data file and move them to the recycled data file. + * @throws IOException if there is an error with reading/writing to the live data file and + * recycled data file. + */ + public void storageAutoClean() throws IOException { + List activitiesForRecycling = new ArrayList<>(); + List activitiesLeftInData = new ArrayList<>(); + if (this.toClean) { + File liveData = recycleData(activitiesForRecycling, activitiesLeftInData); + BufferedWriter recycledDataWriter = new BufferedWriter(new FileWriter(recycledData)); + for (String line : activitiesForRecycling) { + recycledDataWriter.write(line + "\n"); + } + recycledDataWriter.close(); + BufferedWriter liveDataWriter = new BufferedWriter(new FileWriter(liveData)); + for (String line : activitiesLeftInData) { + liveDataWriter.write(line + "\n"); + } + liveDataWriter.close(); + } + } + + /** + * Method to clean up data file and move them to the recycled folder. + * @param listForRecycling an array list consisting of data to be written to recycled folder. + * @param listLeftInData an array list consisting of the data left after clean up. + * @return a storage file that holds the activity list at run time. + * @throws FileNotFoundException if file could not be found at the filepath. + */ + private File recycleData(List listForRecycling, List listLeftInData) throws IOException { + String filePath = storage.dataFilePath; + File liveData = new File(filePath); + Scanner recycledDataScanner = new Scanner(recycledData); + Scanner liveDataScanner = new Scanner(liveData); + while (recycledDataScanner.hasNext()) { + String line = recycledDataScanner.nextLine(); + listForRecycling.add(line); + } + parseLiveData(listForRecycling, listLeftInData, liveDataScanner); + return liveData; + } + + /** + * Method to check the activities in storage line by line and see if they should be cleaned or not. + * @param listForRecycling an array list consisting of data to be written to recycled folder. + * @param listLeftInData an array list consisting of the data left after clean up. + * @param liveDataSc used to scan the activities in storage line by line. + */ + private void parseLiveData(List listForRecycling, List listLeftInData, Scanner liveDataSc) { + cleanUpActivities(listForRecycling, listLeftInData, liveDataSc); + while (liveDataSc.hasNext()) { + String line = liveDataSc.nextLine(); + listLeftInData.add(line); + } + } + + /** + * Checks and clean up completed activities. + * @param listForRecycling an array list consisting of data to be written to recycled folder. + * @param listLeftInData an array list consisting of the data left after clean up. + * @param liveDataSc used to scan the activities in storage line by line. + */ + private void cleanUpActivities(List listForRecycling, List listLeftInData, Scanner liveDataSc) { + while (numberOfActivitiesToClean != 0 && liveDataSc.hasNext()) { + String line = liveDataSc.nextLine(); + String[] tokenizedLine = line.split(","); + Duration duration = Duration.parse(tokenizedLine[3]); + Duration allocatedTime = Duration.parse(tokenizedLine[4]); + int result = duration.compareTo(allocatedTime); + if (result >= 0 && allocatedTime != Duration.parse("PT0S")) { + listForRecycling.add(line); + numberOfActivitiesToClean -= 1; + } else { + listLeftInData.add(line); + } + } + } +} diff --git a/src/main/java/jikan/command/AbortCommand.java b/src/main/java/jikan/command/AbortCommand.java new file mode 100644 index 000000000..25ed797e5 --- /dev/null +++ b/src/main/java/jikan/command/AbortCommand.java @@ -0,0 +1,36 @@ +package jikan.command; + +import jikan.log.Log; +import jikan.activity.ActivityList; +import jikan.exception.NoSuchActivityException; +import jikan.parser.Parser; +import jikan.ui.Ui; + +/** + * Represents a command to abort a currently running activity. + */ +public class AbortCommand extends Command { + + /** + * Constructor to create a new abort command. + */ + public AbortCommand(String parameters) { + super(parameters); + } + + @Override + public void executeCommand(ActivityList activityList) { + try { + if (Parser.startTime == null) { + throw new NoSuchActivityException(); + } else { + Parser.resetInfo(); + String line = "You have aborted the current activity."; + Ui.printDivider(line); + } + } catch (NoSuchActivityException e) { + Ui.printDivider("You have not started any activity."); + Log.makeInfoLog("Abort command failed as no activity was ongoing"); + } + } +} diff --git a/src/main/java/jikan/command/ByeCommand.java b/src/main/java/jikan/command/ByeCommand.java new file mode 100644 index 000000000..aaa0526c9 --- /dev/null +++ b/src/main/java/jikan/command/ByeCommand.java @@ -0,0 +1,59 @@ +package jikan.command; + +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.activity.ActivityList; +import jikan.exception.InvalidTimeFrameException; +import jikan.parser.Parser; +import jikan.ui.Ui; + +import java.util.Scanner; + + + +/** + * Terminates the program. + */ +public class ByeCommand extends Command { + + /** + * Constructor to create a new exit command. + */ + public ByeCommand(String parameters) { + super(parameters); + } + + /** + * Exits the app. If there is a ongoing activity, asks the user if the activity + * should be saved. + */ + @Override + public void executeCommand(ActivityList activityList) { + + try { + // checks if there was an ongoing activity + if (Parser.startTime != null) { + String line = Parser.activityName + " is still running! If you exit now it will be aborted.\n" + + "Would you like to end this activity to save it?"; + Ui.printDivider(line); + Scanner scanner = new Scanner(System.in); + String userInput = scanner.nextLine(); + if (userInput.equalsIgnoreCase("yes") || userInput.equalsIgnoreCase("y")) { + activityList.saveActivity(); + } + } + Ui.exitFromApp(); + } catch (InvalidTimeFrameException e) { + Log.makeInfoLog("End date must be before start date"); + Ui.printDivider("Error: end date must be before start date."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + } + + public static boolean isExit(Command command) { + return command instanceof ByeCommand; // instanceof returns false if it is null + } + +} diff --git a/src/main/java/jikan/command/CleanCommand.java b/src/main/java/jikan/command/CleanCommand.java new file mode 100644 index 000000000..976d838d4 --- /dev/null +++ b/src/main/java/jikan/command/CleanCommand.java @@ -0,0 +1,354 @@ +package jikan.command; + +import jikan.log.Log; +import jikan.activity.ActivityList; +import jikan.exception.NegativeNumberException; +import jikan.exception.InvalidCleanCommandException; +import jikan.cleaner.LogCleaner; +import jikan.cleaner.StorageCleaner; +import jikan.ui.Ui; + +import java.io.IOException; + +/** + * Represents a command to clear previously saved log files. + */ +public class CleanCommand extends Command { + + StorageCleaner storageCleaner; + LogCleaner logCleaner; + private boolean toCleanStorage; + private boolean toCleanLog; + + /** + * Constructor to create a new clean command. + */ + public CleanCommand(String parameters, StorageCleaner cleaner, LogCleaner logCleaner) { + super(parameters); + this.storageCleaner = cleaner; + this.logCleaner = logCleaner; + } + + @Override + public void executeCommand(ActivityList activityList) { + boolean isEmpty = isParameterEmpty(); + if (isEmpty) { + stopExecution(); + } else { + continueExecution(); + } + } + + /** + * Method to check if user input is empty for clean command. + * @return true if is empty and false otherwise. + */ + public boolean isParameterEmpty() { + String parametersTrimmed = this.parameters.trim(); + return parametersTrimmed.isEmpty(); + } + + /** + * Stops execution when user input is empty. + */ + private void stopExecution() { + Log.makeInfoLog("Clean command received empty parameters"); + Ui.printDivider("No valid parameters to clean command found."); + } + + /** + * Method to continue execution given a non empty user input. + */ + private void continueExecution() { + toCleanLog = false; + toCleanStorage = false; + String parametersTrimmed = this.parameters.trim(); + assert !parametersTrimmed.isEmpty(); + try { + String firstWord = getFirstWord(parametersTrimmed); + processCommand(firstWord); + } catch (InvalidCleanCommandException e) { + Log.makeInfoLog("Invalid clean command"); + Ui.printDivider("Invalid format received for clean command."); + } + + } + + /** + * Method to extract first word for a given line. + * @param line a string representing a line of information. + * @return the first word of the line. + */ + public String getFirstWord(String line) { + int delimiter = line.indexOf(" "); + String word; + if (delimiter == -1) { + return line; + } else { + word = line.substring(0, delimiter); + return word; + } + } + + /** + * Method that parses in a word and remove the word from the this.parameters string. + * @param word a word to remove. + * @return a this.parameters string without the word. + */ + public String getRemainingParameter(String word) { + int index = this.parameters.indexOf(word); + int number = word.length(); + String remainingParameter = this.parameters.substring(index + number); + remainingParameter = remainingParameter.trim(); + return remainingParameter; + } + + /** + * Check if we are dealing with storage or log cleaner. + * @param firstWord indicative of whether the user is dealing with logs or storage. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void processCommand(String firstWord) throws InvalidCleanCommandException { + assert !toCleanLog && !toCleanStorage; + if (firstWord.equals("log")) { + toCleanLog = true; + processLogCommand(); + } else { + toCleanStorage = true; + handleCase(firstWord); + } + } + + /** + * Handle the different functions on a case by case basis. + * @param firstWord indicative of the function being called. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void handleCase(String firstWord) throws InvalidCleanCommandException { + switch (firstWord) { + case "on": + handleOnFunction(); + break; + case "off": + handleOffFunction(); + break; + case "/n": + handleSetFunction(); + break; + default: + throw new InvalidCleanCommandException(); + } + } + + /** + * Use to process log commands, thus all the function called in this method + * will only deal with log cleaner. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void processLogCommand() throws InvalidCleanCommandException { + String remainingCommand = getRemainingParameter("log"); + if (remainingCommand.isEmpty()) { + throw new InvalidCleanCommandException(); + } else { + String firstWord = getFirstWord(remainingCommand); + handleCase(firstWord); + } + } + + /** + * A general purpose function to handle the "on" command. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void handleOnFunction() throws InvalidCleanCommandException { + String remainingParameter = getRemainingParameter("on"); + if (remainingParameter.isEmpty()) { + setStatusOn(); + } else { + throw new InvalidCleanCommandException(); + } + } + + /** + * Forwards the "on" command to the appropriate cleaner (either storage or log cleaner). + */ + private void setStatusOn() { + assert !toCleanStorage || !toCleanLog; + if (toCleanStorage) { + setStorageCleanerOn(); + } else if (toCleanLog) { + setLogCleanerOn(); + } + } + + /** + * Method to switch on the storage cleaner. + */ + private void setStorageCleanerOn() { + try { + storageCleaner.setStatus(true, storageCleaner.numberOfActivitiesToClean); + } catch (IOException e) { + Ui.printDivider("Error in loading/writing to storage status file"); + Log.makeInfoLog("Error in accessing the storage status file"); + } + assert storageCleaner.toClean; + Ui.printDivider("Auto cleaning enabled for storage."); + Log.makeInfoLog("User has turned on automated cleaning for storage."); + } + + /** + * Method to switch on the log cleaner. + */ + private void setLogCleanerOn() { + try { + logCleaner.setStatus(true, logCleaner.numberOfLogsToClean); + } catch (IOException e) { + Ui.printDivider("Error in loading/writing to log status file"); + Log.makeInfoLog("Error in accessing the log status file"); + } + assert logCleaner.toClean; + Ui.printDivider("Auto cleaning enabled for logs."); + Log.makeInfoLog("User has turned on automated cleaning for logs."); + } + + /** + * A general purpose function to handle the "off" command. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void handleOffFunction() throws InvalidCleanCommandException { + String remainingParameter = getRemainingParameter("off"); + if (remainingParameter.isEmpty()) { + setStatusOff(); + } else { + throw new InvalidCleanCommandException(); + } + } + + /** + * Forwards the "off" command to the appropriate cleaner (either storage or log cleaner). + */ + private void setStatusOff() { + assert !toCleanStorage || !toCleanLog; + if (toCleanStorage) { + setStorageCleanerOff(); + } else if (toCleanLog) { + setLogCleanerOff(); + } + } + + /** + * Method to switch off the storage cleaner. + */ + private void setStorageCleanerOff() { + try { + storageCleaner.setStatus(false, storageCleaner.numberOfActivitiesToClean); + } catch (IOException e) { + Ui.printDivider("Error in loading/writing to storage status file"); + Log.makeInfoLog("Error in accessing the storage status file"); + } + assert !storageCleaner.toClean; + Ui.printDivider("Auto cleaning disabled for storage."); + Log.makeInfoLog("User has turned off automated cleaning for storage."); + } + + /** + * Method to switch off the log cleaner. + */ + private void setLogCleanerOff() { + try { + logCleaner.setStatus(false, logCleaner.numberOfLogsToClean); + } catch (IOException e) { + Ui.printDivider("Error in loading/writing to log status file"); + Log.makeInfoLog("Error in accessing the log status file"); + } + assert !logCleaner.toClean; + Ui.printDivider("Auto cleaning disabled for logs."); + Log.makeInfoLog("User has turned off automated cleaning for logs."); + } + + /** + * A general purpose function to handle the "/n" command. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void handleSetFunction() throws InvalidCleanCommandException { + String remainingParameter = getRemainingParameter("/n"); + if (remainingParameter.isEmpty()) { + throw new InvalidCleanCommandException(); + } else { + setValue(remainingParameter); + } + } + + /** + * Forwards the "/n" command to the appropriate cleaner (either storage or log cleaner). + * @param remainingParameter a string with information on the value to set to. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void setValue(String remainingParameter) throws InvalidCleanCommandException { + assert !toCleanStorage || !toCleanLog; + if (toCleanStorage) { + setValueForStorage(remainingParameter); + } else if (toCleanLog) { + setValueForLogs(remainingParameter); + } + } + + /** + * Method to set a value for storage cleaner. + * @param remainingParameter a string with information on the value to set to. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void setValueForStorage(String remainingParameter) throws InvalidCleanCommandException { + try { + int value = getNumber(remainingParameter); + storageCleaner.setNumberOfActivitiesToClean(value); + Ui.printDivider("Number of activities to clean is set to " + value); + Log.makeInfoLog("Storage Cleaner set to " + value); + } catch (NegativeNumberException e) { + Ui.printDivider("Please provide a positive number."); + Log.makeInfoLog("Negative number given in clean command"); + } catch (IOException e) { + Ui.printDivider("Error in loading/writing to status file"); + Log.makeInfoLog("Error in accessing the status file"); + } + } + + /** + * Method to set a value for log cleaner. + * @param remainingParameter a string with information on the value to set to. + * @throws InvalidCleanCommandException when the format of user input is wrong. + */ + private void setValueForLogs(String remainingParameter) throws InvalidCleanCommandException { + try { + int value = getNumber(remainingParameter); + logCleaner.setNumberOfLogsToClean(value); + Ui.printDivider("Number of logs to clean is set to " + value); + Log.makeInfoLog("Log Cleaner set to " + value); + } catch (NegativeNumberException e) { + Ui.printDivider("Please provide a positive number."); + Log.makeInfoLog("Negative number given in clean command"); + } catch (IOException e) { + Ui.printDivider("Error in loading/writing to status file"); + Log.makeInfoLog("Error in accessing the status file"); + } + } + + /** + * Method to convert the parameter numberString to an integer. + * @param numberString a string that represents the value to set to. + * @return an integer numberString. + * @throws InvalidCleanCommandException when the format of user input is wrong. + * @throws NegativeNumberException when the numberString is negative. + */ + public int getNumber(String numberString) throws InvalidCleanCommandException, NegativeNumberException { + try { + int value = Integer.parseInt(numberString); + if (value < 0) { + throw new NegativeNumberException(); + } else { + return value; + } + } catch (NumberFormatException e) { + throw new InvalidCleanCommandException(); + } + } +} diff --git a/src/main/java/jikan/command/Command.java b/src/main/java/jikan/command/Command.java new file mode 100644 index 000000000..262b8e685 --- /dev/null +++ b/src/main/java/jikan/command/Command.java @@ -0,0 +1,29 @@ +package jikan.command; + +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; + + +/** + * Represents an executable command. + */ +public abstract class Command { + protected String parameters; + + /** + * Constructor to create a new command. + */ + public Command(String parameters) { + this.parameters = parameters; + } + + /** + * Executes the command and returns the result. + */ + public abstract void executeCommand(ActivityList activityList) throws EmptyNameException, ExtraParametersException; + + +} + diff --git a/src/main/java/jikan/command/ContinueCommand.java b/src/main/java/jikan/command/ContinueCommand.java new file mode 100644 index 000000000..3ae3a74f3 --- /dev/null +++ b/src/main/java/jikan/command/ContinueCommand.java @@ -0,0 +1,57 @@ +package jikan.command; + +import jikan.log.Log; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.NoSuchActivityException; +import jikan.parser.Parser; +import jikan.ui.Ui; + +import java.time.LocalDateTime; + +/** + * Represents a command to continue recording an existing activity. + */ +public class ContinueCommand extends Command { + + /** + * Constructor to create a new continue command. + */ + public ContinueCommand(String parameters) { + super(parameters.strip()); + } + + @Override + public void executeCommand(ActivityList activityList) { + try { + if (Parser.startTime != null) { + Ui.printDivider(Parser.activityName + " is ongoing!"); + Log.makeInfoLog("Could not continue activity due to ongoing activity."); + return; + } + int index = activityList.findActivity(parameters); + if (index != -1) { + // activity is found + Parser.activityName = activityList.get(index).getName(); + Parser.tags = activityList.get(index).getTags(); + Parser.startTime = LocalDateTime.now(); + Parser.continuedIndex = index; + Ui.printDivider(Parser.activityName + " was continued."); + Log.makeInfoLog(Parser.activityName + " was continued."); + } else { + if (parameters.isEmpty()) { + throw new EmptyNameException(); + } else { + throw new NoSuchActivityException(); + } + } + } catch (NoSuchActivityException e) { + Ui.printDivider("No activity with this name exists!"); + Log.makeInfoLog("Continue command failed as there was no such activity saved."); + } catch (EmptyNameException e) { + Ui.printDivider("Activity name cannot be empty!"); + Log.makeInfoLog("Continue command failed as there was no activity name provided."); + } + } +} + diff --git a/src/main/java/jikan/command/DeleteCommand.java b/src/main/java/jikan/command/DeleteCommand.java new file mode 100644 index 000000000..72c36d6a8 --- /dev/null +++ b/src/main/java/jikan/command/DeleteCommand.java @@ -0,0 +1,41 @@ +package jikan.command; + +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.NoSuchActivityException; +import jikan.ui.Ui; + +/** + * Represents a command to delete an activity from the activity list. + */ +public class DeleteCommand extends Command { + + /** + * Constructor to create a new delete command. + */ + public DeleteCommand(String parameters) { + super(parameters.trim()); + } + + @Override + public void executeCommand(ActivityList activityList) { + try { + int index = activityList.findActivity(parameters); + if (index != -1) { + // activity was found + Ui.printDivider("You have deleted " + parameters + "."); + activityList.delete(index); + } else { + if (parameters.isEmpty()) { + throw new EmptyNameException(); + } else { + throw new NoSuchActivityException(); + } + } + } catch (NoSuchActivityException e) { + Ui.printDivider("No activity with this name exists."); + } catch (EmptyNameException e) { + Ui.printDivider("Activity name cannot be empty."); + } + } +} diff --git a/src/main/java/jikan/command/EditCommand.java b/src/main/java/jikan/command/EditCommand.java new file mode 100644 index 000000000..09981ac42 --- /dev/null +++ b/src/main/java/jikan/command/EditCommand.java @@ -0,0 +1,130 @@ +package jikan.command; + +import jikan.activity.ActivityList; +import jikan.exception.NoSuchActivityException; +import jikan.exception.NameTooLongException; +import jikan.exception.ActivityIsRunningException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NegativeDurationException; +import jikan.exception.EmptyNameException; +import jikan.exception.InvalidEditFormatException; +import jikan.exception.ExistingNameException; +import jikan.log.Log; +import jikan.parser.Parser; +import jikan.ui.Ui; + +import java.time.Duration; + +import static jikan.command.GoalCommand.parseDuration; + +/** + * Represents a command to edit an activity in the activity list. + */ +public class EditCommand extends Command { + + /** + * Constructor to create a new edit command. + */ + public EditCommand(String parameters) { + super(parameters); + } + + @Override + public void executeCommand(ActivityList activityList) { + try { + Log.makeInfoLog("Activity could not be edited as there is an ongoing activity"); + if (Parser.startTime != null) { + throw new ActivityIsRunningException(); + } + int delimiter = parameters.indexOf("/en"); + int allocDelim = parameters.indexOf("/ea"); + + String newName = ""; + String tmpAlloc = ""; + Duration newAllocTime = null; + + //edit name + if (delimiter != -1) { + Parser.activityName = parameters.substring(0, delimiter).strip(); + newName = parameters.substring(delimiter + 4).strip(); + if (newName.isEmpty()) { + Ui.printDivider("New activity name cannot be empty."); + throw new EmptyNameException(); + } + if (newName.length() > 25) { + throw new NameTooLongException(); + } + // existing activity of the same name is found + if (activityList.findActivity(newName) != -1) { + throw new ExistingNameException(); + } + //edit allocated time + } else if (allocDelim != -1) { + Parser.activityName = parameters.substring(0, allocDelim).strip(); + tmpAlloc = parameters.substring(allocDelim + 4).strip(); + if (tmpAlloc.isEmpty()) { + throw new InvalidTimeFrameException(); + } + newAllocTime = parseDuration(tmpAlloc); + + //invalid format + } else { + throw new InvalidEditFormatException(); + } + + if (Parser.activityName.isEmpty()) { + Ui.printDivider("Activity name cannot be empty."); + throw new EmptyNameException(); + } + + int index = activityList.findActivity(Parser.activityName); + + if (index != -1) { + if (!(newName.isEmpty() && tmpAlloc.isEmpty())) { + if (!newName.isEmpty()) { + activityList.updateName(index, newName); + } else { + assert newAllocTime != null; + if (newAllocTime.isNegative()) { + throw new NegativeDurationException(); + } else { + activityList.updateAlloc(index, newAllocTime); + } + } + } else { + // no new details provided + throw new InvalidEditFormatException(); + } + Ui.printDivider("Activity named " + Parser.activityName + " has been updated."); + + } else { + // activity is not found + throw new NoSuchActivityException(); + } + } catch (NoSuchActivityException e) { + Ui.printDivider("No activity with this name exists."); + Log.makeInfoLog("Edit command failed as there was no such activity saved."); + } catch (EmptyNameException e) { + Log.makeInfoLog("Edit command failed as there was no activity name provided."); + } catch (StringIndexOutOfBoundsException | ArrayIndexOutOfBoundsException | InvalidEditFormatException e) { + Ui.printDivider("Incorrect edit command format entered."); + Log.makeInfoLog("Edit command failed as an incorrect format was provided."); + } catch (NegativeDurationException e) { + Ui.printDivider("Please enter a positive target time."); + Log.makeInfoLog("Edit command failed as a negative target time was provided."); + } catch (InvalidTimeFrameException e) { + Ui.printDivider("New target time cannot be empty."); + Log.makeInfoLog("Edit command failed as an empty target time was provided"); + } catch (NumberFormatException e) { + Ui.printDivider("Please enter integers in the format HH:MM:SS."); + Log.makeInfoLog("Edit command failed as an incorrect format for the target time was provided."); + } catch (NameTooLongException e) { + Ui.printDivider("Activity name must be shorter than 25 characters."); + } catch (ActivityIsRunningException e) { + Ui.printDivider("Cannot edit an activity if there is a current activity running."); + } catch (ExistingNameException e) { + Ui.printDivider("There is already an activity with that name. "); + } + } + +} diff --git a/src/main/java/jikan/command/EndCommand.java b/src/main/java/jikan/command/EndCommand.java new file mode 100644 index 000000000..3a095ce29 --- /dev/null +++ b/src/main/java/jikan/command/EndCommand.java @@ -0,0 +1,51 @@ +package jikan.command; + +import jikan.exception.ExtraParametersException; +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.activity.ActivityList; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NoSuchActivityException; +import jikan.parser.Parser; +import jikan.ui.Ui; + +import java.util.Scanner; + +import static jikan.Jikan.lastShownList; + +/** + * Represents a command to end an activity. + */ +public class EndCommand extends Command { + + /** + * Constructor to create a new end command. + */ + public EndCommand(String parameters) { + super(parameters); + } + + /** Method to parse the end activity command. */ + @Override + public void executeCommand(ActivityList activityList) { + try { + if (Parser.startTime == null) { + throw new NoSuchActivityException(); + } else { + activityList.saveActivity(); + // reset lastShownList to include new activity + lastShownList.activities.clear(); + lastShownList.activities.addAll(activityList.activities); + } + } catch (NoSuchActivityException e) { + Log.makeInfoLog("End command failed as no activity was ongoing"); + Ui.printDivider("You have not started any activity!"); + } catch (InvalidTimeFrameException e) { + Log.makeInfoLog("End date must be before start date"); + Ui.printDivider("End date must be before start date."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + } +} diff --git a/src/main/java/jikan/command/FilterCommand.java b/src/main/java/jikan/command/FilterCommand.java new file mode 100644 index 000000000..32197e723 --- /dev/null +++ b/src/main/java/jikan/command/FilterCommand.java @@ -0,0 +1,208 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidCommandException; +import jikan.exception.MultipleDelimitersException; +import jikan.exception.EmptyQueryException; +import jikan.ui.Ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static jikan.Jikan.lastShownList; + +/** + * Represents a command to filter activities by specified tags. + */ +public class FilterCommand extends Command { + boolean isFinalCommand; + boolean isChained; + private static final String FILTER = "filter"; + private static final String FIND = "find"; + + /** + * Constructor to create a new filter command. + */ + public FilterCommand(String parameters) throws MultipleDelimitersException { + super(parameters); + isFinalCommand = true; + this.parameters = parameters.replaceAll("\\s+", " "); + this.parameters = parameters.trim(); + if (parameters.contains(";;") || parameters.contains("; ;")) { + throw new MultipleDelimitersException(); + } + } + + /** + * Constructor to create a new filter command with chaining. + */ + public FilterCommand(String parameters, boolean isFinal, boolean hasChaining) throws MultipleDelimitersException { + super(parameters.trim()); + isFinalCommand = isFinal; + isChained = hasChaining; + this.parameters = parameters.replaceAll("\\s+", " "); + this.parameters = parameters.trim(); + if (parameters.contains(";;") || parameters.contains("; ;")) { + throw new MultipleDelimitersException(); + } + } + + /** + * Shows the user all past activities that has tags which match the one or more keywords queried by the user. + * @param activityList the activity list to search for matching activities + */ + @Override + public void executeCommand(ActivityList activityList) { + // remove the magic number later + String[] tokenizedParameters = parameters.split(";", 2); + if (tokenizedParameters.length > 1) { + executeChainedCommand(activityList, tokenizedParameters); + } else { + isFinalCommand = true; + executeSingleCommand(activityList); + } + } + + private void executeChainedCommand(ActivityList activityList, String[] tokenizedParameters) { + if (tokenizedParameters[1].length() > 0) { + isFinalCommand = false; + parameters = tokenizedParameters[0]; + executeSingleCommand(activityList); + String nextCommand = tokenizedParameters[1].trim(); + try { + callNextCommand(nextCommand, activityList); + } catch (InvalidCommandException e) { + Ui.printDivider("Please chain find or filter commands only"); + } + } else { + isFinalCommand = true; + parameters = tokenizedParameters[0]; + searchSubList(); + } + } + + private void executeSingleCommand(ActivityList activityList) { + if (parameters.contains("-s") || isChained == true) { + searchSubList(); + } else { + searchFullList(activityList); + } + } + + private void callNextCommand(String userInput, ActivityList activityList) throws InvalidCommandException { + String[] tokenizedInputs = userInput.split(" ", 2); + String instruction = tokenizedInputs[0]; + Command command = null; + switch (instruction) { + case FIND: + try { + command = new FindCommand(tokenizedInputs[1], false, true); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + } + break; + case FILTER: + try { + command = new FilterCommand(tokenizedInputs[1], false, true); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + } + break; + default: + throw new InvalidCommandException(); + } + + try { + command.executeCommand(activityList); + } catch (EmptyNameException | ExtraParametersException e) { + Ui.printDivider("Error parsing command. Please try again."); + } + } + + /** + * Filters activities by tags from the entire list of activities. + * @param activityList the full list of activities + */ + private void searchFullList(ActivityList activityList) { + try { + String query = parameters; + if (query.length() < 1) { + throw new EmptyQueryException(); + } else { + lastShownList.activities.clear(); + String[] keywords = query.split(" "); + keywords = removeBlanks(keywords); + //keywords = keywords.filter(boolean); + if (keywords.length < 1) { + throw new EmptyQueryException(); + } + for (String keyword : keywords) { + populateLastShownList(activityList, lastShownList, keyword); + } + callPrintResults(); + } + } catch (EmptyQueryException e) { + Ui.printDivider("No keyword was given."); + } + } + + /** + * Filter activities by tags based on the last shown list. + */ + private void searchSubList() { + try { + String query = parameters.replace("-s ", ""); + ActivityList prevList = new ActivityList(); + prevList.activities.addAll(lastShownList.activities); + if (query.length() < 1) { + throw new EmptyQueryException(); + } else { + lastShownList.activities.clear(); + String[] keywords = query.split(" "); + if (keywords.length < 1) { + throw new EmptyQueryException(); + } + for (String keyword : keywords) { + populateLastShownList(prevList, lastShownList, keyword); + } + callPrintResults(); + } + } catch (ArrayIndexOutOfBoundsException | EmptyQueryException e) { + Ui.printDivider("No keyword was given."); + } + } + + private void callPrintResults() { + if (isFinalCommand == true) { + Ui.printResults(lastShownList); + } + } + + private void populateLastShownList(ActivityList targetList, ActivityList lastShownList, String keyword) { + for (Activity i : targetList.activities) { + // if (!lastShownList.activities.contains(i) && i.getTags().contains(keyword)) { + if (!lastShownList.activities.contains(i) && containsIgnoreCase(i.getTagsAsString(), keyword)) { + lastShownList.activities.add(i); + } + } + } + + private boolean containsIgnoreCase(String str, String subString) { + return str.toLowerCase().contains(subString.toLowerCase()); + } + + private String[] removeBlanks(String [] strings) { + List list = new ArrayList<>(Arrays.asList(strings)); + list.removeIf(String::isBlank); + strings = list.toArray(new String[0]); + return strings; + } +} diff --git a/src/main/java/jikan/command/FindCommand.java b/src/main/java/jikan/command/FindCommand.java new file mode 100644 index 000000000..32601960b --- /dev/null +++ b/src/main/java/jikan/command/FindCommand.java @@ -0,0 +1,209 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidCommandException; +import jikan.exception.MultipleDelimitersException; +import jikan.exception.EmptyQueryException; +import jikan.ui.Ui; + +import static jikan.Jikan.lastShownList; +import java.util.ArrayList; + +/** + * Represents a command to find activities by name. + */ +public class FindCommand extends Command { + boolean isFinalCommand; + boolean isChained; + private static final String FILTER = "filter"; + private static final String FIND = "find"; + + /** + * Constructor to create a new find command. + */ + public FindCommand(String parameters) throws MultipleDelimitersException { + super(parameters.trim()); + isFinalCommand = true; + this.parameters = parameters.replaceAll("\\s+", " "); + this.parameters = parameters.trim(); + if (parameters.contains(";;") || parameters.contains("; ;")) { + throw new MultipleDelimitersException(); + } + } + + /** + * Constructor to create a new find command that has chaining. + */ + public FindCommand(String parameters, boolean isFinal, boolean hasChaining) throws MultipleDelimitersException { + super(parameters.trim()); + isFinalCommand = isFinal; + isChained = hasChaining; + this.parameters = parameters.replaceAll("\\s+", " "); + this.parameters = parameters.trim(); + if (parameters.contains(";;") || parameters.contains("; ;")) { + throw new MultipleDelimitersException(); + } + } + + + /** + * Shows the user all past activities that has names which match the keyword queried by the user. + * @param activityList the activity list to search for matching activities + */ + @Override + public void executeCommand(ActivityList activityList) { + // remove the magic number later + String[] tokenizedParameters = parameters.split(";", 2); + try { + checkForInvalidChaining(); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + return; + } + + if (tokenizedParameters.length > 1) { + executeChainedCommand(activityList, tokenizedParameters); + } else { + isFinalCommand = true; + executeSingleCommand(activityList); + } + } + + private void checkForInvalidChaining() throws MultipleDelimitersException { + if (parameters.contains(";;") || parameters.contains("; ;")) { + throw new MultipleDelimitersException(); + } + } + + private void executeChainedCommand(ActivityList activityList, String[] tokenizedParameters) { + if (tokenizedParameters[1].length() > 0) { + isFinalCommand = false; + parameters = tokenizedParameters[0]; + executeSingleCommand(activityList); + String nextCommand = tokenizedParameters[1].trim(); + try { + callNextCommand(nextCommand, activityList); + } catch (InvalidCommandException e) { + Ui.printDivider("Please chain find or filter commands only"); + } + } else { + isFinalCommand = true; + parameters = tokenizedParameters[0]; + searchSubList(); + } + } + + + private void executeSingleCommand(ActivityList activityList) { + if (parameters.contains("-s") || isChained == true) { + searchSubList(); + } else { + searchFullList(activityList); + } + } + + private void callNextCommand(String userInput, ActivityList activityList) throws InvalidCommandException { + String[] tokenizedInputs = userInput.split(" ", 2); + String instruction = tokenizedInputs[0]; + Command command = null; + switch (instruction) { + case FIND: + try { + command = new FindCommand(tokenizedInputs[1], false, true); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + } + break; + case FILTER: + try { + command = new FilterCommand(tokenizedInputs[1], false, true); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + } + break; + default: + throw new InvalidCommandException(); + } + + try { + command.executeCommand(activityList); + } catch (EmptyNameException | ExtraParametersException e) { + Ui.printDivider("Error parsing command. Please try again."); + } + } + + /** + * Find activities which has names containing the keywords from the entire list. + * @param activityList full like of activities + */ + private void searchFullList(ActivityList activityList) { + try { + if (parameters.length() < 1) { + throw new EmptyQueryException(); + } else { + String[] keywords = parameters.split(" / "); + lastShownList.activities.clear(); + for (String keyword : keywords) { + populateLastShownList(keyword, activityList.activities); + } + callPrintResults(); + } + } catch (EmptyQueryException e) { + Ui.printDivider("No keyword was given."); + } + } + + /** + * Find activities which has names containing the keywords from the last shown list. + */ + private void searchSubList() { + try { + String query = parameters.replace("-s ", ""); + ArrayList prevList = new ArrayList<>(); + prevList.addAll(lastShownList.activities); + if (query.length() < 1) { + throw new EmptyQueryException(); + } else { + String[] keywords = query.split(" / "); + lastShownList.activities.clear(); + for (String keyword : keywords) { + populateLastShownList(keyword, prevList); + } + } + callPrintResults(); + } catch (ArrayIndexOutOfBoundsException | EmptyQueryException e) { + Ui.printDivider("No keyword was given."); + } + } + + private void callPrintResults() { + if (isFinalCommand == true) { + Ui.printResults(lastShownList); + } + } + + /** + * Fills the last shown list with the results from matching names of activities to a keyword. + * @param keyword the keyword to match against + * @param activities the list of activities to search + */ + private void populateLastShownList(String keyword, ArrayList activities) { + for (Activity i : activities) { + if (containsIgnoreCase(i.getName(), keyword) && !lastShownList.activities.contains(i)) { + lastShownList.activities.add(i); + } + } + } + + private boolean containsIgnoreCase(String str, String subString) { + return str.toLowerCase().contains(subString.toLowerCase()); + } + +} diff --git a/src/main/java/jikan/command/GoalCommand.java b/src/main/java/jikan/command/GoalCommand.java new file mode 100644 index 000000000..d5ba89240 --- /dev/null +++ b/src/main/java/jikan/command/GoalCommand.java @@ -0,0 +1,237 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyGoalException; +import jikan.exception.EmptyTagException; +import jikan.exception.InvalidGoalCommandException; +import jikan.exception.NegativeDurationException; +import jikan.exception.NoSuchTagException; +import jikan.log.Log; +import jikan.storage.Storage; +import jikan.storage.StorageHandler; +import jikan.ui.Ui; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import static java.lang.Integer.valueOf; + + +/** + * Represents a command to set a goal for activities with a specific tag in the activity list. + */ + +public class GoalCommand extends Command { + private static Scanner scanner; + private static final String TAG_FILE_PATH = "data/tag/tag.csv"; + public static Storage tagStorage; // Storage the list was loaded from + public static StorageHandler tagStorageHandler; + + /** + * Constructor to create a new goal command. + * @param parameters the parameters of the goal command. + * @param scanner to read the user input. + */ + public GoalCommand(String parameters, Scanner scanner, Storage tagStorage) { + super(parameters); + this.scanner = scanner; + this.tagStorage = tagStorage; + this.tagStorageHandler = new StorageHandler(tagStorage); + } + + @Override + public void executeCommand(ActivityList activityList) { + try { + int delimiter = parameters.indexOf("/g"); + int deleteDelim = parameters.indexOf("/d"); + String tagName = ""; + int index; + if (delimiter != -1) { + tagName = parameters.substring(0, delimiter - 1).strip(); + if (tagName.isEmpty()) { + throw new EmptyTagException(); + } + index = checkIfExists(tagName, TAG_FILE_PATH); + String tmpTime = parameters.substring(delimiter + 3); + if (tmpTime.isEmpty()) { + throw new EmptyGoalException(); + } + Duration goalTime = parseDuration(tmpTime); + + if (goalTime.isNegative()) { + throw new NegativeDurationException(); + } + + if (index != -1) { + Ui.printDivider("The goal for this tag already exists, do you want to update the goal?"); + String userInput = scanner.nextLine(); + updateGoal(userInput, tagName, goalTime, index); + + } else { + // tag does not exist in the activity list. + if (!existInActivity(activityList, tagName)) { + throw new NoSuchTagException(); + } else { + tagStorage.writeToFile(tagName + "," + goalTime); + Ui.printDivider("The goal for " + tagName + " has been added."); + } + } + } else if (deleteDelim != -1) { + tagName = parameters.substring(0, deleteDelim - 1).strip(); + index = checkIfExists(tagName, TAG_FILE_PATH); + if (index != -1) { + Ui.printDivider("The goal for this tag has been deleted."); + deleteLine(index); + } else { + throw new NoSuchTagException(); + } + } else { + throw new InvalidGoalCommandException(); + } + + } catch (EmptyTagException e) { + Ui.printDivider("Tag name cannot be empty."); + Log.makeInfoLog("Goal command failed as no tag name was provided."); + } catch (InvalidGoalCommandException e) { + Ui.printDivider("Invalid command format entered."); + Log.makeInfoLog("Goal command failed as an incorrect format was provided."); + } catch (IOException e) { + Ui.printDivider("Error reading the file.\n" + + "If the file was open, please close it and try again."); + } catch (NoSuchTagException e) { + Ui.printDivider("There is no such tag."); + Log.makeInfoLog("Goal command failed as there was no such tag saved."); + } catch (ArrayIndexOutOfBoundsException | StringIndexOutOfBoundsException + | EmptyGoalException | NumberFormatException e) { + Ui.printDivider("Please enter the goal in the format HH:MM:SS."); + Log.makeInfoLog("Goal command failed as an incorrect format for the goal time was provided."); + } catch (NegativeDurationException e) { + Ui.printDivider("Please enter a positive goal time."); + Log.makeInfoLog("Goal command failed as a negative goal time was provided."); + } + } + + /** + * Check that tag exists in the tag list. + * @param tagName the tag name. + * @param filePath the file path of the tag file. + * @return index the index of the tag in the tag list. + * @throws IOException when there is an error loading/creating the file. + */ + public static int checkIfExists(String tagName, String filePath) throws IOException { + BufferedReader br = new BufferedReader(new FileReader(filePath)); + int index = 0; + int status = 0; + try { + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + String[] name; + while (line != null) { + name = line.split(","); + if (name[0].equals(tagName)) { + status = 1; + break; + } + sb.append(line); + sb.append("\n"); + line = br.readLine(); + index++; + } + } finally { + br.close(); + } + if (status == 0) { + index = -1; + } + return index; + } + + /** + * Update the goal for the existing specified tag. + * @param userInput the user response. + * @param tagName the tag name. + * @param goalTime the amount of time the user wants to assign to the tag. + * @param index the index of the tag in the tag list. + */ + private static void updateGoal(String userInput, String tagName, Duration goalTime, int index) throws IOException { + if (userInput.equalsIgnoreCase("yes") || userInput.equalsIgnoreCase("y")) { + deleteLine(index); + tagStorage.writeToFile(tagName + "," + goalTime); + Ui.printDivider("The goal for " + tagName + " was updated"); + } else if (userInput.equalsIgnoreCase("no") || userInput.equalsIgnoreCase("n")) { + Ui.printDivider("Okay then, what else can I do for you?"); + } else { + Ui.printDivider("Incorrect format entered, please only enter yes or no."); + } + } + + /** + * Removes the line whose index matches lineNumber from file. + * + * @param lineNumber Index of line to remove. + * @throws IOException If an error occurs while writing the new list to file. + */ + public static void deleteLine(int lineNumber) throws IOException { + // Read file into list of strings, where each string is a line in the file + List fileContent = new ArrayList<>(Files.readAllLines(Paths.get(TAG_FILE_PATH), + StandardCharsets.UTF_8)); + fileContent.remove(lineNumber); + saveNewTags(fileContent); + } + + /** + * Saves the updated tags to the csv file. + * + * @param newList The list containing the updated data. + * @throws IOException If an error occurs while writing the new list to file. + */ + public static void saveNewTags(List newList) throws IOException { + tagStorage.clearFile(); + FileWriter fw = new FileWriter(TAG_FILE_PATH, true); + + for (String s : newList) { + fw.write(s + System.lineSeparator()); + } + fw.close(); + } + + /** + * Check if the tag exists in the activity list. + * @param targetList the activity list to check. + * @param tagName the specified tag name. + * @return true or false. + */ + private boolean existInActivity(ActivityList targetList, String tagName) { + for (Activity i : targetList.activities) { + if (i.getTags().contains(tagName)) { + return true; + } + } + return false; + } + + /** + * Converts the user input into a duration object. + * @param input the user input. + * @return the duration object. + */ + public static Duration parseDuration(String input) throws + ArrayIndexOutOfBoundsException { + String[] fields = input.split(":"); + int colonIndex = input.indexOf(':'); + String hh = fields[0]; + String mm = fields[1]; + String ss = fields[2]; + return Duration.ofHours(valueOf(hh)).plusMinutes(valueOf(mm)).plusSeconds(valueOf(ss)); + } +} diff --git a/src/main/java/jikan/command/GraphCommand.java b/src/main/java/jikan/command/GraphCommand.java new file mode 100644 index 000000000..a2d5568cf --- /dev/null +++ b/src/main/java/jikan/command/GraphCommand.java @@ -0,0 +1,103 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.ExtraParametersException; +import jikan.exception.MissingParametersException; +import jikan.log.Log; +import jikan.ui.Ui; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Set; + +import static jikan.Jikan.lastShownList; + +public class GraphCommand extends Command { + + private static final String ALLOCATIONS = "allocations"; + private static final String TAGS = "tags"; + private static final String ACTIVITIES = "activities"; + + String[] inputs; + + /** + * Constructor to create a new command. + * @param parameters Either time interval for graph or 'tags' flag + * to graph by tags + */ + public GraphCommand(String parameters) throws ExtraParametersException { + super(parameters); + + this.inputs = parameters.split(" "); + if (inputs.length > 2) { + throw new ExtraParametersException(); + } + } + + @Override + public void executeCommand(ActivityList activityList) { + try { + switch (inputs[0]) { + case ALLOCATIONS: + Ui.graphAllocation(lastShownList); + Log.makeInfoLog("Allocations was graphed"); + break; + case TAGS: + graphTags(); + Log.makeInfoLog("Tags were graphed"); + break; + case ACTIVITIES: + graphActivities(); + Log.makeInfoLog("Activities were graphed"); + break; + default: + Ui.printDivider("Please specify whether you want to graph activities / tags / allocations."); + } + } catch (NumberFormatException | MissingParametersException e) { + Ui.printDivider("Please input an integer for the time interval."); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("Please specify whether you want to graph activities / tags / allocations."); + } + } + + private void graphActivities() throws MissingParametersException { + if (inputs.length < 2) { + throw new MissingParametersException(); + } else { + int interval = Integer.parseInt(inputs[1]); + Ui.printActivityGraph(interval); + } + } + + private void graphTags() throws MissingParametersException { + HashMap tags = new HashMap<>(); + for (Activity activity : lastShownList.activities) { + extractTags(tags, activity); + } + if (inputs.length < 2) { + throw new MissingParametersException(); + } else { + int interval = Integer.parseInt(inputs[1]); + Ui.printTagsGraph(tags, interval); + } + } + + /** + * Gets the tags from the activities in the list together with the associated duration. + * @param tags the HashMap to store the tag name and duration. + * @param activity the activity containing the tag. + */ + public static void extractTags(HashMap tags, Activity activity) { + Set activityTags = activity.getTags(); + for (String tag : activityTags) { + if (tags.containsKey(tag)) { + Duration oldDuration = tags.get(tag); + Duration newDuration = oldDuration.plus(activity.getDuration()); + tags.put(tag, newDuration); + } else { + tags.put(tag, activity.getDuration()); + } + } + } +} diff --git a/src/main/java/jikan/command/ListCommand.java b/src/main/java/jikan/command/ListCommand.java new file mode 100644 index 000000000..d1b1ff030 --- /dev/null +++ b/src/main/java/jikan/command/ListCommand.java @@ -0,0 +1,206 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.ExtraParametersException; +import jikan.ui.Ui; + +import jikan.exception.InvalidTimeFrameException; + +import java.time.LocalDate; +import java.time.DayOfWeek; +import java.time.Month; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAdjusters; +import java.util.Calendar; + +import static jikan.Jikan.lastShownList; + +/** + * Represents a command to list all activities in the activity list to the user. + */ +public class ListCommand extends Command { + + /** + * Constructor to create a new list command. + */ + public ListCommand(String parameters) { + super(parameters); + } + + /** + * Parse a list command. The user can specify either a single date or a specific time frame. + * + * @param activityList The activity list to search for matching activities. + */ + @Override + public void executeCommand(ActivityList activityList) { + // If no time frame is specified, print the entire list + if (parameters == null || parameters.isBlank()) { + listAll(activityList); + } else { + parameters = parameters.strip(); + try { + listInterval(activityList); + } catch (DateTimeParseException e) { + Ui.printDivider("Please enter a valid date in the format dd/MM/yyyy or yyyy-MM-dd\n" + + "Or use day / week / month to view tasks in the respective time period."); + } catch (InvalidTimeFrameException e) { + Ui.printDivider("Please enter a valid time frame; the end date must come after the start date."); + } catch (ExtraParametersException e) { + Ui.printDivider("Extra parameters detected!\n" + + "Use day / week / month to view tasks in the respective time period."); + } + } + } + + private void listAll(ActivityList activityList) { + lastShownList.activities.clear(); + Ui.printList(activityList); + + // Can't do lastShownList = activityList, otherwise we just copy + lastShownList.activities.addAll(activityList.activities); + } + + private void listInterval(ActivityList activityList) + throws InvalidTimeFrameException, DateTimeParseException, ExtraParametersException { + String[] listInputs; + listInputs = parameters.split(" ", 2); + + lastShownList.activities.clear(); + + LocalDate startDate; + LocalDate endDate = null; + + // Parse either format + DateTimeFormatter parser = DateTimeFormatter.ofPattern("[dd/MM/yyyy][yyyy-MM-dd]"); + + // Check if the user has given a verbal input + // (User can either say day or daily and get the same output) + switch (listInputs[0]) { + case "today": + // Fallthrough + case "day": + // Fallthrough + case "daily": + checkExtraParameters(listInputs); + startDate = LocalDate.now(); + break; + case "yesterday": + checkExtraParameters(listInputs); + startDate = LocalDate.now().minusDays(1); + break; + case "week": + // Fallthrough + case "weekly": + startDate = getStartOfWeek(listInputs, parser); + // Set current Monday and Sunday as time range + startDate = startDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + endDate = startDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + break; + case "month": + // Fallthrough + case "monthly": + startDate = getStartOfMonth(listInputs); + if (startDate == null) { + return; + } + endDate = startDate.with(TemporalAdjusters.lastDayOfMonth()); + break; + default: + // date / date range is given + startDate = LocalDate.parse(listInputs[0], parser); + if (listInputs.length == 2) { + endDate = LocalDate.parse(listInputs[1], parser); + } + break; + } + printList(activityList, startDate, endDate); + } + + private LocalDate getStartOfMonth(String[] listInputs) throws ExtraParametersException { + LocalDate startDate;// If user has input a specific month, use that; + // Otherwise get current date + if (listInputs.length == 2) { + try { + startDate = getMonth(listInputs[1]); + } catch (IllegalArgumentException e) { + Ui.printDivider("Please specify the full month name."); + return null; + } + } else { + startDate = LocalDate.now(); + startDate = startDate.withDayOfMonth(1); + } + return startDate; + } + + private LocalDate getStartOfWeek(String[] listInputs, DateTimeFormatter parser) { + LocalDate startDate;// If user has input a specific date to obtain the week from, use that; + // (eg. the input is list week 2020-05-20) + // Otherwise get current date + if (listInputs.length == 2) { + if (listInputs[1].isBlank()) { + startDate = LocalDate.now(); + } else { + startDate = LocalDate.parse(listInputs[1], parser); + } + } else { + startDate = LocalDate.now(); + } + return startDate; + } + + private void printList(ActivityList activityList, LocalDate startDate, LocalDate endDate) + throws InvalidTimeFrameException { + // Only one date is specified; return all entries with start date coinciding with that date + if (endDate == null) { + for (Activity i : activityList.activities) { + if (i.getDate().equals(startDate)) { + lastShownList.activities.add(i); + } + } + Ui.printList(lastShownList); + // Both start and end dates are specified + } else { + + if (endDate.isBefore(startDate)) { + throw new InvalidTimeFrameException(); + } + + for (Activity i : activityList.activities) { + if (i.isWithinDateFrame(startDate, endDate)) { + lastShownList.activities.add(i); + } + } + Ui.printList(lastShownList); + } + } + + private LocalDate getMonth(String listInput) throws ExtraParametersException { + LocalDate startDate; + Month month; + if (listInput.isBlank()) { + // return current month + LocalDate currentDate = LocalDate.now(); + month = currentDate.getMonth(); + YearMonth yearMonth = YearMonth.of(Calendar.getInstance().get(Calendar.YEAR), month.getValue()); + startDate = yearMonth.atDay(1); + } else { + month = Month.valueOf(listInput.toUpperCase()); + YearMonth yearMonth = YearMonth.of(Calendar.getInstance().get(Calendar.YEAR), month.getValue()); + startDate = yearMonth.atDay(1); + } + return startDate; + } + + private void checkExtraParameters(String[] listInputs) throws ExtraParametersException { + if (listInputs.length > 1) { + if (!listInputs[1].isBlank()) { + throw new ExtraParametersException(); + } + } + } +} diff --git a/src/main/java/jikan/command/StartCommand.java b/src/main/java/jikan/command/StartCommand.java new file mode 100644 index 000000000..51446924a --- /dev/null +++ b/src/main/java/jikan/command/StartCommand.java @@ -0,0 +1,399 @@ +package jikan.command; + +import jikan.log.Log; +import jikan.activity.ActivityList; +import jikan.parser.Parser; +import jikan.ui.Ui; + + +import java.time.DateTimeException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * Represents a command to start an activity. + */ +public class StartCommand extends Command { + + private Scanner scanner; + private boolean hasAllocation = false; + private boolean hasTag = false; + private boolean hasAllocationAndTag = false; + private static final int MAX_ACTIVITY_LENGTH = 25; + + /** + * Constructor to create a new start command. + */ + public StartCommand(String parameters, Scanner scanner) { + super(parameters); + this.scanner = scanner; + } + + @Override + public void executeCommand(ActivityList activityList) { + boolean hasStarted = hasStarted(Parser.startTime); + if (hasStarted) { + stopExecution(); + } else { + assert Parser.tags.isEmpty(); + continueExecution(activityList); + } + } + + /** + * Checks if there is a concurrently running activity. + * @param startTime a LocalDateTime object to check if parser is waiting for a running activity. + * @return true if there is a concurrent running activity and false otherwise. + */ + private boolean hasStarted(LocalDateTime startTime) { + return startTime != null; + } + + /** + * Stops executing current activity if a concurrent running activity is found. + */ + private void stopExecution() { + assert Parser.startTime != null; + String line = Parser.activityName + " is ongoing!"; + Log.makeInfoLog("Could not start activity due to already ongoing activity."); + Ui.printDivider(line); + } + + /** + * Continues execution as no concurrent running activity is found. + * @param activityList a list of tracked activities. + */ + private void continueExecution(ActivityList activityList) { + String activityName = parseActivityName(this.parameters); + if (activityName.isEmpty()) { + Log.makeFineLog("Empty activity name was provided."); + Ui.printDivider("Activity name cannot be empty"); + } else { + checkActivity(activityName, activityList); + } + } + + + /** + * Get activity name from parameters to start command. + * @param parameters the parameters to start command. + * @return activity name of the activity to be started. + */ + private String parseActivityName(String parameters) { + String scenario; + String activityName; + int tagDelimiter = parameters.indexOf("/t"); + int allocateDelimiter = parameters.indexOf("/a"); + scenario = getScenario(tagDelimiter, allocateDelimiter); + switch (scenario) { + case "hasTagAndAllocation": + assert tagDelimiter != -1 && allocateDelimiter != -1; + activityName = handleTagAndAllocation(this.parameters, tagDelimiter, allocateDelimiter); + break; + case "hasTagOnly": + assert tagDelimiter != -1 && allocateDelimiter == -1; + activityName = handleTagOrAllocation(this.parameters, tagDelimiter); + break; + case "hasAllocationOnly": + assert tagDelimiter == -1 && allocateDelimiter != -1; + activityName = handleTagOrAllocation(this.parameters, allocateDelimiter); + break; + case "hasNoTagAndAllocation": + assert tagDelimiter == -1 && allocateDelimiter == -1; + activityName = this.parameters.trim(); + break; + default: + activityName = ""; + break; + } + return activityName; + } + + /** + * Method to check for tags and allocated time. + * @param tagDelimiter index where tag flag is found. + * @param allocateDelimiter index where allocate flag is found. + * @return a string with information about whether tags and allocated time are found. + */ + private String getScenario(int tagDelimiter, int allocateDelimiter) { + if (tagDelimiter != -1 && allocateDelimiter != -1) { + this.hasAllocationAndTag = true; + return "hasTagAndAllocation"; + } else if (tagDelimiter != -1) { + this.hasTag = true; + return "hasTagOnly"; + } else if (allocateDelimiter != -1) { + this.hasAllocation = true; + return "hasAllocationOnly"; + } else { + return "hasNoTagAndAllocation"; + } + } + + /** + * Method to extract activity name. + * @param parameters parameters to start command. + * @param tagDelimiter index where tag flag is found. + * @param allocateDelimiter index where allocate flag is found. + * @return activity name + */ + private String handleTagAndAllocation(String parameters, int tagDelimiter, int allocateDelimiter) { + String activityName; + int delimiter = 0; + if (tagDelimiter < allocateDelimiter) { + delimiter = tagDelimiter; + } else if (allocateDelimiter < tagDelimiter) { + delimiter = allocateDelimiter; + } + assert delimiter == tagDelimiter || delimiter == allocateDelimiter; + activityName = parameters.substring(0, delimiter); + activityName = activityName.trim(); + if (activityName.isEmpty()) { + return ""; + } else { + return activityName; + } + } + + /** + * Method to extract activity name. + * @param parameters parameters to start command. + * @param delimiter index where tag flag or allocate flag is found. + * @return activity name. + */ + private String handleTagOrAllocation(String parameters, int delimiter) { + assert delimiter != -1; + String activityName = parameters.substring(0, delimiter); + activityName = activityName.trim(); + if (activityName.isEmpty()) { + return ""; + } else { + return activityName; + } + } + + /** + * Method to check if the activity exists in activity list and does not exceed 25 characters. + * @param activityName the string representing activity name. + * @param activityList a list of tracked activities. + */ + private void checkActivity(String activityName, ActivityList activityList) { + assert !activityName.isEmpty(); + int index = activityList.findActivity(activityName); + if (index != -1) { + Ui.printDivider("There is already an activity with this name. Would you like to continue it?"); + continueActivity(activityList, scanner, index); + } else if (activityName.length() > MAX_ACTIVITY_LENGTH) { + Log.makeInfoLog("Activity name longer than 25 characters."); + Ui.printDivider("Please input an activity name that is shorter than 25 characters."); + } else { + addActivityToList(activityName); + } + } + + /** + * Method to add a activity to the activity list. + * @param activityName the string representing activity name. + */ + private void addActivityToList(String activityName) { + if (hasAllocationAndTag) { + parseActivityWithBothField(activityName, this.parameters); + } else if (hasAllocation) { + addActivityWithAllocation(activityName, this.parameters); + } else if (hasTag) { + addActivityWithTag(activityName, this.parameters); + } else { + addActivity(activityName); + } + } + + /** + * Parse the tag and allocate information. + * @param activityName the string representing activity name. + * @param line a line with information on tags and allocated time. + */ + private void parseActivityWithBothField(String activityName, String line) { + int allocateIndex = line.indexOf("/a"); + int tagIndex = line.indexOf("/t"); + assert tagIndex != -1 && allocateIndex != -1; + String tagInfo = ""; + String durationInfo = ""; + if (tagIndex < allocateIndex) { + tagInfo = line.substring(tagIndex, allocateIndex); + durationInfo = line.substring(allocateIndex); + tagInfo = tagInfo.trim(); + durationInfo = durationInfo.trim(); + } else if (allocateIndex < tagIndex) { + durationInfo = line.substring(allocateIndex, tagIndex); + tagInfo = line.substring(tagIndex); + tagInfo = tagInfo.trim(); + durationInfo = durationInfo.trim(); + } + durationInfo = durationInfo.substring(2); + durationInfo = durationInfo.trim(); + tagInfo = tagInfo.substring(2); + tagInfo = tagInfo.trim(); + addActivityWithBothField(durationInfo, tagInfo, activityName); + } + + /** + * Add activity with both tag and allocate time. + * @param durationInfo information about the duration. + * @param tagInfo information about the tag. + * @param activityName the string representing activity name. + */ + private void addActivityWithBothField(String durationInfo, String tagInfo, String activityName) { + String[] tagStrings = tagInfo.split(" "); + try { + checkBothField(durationInfo, tagInfo, activityName, tagStrings); + } catch (DateTimeException e) { + Log.makeInfoLog("Allocated time provided was not valid."); + Ui.printDivider("Time provided is invalid, please provide time in this format" + + " HH:MM:SS."); + } + } + + /** + * Check if allocated time and tags are formatted correctly/not empty. + * @param durationInfo information about the duration. + * @param tagInfo information about the tag. + * @param activityName the string representing activity name. + * @param tagStrings tokenized information about the tags. + */ + private void checkBothField(String durationInfo, String tagInfo, String activityName, String[] tagStrings) { + LocalTime startTime = LocalTime.MIN; + LocalTime endTime; + endTime = LocalTime.parse(durationInfo); + Duration allocatedTime = Duration.between(startTime, endTime); + ArrayList tags = getTags(tagStrings); + if (tagInfo.isEmpty()) { + Log.makeInfoLog("No tags found with tag flag."); + Ui.printDivider("Please provide a valid tag"); + } else if (allocatedTime == Duration.parse("PT0S")) { + Log.makeInfoLog("Allocated time is zero."); + Ui.printDivider("Please provide a non zero allocated time."); + } else if (tags.size() > 2) { + Log.makeInfoLog("Activity has more than 2 tags"); + Ui.printDivider("There cannot be more than 2 tags."); + } else { + Parser.tags.addAll(tags); + Parser.allocatedTime = allocatedTime; + addActivity(activityName); + } + } + + /** + * Add activity with allocated time only. + * @param activityName the string representing activity name. + * @param line a line with information about allocated time. + */ + private void addActivityWithAllocation(String activityName, String line) { + int index = line.indexOf("/a"); + String durationInfo = line.substring(index + 2); + durationInfo = durationInfo.trim(); + LocalTime startTime = LocalTime.MIN; + try { + checkTime(activityName, durationInfo, startTime); + } catch (DateTimeException e) { + Log.makeInfoLog("Allocated time provided was not valid."); + Ui.printDivider("Time provided is invalid, please provide time in this format" + + " HH:MM:SS."); + } + } + + /** + * Method to check if allocated time given is valid before adding the activity. + * @param activityName the string representing activity name. + * @param durationInfo the string representing the duration information. + * @param startTime a LOCALTIME object representing the time 00:00:00. + */ + private void checkTime(String activityName, String durationInfo, LocalTime startTime) { + LocalTime endTime; + endTime = LocalTime.parse(durationInfo); + Duration allocatedTime = Duration.between(startTime, endTime); + if (allocatedTime == Duration.parse("PT0S")) { + Log.makeInfoLog("Allocated time is zero."); + Ui.printDivider("Please provide a non zero allocated time."); + } else { + Parser.allocatedTime = allocatedTime; + addActivity(activityName); + } + } + + /** + * Add activity with tags only. + * @param activityName the string representing activity name. + * @param line a line with information about tags. + */ + private void addActivityWithTag(String activityName, String line) { + int index = line.indexOf("/t"); + String tagInfo = line.substring(index + 2); + tagInfo = tagInfo.trim(); + String[] tagStrings = tagInfo.split(" "); + ArrayList tags = getTags(tagStrings); + if (tagInfo.isEmpty()) { + Log.makeInfoLog("No tags found with tag flag."); + Ui.printDivider("Please provide a valid tag."); + } else if (tags.size() > 2) { + Log.makeInfoLog("Activity has more than 2 tags"); + Ui.printDivider("There cannot be more than 2 tags."); + } else { + Parser.tags.addAll(tags); + addActivity(activityName); + } + } + + /** + * Method to get valid tags from an array of tags. + * @param tagStrings array of tags + * @return an array list consisting of valid tags + */ + private ArrayList getTags(String[] tagStrings) { + ArrayList tags = new ArrayList<>(); + for (String s : tagStrings) { + String tag = s.trim(); + if (!tag.isEmpty()) { + tags.add(tag); + } + } + return tags; + } + + + /** + * Add activity to activity list. + * @param activityName the string representing activity name. + */ + private void addActivity(String activityName) { + Parser.activityName = activityName; + Parser.startTime = LocalDateTime.now(); + Log.makeFineLog(activityName + " was started."); + Ui.printDivider(activityName + " was started."); + } + + + /** + * Received user input on whether or not to continue the activity. + * @param activityList List of activities. + * @param scanner Parse user input. + */ + private static void continueActivity(ActivityList activityList, Scanner scanner, int index) { + String userInput = scanner.nextLine(); + if (userInput.equalsIgnoreCase("yes") || userInput.equalsIgnoreCase("y")) { + Parser.activityName = activityList.get(index).getName(); + Parser.startTime = LocalDateTime.now(); + Parser.tags = activityList.get(index).getTags(); + Parser.continuedIndex = index; + Ui.printDivider(Parser.activityName + " was continued."); + Log.makeFineLog(Parser.activityName + " was continued."); + } else if (userInput.equalsIgnoreCase("no") || userInput.equalsIgnoreCase("n")) { + Parser.activityName = null; + Ui.printDivider("Okay then, what else can I do for you?"); + } else { + Ui.printDivider("Incorrect format entered, please only enter yes or no."); + } + } +} diff --git a/src/main/java/jikan/command/ViewGoalsCommand.java b/src/main/java/jikan/command/ViewGoalsCommand.java new file mode 100644 index 000000000..b941f1370 --- /dev/null +++ b/src/main/java/jikan/command/ViewGoalsCommand.java @@ -0,0 +1,90 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyTagException; +import jikan.storage.Storage; +import jikan.storage.StorageHandler; +import jikan.ui.Ui; + +import java.io.FileNotFoundException; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Scanner; + +/** + * Represents a command to view goals for tags in the activity list. + */ +public class ViewGoalsCommand extends Command { + + //File tagFile; + private static final String TAG_FILE_PATH = "data/tag/tag.csv"; + public Storage tagStorage; // Storage the list was loaded from + public StorageHandler tagStorageHandler; + + /** + * Constructor to create a new viewgoal command. + * @param parameters the parameters of the goal command. + */ + public ViewGoalsCommand(String parameters, Storage tagStorage) { + super(parameters); + this.tagStorage = tagStorage; + this.tagStorageHandler = new StorageHandler(tagStorage); + } + + @Override + public void executeCommand(ActivityList activityList) { + HashMap tagsGoals = new HashMap<>(); + populateTagList(tagStorage, tagsGoals); + getGoalData(activityList,tagsGoals); + + } + + private void getGoalData(ActivityList activityList, HashMap tagsGoals) { + HashMap tagsActual = new HashMap<>(); + for (Activity activity : activityList.activities) { + GraphCommand.extractTags(tagsActual, activity); + } + try { + if (tagsActual.isEmpty() || tagsGoals.isEmpty()) { + throw new EmptyTagException(); + } + Ui.printGoals(tagsGoals, tagsActual); + } catch (NullPointerException | EmptyTagException e) { + Ui.printDivider("There are no tags to display."); + } + } + + /** + * Populates task list from file. + * + * @param tagStorage Storage object containing the tag file. + */ + private void populateTagList(Storage tagStorage, HashMap tagsGoals) { + try { + Scanner dataScanner = new Scanner(tagStorage.dataFile); + while (dataScanner.hasNext()) { + parseDataLine(dataScanner.nextLine(), tagsGoals); + } + } catch (FileNotFoundException e) { + System.out.println("Data file not found. Could not load into the current session's tag list."); + } catch (NullPointerException e) { + System.out.println("Error."); + } + } + + /** + * Parses the current line in the tag file. + * + * @param s String to parse. + */ + private void parseDataLine(String s, HashMap tagsGoals) { + if (!s.isEmpty()) { + List strings = Arrays.asList(s.split(",")); + Duration duration = Duration.parse(strings.get(1)); + tagsGoals.put(strings.get(0), duration); + } + } +} diff --git a/src/main/java/jikan/exception/ActivityIsRunningException.java b/src/main/java/jikan/exception/ActivityIsRunningException.java new file mode 100644 index 000000000..dd9cb6277 --- /dev/null +++ b/src/main/java/jikan/exception/ActivityIsRunningException.java @@ -0,0 +1,5 @@ +package jikan.exception; + +public class ActivityIsRunningException extends Exception { +} + diff --git a/src/main/java/jikan/exception/EmptyGoalException.java b/src/main/java/jikan/exception/EmptyGoalException.java new file mode 100644 index 000000000..13ff1a8bc --- /dev/null +++ b/src/main/java/jikan/exception/EmptyGoalException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class EmptyGoalException extends Exception { +} diff --git a/src/main/java/jikan/exception/EmptyNameException.java b/src/main/java/jikan/exception/EmptyNameException.java new file mode 100644 index 000000000..9e380e6e9 --- /dev/null +++ b/src/main/java/jikan/exception/EmptyNameException.java @@ -0,0 +1,7 @@ +package jikan.exception; + +/** + * An exception that is thrown when the task name field is empty. + */ +public class EmptyNameException extends Exception { +} diff --git a/src/main/java/jikan/exception/EmptyQueryException.java b/src/main/java/jikan/exception/EmptyQueryException.java new file mode 100644 index 000000000..f6cef53b6 --- /dev/null +++ b/src/main/java/jikan/exception/EmptyQueryException.java @@ -0,0 +1,7 @@ +package jikan.exception; + +/** + * An exception that is thrown when the query for find or filter is not provided. + */ +public class EmptyQueryException extends Exception { +} \ No newline at end of file diff --git a/src/main/java/jikan/exception/EmptyTagException.java b/src/main/java/jikan/exception/EmptyTagException.java new file mode 100644 index 000000000..4f4e94233 --- /dev/null +++ b/src/main/java/jikan/exception/EmptyTagException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class EmptyTagException extends Exception { +} diff --git a/src/main/java/jikan/exception/ExistingNameException.java b/src/main/java/jikan/exception/ExistingNameException.java new file mode 100644 index 000000000..168ee626e --- /dev/null +++ b/src/main/java/jikan/exception/ExistingNameException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class ExistingNameException extends Exception { +} diff --git a/src/main/java/jikan/exception/ExistingTagGoalException.java b/src/main/java/jikan/exception/ExistingTagGoalException.java new file mode 100644 index 000000000..9c1ba5d23 --- /dev/null +++ b/src/main/java/jikan/exception/ExistingTagGoalException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class ExistingTagGoalException extends Exception { +} diff --git a/src/main/java/jikan/exception/ExtraParametersException.java b/src/main/java/jikan/exception/ExtraParametersException.java new file mode 100644 index 000000000..e5ed06338 --- /dev/null +++ b/src/main/java/jikan/exception/ExtraParametersException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class ExtraParametersException extends Throwable { +} diff --git a/src/main/java/jikan/exception/InvalidCleanCommandException.java b/src/main/java/jikan/exception/InvalidCleanCommandException.java new file mode 100644 index 000000000..ce34c8e25 --- /dev/null +++ b/src/main/java/jikan/exception/InvalidCleanCommandException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class InvalidCleanCommandException extends Exception { +} diff --git a/src/main/java/jikan/exception/InvalidCommandException.java b/src/main/java/jikan/exception/InvalidCommandException.java new file mode 100644 index 000000000..67ebf4ee6 --- /dev/null +++ b/src/main/java/jikan/exception/InvalidCommandException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class InvalidCommandException extends Exception { +} diff --git a/src/main/java/jikan/exception/InvalidEditFormatException.java b/src/main/java/jikan/exception/InvalidEditFormatException.java new file mode 100644 index 000000000..292f82955 --- /dev/null +++ b/src/main/java/jikan/exception/InvalidEditFormatException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class InvalidEditFormatException extends Exception { +} diff --git a/src/main/java/jikan/exception/InvalidGoalCommandException.java b/src/main/java/jikan/exception/InvalidGoalCommandException.java new file mode 100644 index 000000000..151ec80f8 --- /dev/null +++ b/src/main/java/jikan/exception/InvalidGoalCommandException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class InvalidGoalCommandException extends Exception { +} diff --git a/src/main/java/jikan/exception/InvalidTimeFrameException.java b/src/main/java/jikan/exception/InvalidTimeFrameException.java new file mode 100644 index 000000000..2ad7eb7d5 --- /dev/null +++ b/src/main/java/jikan/exception/InvalidTimeFrameException.java @@ -0,0 +1,7 @@ +package jikan.exception; + +/** + * An exception that is thrown when the time frame is invalid (e.g., the end time comes before the start time) + */ +public class InvalidTimeFrameException extends Exception { +} diff --git a/src/main/java/jikan/exception/MissingParametersException.java b/src/main/java/jikan/exception/MissingParametersException.java new file mode 100644 index 000000000..d49c03493 --- /dev/null +++ b/src/main/java/jikan/exception/MissingParametersException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class MissingParametersException extends Exception { +} diff --git a/src/main/java/jikan/exception/MultipleDelimitersException.java b/src/main/java/jikan/exception/MultipleDelimitersException.java new file mode 100644 index 000000000..55ef1153c --- /dev/null +++ b/src/main/java/jikan/exception/MultipleDelimitersException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class MultipleDelimitersException extends Exception{ +} diff --git a/src/main/java/jikan/exception/NameTooLongException.java b/src/main/java/jikan/exception/NameTooLongException.java new file mode 100644 index 000000000..c29a1a8db --- /dev/null +++ b/src/main/java/jikan/exception/NameTooLongException.java @@ -0,0 +1,7 @@ +package jikan.exception; + +/** + * An exception that is thrown when the task name field is longer than 25 characters. + */ +public class NameTooLongException extends Exception { +} diff --git a/src/main/java/jikan/exception/NegativeDurationException.java b/src/main/java/jikan/exception/NegativeDurationException.java new file mode 100644 index 000000000..2ffe6d722 --- /dev/null +++ b/src/main/java/jikan/exception/NegativeDurationException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class NegativeDurationException extends Exception { +} diff --git a/src/main/java/jikan/exception/NegativeNumberException.java b/src/main/java/jikan/exception/NegativeNumberException.java new file mode 100644 index 000000000..d54ea2454 --- /dev/null +++ b/src/main/java/jikan/exception/NegativeNumberException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class NegativeNumberException extends Exception { +} diff --git a/src/main/java/jikan/exception/NoSuchActivityException.java b/src/main/java/jikan/exception/NoSuchActivityException.java new file mode 100644 index 000000000..90f080c70 --- /dev/null +++ b/src/main/java/jikan/exception/NoSuchActivityException.java @@ -0,0 +1,7 @@ +package jikan.exception; + +/** + * An exception that is thrown when the task name is unknown. + */ +public class NoSuchActivityException extends Exception { +} diff --git a/src/main/java/jikan/exception/NoSuchTagException.java b/src/main/java/jikan/exception/NoSuchTagException.java new file mode 100644 index 000000000..94db0f41b --- /dev/null +++ b/src/main/java/jikan/exception/NoSuchTagException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class NoSuchTagException extends Exception { +} diff --git a/src/main/java/jikan/exception/WrongDateFormatException.java b/src/main/java/jikan/exception/WrongDateFormatException.java new file mode 100644 index 000000000..7fd7aaf7b --- /dev/null +++ b/src/main/java/jikan/exception/WrongDateFormatException.java @@ -0,0 +1,4 @@ +package jikan.exception; + +public class WrongDateFormatException extends Exception { +} diff --git a/src/main/java/jikan/log/Log.java b/src/main/java/jikan/log/Log.java new file mode 100644 index 000000000..808a2f7a2 --- /dev/null +++ b/src/main/java/jikan/log/Log.java @@ -0,0 +1,80 @@ +package jikan.log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; + +/** + * Represents a logger object to log user commands and outcomes to a logfile. + */ +public class Log { + private static Logger logger; + private static SimpleFormatter formatterTxt; + public String logFilePath = "data/LogRecord.txt"; + private static File logFile; + + /** + * Constructor for a new logger. + */ + public Log() { + logger = Logger.getLogger(Log.class.getName()); + LogManager.getLogManager().reset(); + logger.setLevel(Level.ALL); + + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(Level.WARNING); + logger.addHandler(consoleHandler); + + logFile = new File(logFilePath); + + if (!logFile.exists()) { + try { + // Create file + logFile.getParentFile().mkdirs(); // Create data directory (does nothing if directory already exists) + logFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + FileHandler fileHandler = null; + try { + fileHandler = new FileHandler("data/LogRecord.txt", true); + } catch (IOException e) { + e.printStackTrace(); + } + formatterTxt = new SimpleFormatter(); + fileHandler.setFormatter(formatterTxt); + fileHandler.setLevel(Level.INFO); + logger.addHandler(fileHandler); + } + + /** + * Creates a long entry at FINE level. + * @param message the FINE message to be logged + */ + public static void makeFineLog(String message) { + logger.log(Level.FINE, message); + } + + /** + * Creates a long entry at INFO level. + * @param message the INFO message to be logged + */ + public static void makeInfoLog(String message) { + logger.log(Level.INFO, message); + } + + /** + * Creates a long entry at WARNING level. + * @param message the WARNING warning message to be logged + */ + public void makeWarningLog(String message) { + logger.log(Level.WARNING, message); + } +} diff --git a/src/main/java/jikan/parser/Parser.java b/src/main/java/jikan/parser/Parser.java new file mode 100644 index 000000000..e4102bbdd --- /dev/null +++ b/src/main/java/jikan/parser/Parser.java @@ -0,0 +1,219 @@ +package jikan.parser; + +import jikan.exception.ExtraParametersException; +import jikan.exception.MultipleDelimitersException; +import jikan.log.Log; +import jikan.cleaner.StorageCleaner; +import jikan.storage.Storage; +import jikan.ui.Ui; + +import jikan.command.AbortCommand; +import jikan.command.ByeCommand; +import jikan.command.CleanCommand; +import jikan.command.Command; +import jikan.command.ContinueCommand; +import jikan.command.DeleteCommand; +import jikan.command.EditCommand; +import jikan.command.EndCommand; +import jikan.command.FilterCommand; +import jikan.command.FindCommand; +import jikan.command.GoalCommand; +import jikan.command.GraphCommand; +import jikan.command.ListCommand; +import jikan.command.StartCommand; +import jikan.command.ViewGoalsCommand; + +import jikan.cleaner.LogCleaner; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +import static jikan.log.Log.makeInfoLog; + + +/** + * Represents the object which parses user input to relevant functions for the execution of commands. + */ +public class Parser { + + private static final String ABORT = "abort"; + private static final String BYE = "bye"; + private static final String CLEAN = "clean"; + private static final String CONTINUE = "continue"; + private static final String DELETE = "delete"; + private static final String EDIT = "edit"; + private static final String END = "end"; + private static final String FILTER = "filter"; + private static final String FIND = "find"; + private static final String GOAL = "goal"; + private static final String GRAPH = "graph"; + private static final String LIST = "list"; + private static final String START = "start"; + + + public static LocalDateTime startTime = null; + public static LocalDateTime endTime = null; + public static String activityName = null; + public static Duration allocatedTime = Duration.parse("PT0S"); + public static Set tags = new HashSet<>(); + public StorageCleaner cleaner; + public LogCleaner logcleaner; + public Storage tagStorage; + public static String[] tokenizedInputs; + String instruction; + private static Log logger = new Log(); + + // flag to check if the current activity is a continued one + public static int continuedIndex = -1; + + /** + * Parses user commands to relevant functions to carry out the commands. + * + * @param scanner scanner object which reads user input + */ + public Command parseUserCommands(Scanner scanner) throws NullPointerException, + ArrayIndexOutOfBoundsException { + makeInfoLog("Starting to parse inputs."); + + String userInput = scanner.nextLine(); + tokenizedInputs = userInput.split(" ", 2); + instruction = tokenizedInputs[0]; + Command command = null; + + switch (instruction) { + case BYE: + if (tokenizedInputs.length > 1 && !tokenizedInputs[1].isBlank()) { + Ui.printDivider("Extra parameters detected."); + break; + } + command = new ByeCommand(null); + break; + case START: + try { + command = new StartCommand(tokenizedInputs[1], scanner); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + makeInfoLog("Activity started without activity name"); + Ui.printDivider("Activity name cannot be empty."); + } + break; + case END: + if (tokenizedInputs.length > 1 && !tokenizedInputs[1].isBlank()) { + Ui.printDivider("Extra parameters detected."); + break; + } + command = new EndCommand(null); + break; + case ABORT: + if (tokenizedInputs.length > 1 && !tokenizedInputs[1].isBlank()) { + Ui.printDivider("Extra parameters detected."); + break; + } + command = new AbortCommand(null); + break; + case LIST: + if (tokenizedInputs.length == 1) { + command = new ListCommand(null); + } else { + command = new ListCommand(tokenizedInputs[1]); + } + break; + case DELETE: + try { + command = new DeleteCommand(tokenizedInputs[1]); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("Activity name cannot be empty."); + } + break; + case FIND: + try { + command = new FindCommand(tokenizedInputs[1]); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + } + break; + case FILTER: + try { + command = new FilterCommand(tokenizedInputs[1]); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } catch (MultipleDelimitersException e) { + Ui.printDivider("Please only use one ';' between each command."); + } + break; + case EDIT: + try { + command = new EditCommand(tokenizedInputs[1]); + } catch (StringIndexOutOfBoundsException | ArrayIndexOutOfBoundsException e) { + Ui.printDivider("Activity name cannot be empty."); + makeInfoLog("Edit command failed as there was no existing activity name provided."); + } + break; + case CLEAN: + try { + command = new CleanCommand(tokenizedInputs[1], this.cleaner, this.logcleaner); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("No keyword was given."); + } + break; + case CONTINUE: + try { + command = new ContinueCommand(tokenizedInputs[1]); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("Activity name cannot be empty."); + makeInfoLog("Continue command failed as there was no activity name provided."); + } + break; + case GRAPH: + try { + command = new GraphCommand(tokenizedInputs[1]); + } catch (NumberFormatException e) { + Ui.printDivider("Please input an integer for the time interval."); + } catch (ExtraParametersException e) { + Ui.printDivider("Extra parameters or invalid format detected."); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printDivider("Please specify whether you want to graph activities / tags / allocations."); + } + break; + case GOAL: + try { + if (tokenizedInputs.length == 1 || tokenizedInputs[1].isBlank() || tokenizedInputs[1] == null) { + command = new ViewGoalsCommand(null, this.tagStorage); + } else { + command = new GoalCommand(tokenizedInputs[1], scanner, this.tagStorage); + } + } catch (StringIndexOutOfBoundsException e) { + Ui.printDivider("Tag name cannot be empty."); + } + break; + default: + parseDefault(); + break; + } + return command; + } + + /** + * Method to parse user inputs that are not recognised. + */ + private void parseDefault() { + String line = "OOPS!!! I'm sorry, but I don't know what that means :-("; + makeInfoLog("Invalid command entered"); + Ui.printDivider(line); + } + + + /** + * Resets parameters, called when an activity is ended or aborted. + */ + public static void resetInfo() { + startTime = null; + activityName = null; + tags = new HashSet<>(); + allocatedTime = Duration.parse("PT0S"); + } +} \ No newline at end of file diff --git a/src/main/java/jikan/storage/Storage.java b/src/main/java/jikan/storage/Storage.java new file mode 100644 index 000000000..1d8bbea8c --- /dev/null +++ b/src/main/java/jikan/storage/Storage.java @@ -0,0 +1,103 @@ +package jikan.storage; + +import jikan.activity.ActivityList; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +/** + * Class that holds the path and File object for the current data file. + */ +public class Storage { + + /** Path to current data file. */ + public String dataFilePath; + + /** File object for current data file. */ + public File dataFile; + + /** Activity list linked to this storage file. */ + public ActivityList activityList; + + /** + * Constructs a Storage object for the input file path. + * + * @param dataFilePath The data file's file path. + */ + public Storage(String dataFilePath) { + assert dataFilePath != null : "dataFilePath must not be null"; + this.dataFilePath = dataFilePath; + dataFile = new File(dataFilePath); + assert dataFile instanceof File; + } + + /** + * Writes the input string to file. + * + * @param s The input string. + * @throws IOException If an error occurs while writing. + */ + public void writeToFile(String s) throws IOException { + FileWriter fw = new FileWriter(dataFilePath, true); + fw.write(s + System.lineSeparator()); + fw.close(); + } + + /** + * Loads the data file. Creates file and directories if data file did not previously exist. + * + * @return True if file previously existed (and was not created); False if file did not exist and was created. + */ + public boolean loadFile() throws IOException { + + // Create data file if it does not exist already + if (!dataFile.exists()) { + createDataFile(); + return false; // false = file didn't previously exist, so it was created + } + return true; // true = file previously existed, and was not created + } + + /** + * Creates a file and any non-existing directories to that file. + * + * @throws IOException If an error occurs while creating the file or directories. + */ + private void createDataFile() throws IOException { + dataFile.getParentFile().mkdirs(); // Create data directory (does nothing if directory already exists) + dataFile.createNewFile(); + } + + /** + * Creates ActivityList and loads data from data file if the data file previously existed. + * Otherwise, an empty task list is initialized. + * @return an ActivityList object containing a list of activities provided by the data file. + */ + public ActivityList createActivityList() { + try { + if (loadFile()) { + activityList = new ActivityList(this, dataFile); + } else { + activityList = new ActivityList(this); + } + } catch (IOException e) { + System.out.println("Error loading/creating data file."); + } + assert activityList instanceof ActivityList : "Method should return an ActivityList"; + return activityList; + } + + /** + * Clears the data file. + * @throws FileNotFoundException If file is not found. + */ + public void clearFile() throws FileNotFoundException { + PrintWriter writer = new PrintWriter(dataFile); + writer.print(""); + writer.close(); + } + +} diff --git a/src/main/java/jikan/storage/StorageHandler.java b/src/main/java/jikan/storage/StorageHandler.java new file mode 100644 index 000000000..3707c4192 --- /dev/null +++ b/src/main/java/jikan/storage/StorageHandler.java @@ -0,0 +1,85 @@ +package jikan.storage; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Class containing useful functions for modifying the data file. + */ +public class StorageHandler { + Storage storage; + + /** + * Constructs a StorageHandler object for the input Storage. + * + * @param storage The data file's file path. + */ + public StorageHandler(Storage storage) { + assert storage != null : "Input Storage must not be a null pointer"; + this.storage = storage; + } + + /** + * Removes the line whose index matches lineNumber from file at dataFilePath. + * + * @param lineNumber Index of line to remove. + * @param storage Storage object which contains path to save file. + * @throws IOException If an error occurs while writing the new list to file. + */ + public void removeLine(int lineNumber, Storage storage) throws IOException { + assert storage != null : "Input Storage must not be a null pointer"; + assert lineNumber >= 0 : "lineNumber cannot be negative"; + // Read file into list of strings, where each string is a line in the file + List fileContent = new ArrayList<>(Files.readAllLines(Paths.get(storage.dataFilePath), + StandardCharsets.UTF_8)); + + fileContent.remove(lineNumber); + + saveNewList(fileContent, storage.dataFile); + } + + /** + * Saves the updated activity list to a list of strings to write to the save file. + * @param activities New activity list. + * @param storage Storage object to obtain file path. + * @throws IOException If an error occurs while writing the new list to file. + */ + public void updateField(ArrayList activities, Storage storage) throws IOException { + assert storage != null : "Input Storage must not be a null pointer"; + List fileContent = new ArrayList<>(); + for (Activity a : activities) { + fileContent.add(a.toData()); + } + saveNewList(fileContent, storage.dataFile); + } + + /** + * Saves a the updated activity list to the csv file. + * + * @param newList The list containing the updated data. + * @param dataFile The file to save to. + * @throws IOException If an error occurs while writing the new list to file. + */ + public void saveNewList(List newList, File dataFile) throws IOException { + assert dataFile != null; + FileOutputStream fileOutputStream = new FileOutputStream(dataFile); + PrintWriter printWriter = new PrintWriter(fileOutputStream); + + for (String s : newList) { + printWriter.println(s); + } + printWriter.close(); + fileOutputStream.close(); + } +} diff --git a/src/main/java/jikan/ui/Ui.java b/src/main/java/jikan/ui/Ui.java new file mode 100644 index 000000000..54a559ff9 --- /dev/null +++ b/src/main/java/jikan/ui/Ui.java @@ -0,0 +1,278 @@ +package jikan.ui; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.parser.Parser; + +import java.text.DecimalFormat; +import java.time.Duration; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import static jikan.Jikan.lastShownList; + +public class Ui { + public static final String LOGO = "::::::::::: ::::::::::: ::: ::: ::: :::: :::\n" + + " :+: :+: :+: :+: :+: :+: :+:+: :+:\n" + + " +:+ +:+ +:+ +:+ +:+ +:+ :+:+:+ +:+\n" + + " +#+ +#+ +#++:++ +#++:++#++: +#+ +:+ +#+\n" + + " +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+#\n" + + "#+# #+# #+# #+# #+# #+# #+# #+# #+#+#\n" + + " ##### ########### ### ### ### ### ### ####"; + + public static final String WELCOMEMESSAGE = " Hello there! I'm Jikan, your trusty time tracker.\n" + + " What can I do for you today?"; + + public static final String DIVIDER = "-------------------------------" + + "-----------------------------------------------------------"; + + public static final int PROGRESSCONVERTER = 2; + + public static final int TOTALBARS = 50; + + private static final DecimalFormat DF2 = new DecimalFormat("#.##"); + + /** Prints the logo and greeting so users know the app is working. */ + public void printGreeting() { + System.out.println(LOGO); + System.out.println(DIVIDER); + System.out.println(WELCOMEMESSAGE); + System.out.println(DIVIDER); + } + + /** Prints exit message and exits the app. */ + public static void exitFromApp() { + System.out.println(DIVIDER); + System.out.println(" Bye! See you again."); + System.out.println(DIVIDER); + System.exit(0); + } + + /** Prints divider between user input and app feedback. */ + public static void printDivider(String line) { + System.out.println(DIVIDER); + System.out.println(line); + System.out.println(DIVIDER); + } + + private static void printTableFormat(ActivityList activityList, int index, boolean gotTags) { + long durationInNanos = (activityList.get(index).getDuration()).toNanos(); + long allocatedTimeInNanos = (activityList.get(index).getAllocatedTime()).toNanos(); + String duration = formatString(durationInNanos); + String allocatedTime = formatString(allocatedTimeInNanos); + String printIndex = String.valueOf(index + 1); + if (index < 9) { + printIndex = String.valueOf(index + 1) + " "; + } + if (gotTags) { + System.out.println(String.format("%s %s %-25s %s %-10s %s %-10s %s %-10s %s %s", + printIndex, "|", activityList.get(index).getName(), "|", duration, "|", + allocatedTime, "|", + activityList.get(index).getDate().toString(), "|", + activityList.get(index).getTags().toString())); + } else { + System.out.println(String.format("%s %s %-25s %s %-10s %s %-10s %s %-10s %s %s", + printIndex, "|", activityList.get(index).getName(), "|", duration, "|", + allocatedTime, "|", + activityList.get(index).getDate().toString(), "|", + "")); + } + } + + private static String formatString(long timeInNanos) { + return String.format("%02d:%02d:%02d", + TimeUnit.NANOSECONDS.toHours(timeInNanos), + TimeUnit.NANOSECONDS.toMinutes(timeInNanos) + - TimeUnit.HOURS.toMinutes(TimeUnit.NANOSECONDS.toHours(timeInNanos)), + TimeUnit.NANOSECONDS.toSeconds(timeInNanos) + - TimeUnit.MINUTES.toSeconds(TimeUnit.NANOSECONDS.toMinutes(timeInNanos))); + } + + /** + * Prints the results from a 'find' or 'filter' command. + * @param resultsList the list of activities to print + */ + public static void printResults(ActivityList resultsList) { + if (resultsList.activities.size() > 0) { + System.out.println(DIVIDER); + System.out.println("Here are the matching activities in your list:\n"); + System.out.println(String.format(" %s %-25s %s %-10s %s %-10s %s %s", + "|", "Name", "|", "Duration", "|", "Allocation","|", "Date", "|", "Tags")); + for (int i = 0; i < resultsList.getSize(); i++) { + if (resultsList.get(i).getTags() != null && !resultsList.get(i).getTags().isEmpty()) { + printTableFormat(resultsList, i, true); + } else { + printTableFormat(resultsList, i, false); + } + } + System.out.println(DIVIDER); + } else { + printDivider("There are no activities matching that description."); + } + } + + /** Prints all the activities in a list. */ + public static void printList(ActivityList activityList) { + System.out.println(DIVIDER); + System.out.println("Your completed activities:"); + System.out.println(String.format(" %s %-25s %s %-10s %s %-10s %s %-10s %s %s", + "|", "Name", "|", "Duration", "|", "Allocation", "|", "Date", "|", "Tags")); + for (int i = 0; i < activityList.getSize(); i++) { + if (activityList.get(i).getTags() != null && !activityList.get(i).getTags().isEmpty()) { + printTableFormat(activityList, i, true); + } else { + printTableFormat(activityList, i, false); + } + } + System.out.println(DIVIDER); + } + + /** + * Prints a graph of the last shown list. + * @param interval The time interval for the graph. + */ + public static void printActivityGraph(int interval) { + System.out.println(DIVIDER); + System.out.println(String.format("%-25s %s %s", "Name", "|", "Duration")); + for (int i = 0; i < lastShownList.getSize(); i++) { + Activity activity = lastShownList.get(i); + Duration duration = activity.getDuration(); + double minutes = duration.toMinutes() / (double) interval; + int scaledMinutes = (int) Math.round(minutes); + System.out.print(String.format("%-25s %s", activity.getName(), "|")); + for (int j = 0; j < scaledMinutes; j++) { + System.out.print("*"); + } + System.out.println(""); + } + System.out.println(DIVIDER); + } + + /** + * Prints a graph based on activity tags. + * @param tags The set of tags to be graphed. + */ + public static void printTagsGraph(HashMap tags, int interval) { + System.out.println(DIVIDER); + System.out.println(String.format("%-10s %s %s", "Tag", "|", "Duration")); + tags.forEach((key,value) -> { + double minutes = value.toMinutes() / interval; + int scaledMinutes = (int) Math.round(minutes); + System.out.print(String.format("%-10s %s", key, "|")); + for (int j = 0; j < scaledMinutes; j++) { + System.out.print("*"); + } + System.out.println(""); + }); + System.out.println(DIVIDER); + } + + /** Print goals as a table. + * Print goals as a table. + * @param tagsGoals the goals set for each tag. + * @param tagsActual the actual duration spent for each tag. + */ + public static void printGoals(HashMap tagsGoals, HashMap tagsActual) { + System.out.println(DIVIDER); + System.out.println(String.format(" %-15s %s %-15s %s %-15s %s %s", + "Tag", "|", "Goal", "|", "Actual", "|", "Duration left")); + tagsGoals.forEach((key, value) -> { + String message; + String goalDuration = convertDuration(tagsGoals.get(key)); + String actualDuration = convertDuration(tagsActual.get(key)); + Duration difference = tagsGoals.get(key).minus(tagsActual.get(key)); + String diffDuration = convertDuration(difference); + if (difference.isNegative()) { + if (diffDuration.equals("00:00:00")) { + message = " [You have met your goal!]"; + } else { + message = " [You have exceeded your goal!]"; + } + } else { + message = " [You have not met your goal!]"; + } + System.out.println(String.format(" %-15s %s %-15s %s %-15s %s %s", key, "|", goalDuration, + "|", actualDuration, "|", diffDuration + message)); + }); + System.out.println(DIVIDER); + } + + /** Prints a progress message and progress bar based on the percentage of allocate time achieved. + * @param percent percentage of allocated time achieved + */ + public static void printProgressMessage(double percent) { + System.out.println(DIVIDER); + if (percent < 100) { + System.out.println("Almost there ! Here's your progress:"); + } else { + System.out.println("Great job! Here's your progress:"); + } + int starsLeft = (int) (percent / PROGRESSCONVERTER); + System.out.print("Progress for " + Parser.activityName + ": "); + System.out.print("|"); + for (int i = 0; i < TOTALBARS; i++) { + if (starsLeft > 0) { + System.out.print("*"); + starsLeft--; + } else { + System.out.print(" "); + } + } + System.out.print("|"); + System.out.println((int) percent + "% completed"); + System.out.println(DIVIDER); + } + + + /** Method to print progress bar without message. + * @param percent percentage of allocated time achieved. + * @param activityName name of a particular activity. + */ + public static void printProgressBar(double percent, String activityName) { + int starsLeft = (int) (percent / PROGRESSCONVERTER); + String line = "Progress for " + activityName + ": "; + System.out.print(String.format("%-35s", line)); + System.out.print("|"); + for (int i = 0; i < TOTALBARS; i++) { + if (starsLeft > 0) { + System.out.print("*"); + starsLeft--; + } else { + System.out.print(" "); + } + } + System.out.println("| " + DF2.format(percent) + "%"); + } + + /** + * Method to graph out all the allocations. + * @param activityList a list of all activities. + */ + public static void graphAllocation(ActivityList activityList) { + System.out.println(DIVIDER); + for (int i = 0; i < activityList.getSize(); i++) { + if (activityList.get(i).getAllocatedTime() == Duration.parse("PT0S")) { + continue; + } + double percent = activityList.get(i).getProgressPercent(); + String activityName = activityList.get(i).getName(); + printProgressBar(percent, activityName); + } + System.out.println(DIVIDER); + } + + /** Converts duration object to a string for printing. + * @param dur the duration object. + * @return duration the duration as a string. + */ + public static String convertDuration(Duration dur) { + long durationInNanos = dur.toNanos(); + String duration = String.format("%02d:%02d:%02d", + TimeUnit.NANOSECONDS.toHours(durationInNanos), + TimeUnit.NANOSECONDS.toMinutes(durationInNanos) + - TimeUnit.HOURS.toMinutes(TimeUnit.NANOSECONDS.toHours(durationInNanos)), + TimeUnit.NANOSECONDS.toSeconds(durationInNanos) + - TimeUnit.MINUTES.toSeconds(TimeUnit.NANOSECONDS.toMinutes(durationInNanos))); + return duration; + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d5..000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/test/java/jikan/activity/ActivityListTest.java b/src/test/java/jikan/activity/ActivityListTest.java new file mode 100644 index 000000000..70b832623 --- /dev/null +++ b/src/test/java/jikan/activity/ActivityListTest.java @@ -0,0 +1,38 @@ +package jikan.activity; + +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.storage.Storage; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ActivityListTest { + + @Test + void getIndex() throws InvalidTimeFrameException, NameTooLongException { + ActivityList activities = new ActivityList(); + activities.storage = new Storage("data/activityList_test.txt"); + HashSet tags = new HashSet(); + tags.add("tag1"); + tags.add("tag2"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + + assertEquals(activity2, activities.get(1)); + + activities.storage.dataFile.delete(); + } +} \ No newline at end of file diff --git a/src/test/java/jikan/activity/ActivityTest.java b/src/test/java/jikan/activity/ActivityTest.java new file mode 100644 index 000000000..956892b83 --- /dev/null +++ b/src/test/java/jikan/activity/ActivityTest.java @@ -0,0 +1,47 @@ +package jikan.activity; + +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ActivityTest { + + HashSet tags = new HashSet(); + //the tags is empty here as adding needs to be done in a method. + Activity activity; + + { + try { + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + activity = new Activity("Activity", startTime, endTime, duration, tags, allocatedTime); + } catch (InvalidTimeFrameException e) { + e.printStackTrace(); + } catch (NameTooLongException e) { + e.printStackTrace(); + } + } + + @Test + void getDuration() { + assertEquals("PT2H", activity.getDuration().toString()); + } + + @Test + void getName() { + assertEquals("Activity", activity.getName()); + } + + @Test + void getTags() { + assertEquals(tags, activity.getTags()); + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/AbortCommandTest.java b/src/test/java/jikan/command/AbortCommandTest.java new file mode 100644 index 000000000..0b99aef5b --- /dev/null +++ b/src/test/java/jikan/command/AbortCommandTest.java @@ -0,0 +1,26 @@ +package jikan.command; + +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.parser.Parser; +import jikan.ui.Ui; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertNull; + +class AbortCommandTest { + @Test + void executeAbort() { + Parser.startTime = LocalDateTime.now(); + Command command = new AbortCommand(null); + try { + command.executeCommand(null); + assertNull(Parser.startTime); + } catch (EmptyNameException | ExtraParametersException e) { + System.out.println("Filed error."); + } + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/CleanCommandTest.java b/src/test/java/jikan/command/CleanCommandTest.java new file mode 100644 index 000000000..5f361a5a3 --- /dev/null +++ b/src/test/java/jikan/command/CleanCommandTest.java @@ -0,0 +1,93 @@ +package jikan.command; + +import jikan.cleaner.LogCleaner; +import jikan.cleaner.StorageCleaner; +import jikan.exception.InvalidCleanCommandException; +import jikan.exception.NegativeNumberException; +import jikan.storage.Storage; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CleanCommandTest { + + Storage storage = new Storage("data/test.csv"); + StorageCleaner storageCleaner = new StorageCleaner(storage); + LogCleaner logCleaner = new LogCleaner(); + + @Test + void checkIfParameterEmpty() { + CleanCommand cleanCommand = new CleanCommand("", storageCleaner, logCleaner); + boolean isEmpty = cleanCommand.isParameterEmpty(); + assertEquals(isEmpty,true); + cleanCommand = new CleanCommand(" ", storageCleaner, logCleaner); + isEmpty = cleanCommand.isParameterEmpty(); + assertEquals(isEmpty, true); + cleanCommand = new CleanCommand(" abc", storageCleaner, logCleaner); + isEmpty = cleanCommand.isParameterEmpty(); + assertEquals(isEmpty, false); + cleanCommand = new CleanCommand("abc ", storageCleaner, logCleaner); + isEmpty = cleanCommand.isParameterEmpty(); + assertEquals(isEmpty, false); + cleanCommand = new CleanCommand(" abc ", storageCleaner, logCleaner); + isEmpty = cleanCommand.isParameterEmpty(); + assertEquals(isEmpty, false); + } + + @Test + void checkGetFirstWord() { + CleanCommand cleanCommand = new CleanCommand("", storageCleaner, logCleaner); + String firstWord = cleanCommand.getFirstWord(""); + assertEquals(firstWord,""); + firstWord = cleanCommand.getFirstWord("abc"); + assertEquals(firstWord, "abc"); + firstWord = cleanCommand.getFirstWord("abc def geh"); + assertEquals(firstWord, "abc"); + firstWord = cleanCommand.getFirstWord("ab c"); + assertEquals(firstWord, "ab"); + } + + @Test + void checkGetRemainingParameter() { + CleanCommand cleanCommand = new CleanCommand("log on", storageCleaner, logCleaner); + String remainingParameter = cleanCommand.getRemainingParameter("log"); + assertEquals(remainingParameter, "on"); + cleanCommand = new CleanCommand("log on", storageCleaner, logCleaner); + remainingParameter = cleanCommand.getRemainingParameter("log"); + assertEquals(remainingParameter, "on"); + cleanCommand = new CleanCommand("log on /n", storageCleaner, logCleaner); + remainingParameter = cleanCommand.getRemainingParameter("log"); + assertEquals(remainingParameter, "on /n"); + cleanCommand = new CleanCommand("log on 123 ", storageCleaner, logCleaner); + remainingParameter = cleanCommand.getRemainingParameter("log"); + assertEquals(remainingParameter, "on 123"); + cleanCommand = new CleanCommand("log", storageCleaner, logCleaner); + remainingParameter = cleanCommand.getRemainingParameter("log"); + assertEquals(remainingParameter, ""); + } + + @Test + void checkGetNumber() { + CleanCommand cleanCommand = new CleanCommand("", storageCleaner, logCleaner); + Assertions.assertThrows(InvalidCleanCommandException.class, () -> { + cleanCommand.getNumber("123 "); + }); + Assertions.assertThrows(NegativeNumberException.class, () -> { + cleanCommand.getNumber("-123"); + }); + try { + int number = cleanCommand.getNumber("123"); + assertEquals(number, 123); + } catch (InvalidCleanCommandException | NegativeNumberException e) { + // Should not reach here + } + try { + int number = cleanCommand.getNumber("0"); + assertEquals(number, 0); + } catch (InvalidCleanCommandException | NegativeNumberException e) { + // Should not reach here + } + } +} diff --git a/src/test/java/jikan/command/ContinueCommandTest.java b/src/test/java/jikan/command/ContinueCommandTest.java new file mode 100644 index 000000000..3bde1e22d --- /dev/null +++ b/src/test/java/jikan/command/ContinueCommandTest.java @@ -0,0 +1,80 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.parser.Parser; +import jikan.storage.Storage; +import jikan.ui.Ui; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ContinueCommandTest { + Storage storage = new Storage("data/activityList_test.txt"); + ActivityList activities = new ActivityList(storage); + HashSet tags = new HashSet<>(); + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags.add("tag1"); + tags.add("tag2"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + private void resetFields() { + Parser.startTime = null; + Parser.tags.clear(); + } + + @Test + void executeContinue() throws InterruptedException { + try { + populateActivityList(); + String parameters = "Activity2"; + Command command = new ContinueCommand(parameters); + command.executeCommand(activities); + + LocalDateTime startTime = LocalDateTime.now(); + assertEquals(startTime.getMinute(), Parser.startTime.getMinute()); + final Duration initial = activities.get(1).getDuration(); + Thread.sleep(2000); + + resetFields(); + // End Activity2 + command = new EndCommand(null); + command.executeCommand(activities); + Duration elapsed = initial.plus(Duration.between(startTime, LocalDateTime.now())); + Duration duration = activities.get(1).getDuration(); + assertEquals(elapsed.toMinutes(), duration.toMinutes()); + } catch (EmptyNameException | InvalidTimeFrameException | ExtraParametersException e) { + System.out.println("Error."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + } + +} \ No newline at end of file diff --git a/src/test/java/jikan/command/DeleteCommandTest.java b/src/test/java/jikan/command/DeleteCommandTest.java new file mode 100644 index 000000000..e2c4f85b4 --- /dev/null +++ b/src/test/java/jikan/command/DeleteCommandTest.java @@ -0,0 +1,70 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.storage.Storage; +import jikan.ui.Ui; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DeleteCommandTest { + + Storage storage = new Storage("data/activityList_test.txt"); + ActivityList activities = new ActivityList(storage); + HashSet tags = new HashSet<>(); + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags.add("tag1"); + tags.add("tag2"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + @Test + void executeDelete() { + try { + populateActivityList(); + } catch (InvalidTimeFrameException e) { + System.out.println("Invalid time frame."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + String parameters = "Activity2"; + + Command command = new DeleteCommand(parameters); + try { + command.executeCommand(activities); + } catch (EmptyNameException | ExtraParametersException e) { + System.out.println("Field error."); + } + + assertEquals(activities.get(1).getName(), "Activity3"); + assertEquals(activities.getSize(), 2); + } + +} \ No newline at end of file diff --git a/src/test/java/jikan/command/EditCommandTest.java b/src/test/java/jikan/command/EditCommandTest.java new file mode 100644 index 000000000..13ba3f0a4 --- /dev/null +++ b/src/test/java/jikan/command/EditCommandTest.java @@ -0,0 +1,90 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.storage.Storage; +import jikan.ui.Ui; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EditCommandTest { + + Storage storage = new Storage("data/activityList_test.txt"); + ActivityList activities = new ActivityList(storage); + HashSet tags = new HashSet<>(); + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags.add("tag1"); + tags.add("tag2"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + @Test + void executeEditName() { + try { + populateActivityList(); + } catch (InvalidTimeFrameException e) { + System.out.println("Invalid time frame."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + String parameters = "Activity2 /en Activity4"; + Command command = new EditCommand(parameters); + try { + command.executeCommand(activities); + } catch (EmptyNameException | ExtraParametersException | NullPointerException e) { + System.out.println("Field error."); + return; // Only needed for UNIX test + } + + assertEquals(activities.get(1).getName(), "Activity4"); + } + + @Test + void executeEditAllocatedTime() { + try { + populateActivityList(); + } catch (InvalidTimeFrameException e) { + System.out.println("Invalid time frame."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + String parameters = "Activity2 /ea 10:10:10"; + Command command = new EditCommand(parameters); + try { + command.executeCommand(activities); + } catch (EmptyNameException | ExtraParametersException | NullPointerException e) { + System.out.println("Field error."); + return; // Only needed for UNIX test + } + + assertEquals(activities.get(1).getAllocatedTime(), Duration.parse("PT10H10M10S")); + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/FilterCommandTest.java b/src/test/java/jikan/command/FilterCommandTest.java new file mode 100644 index 000000000..ba0073860 --- /dev/null +++ b/src/test/java/jikan/command/FilterCommandTest.java @@ -0,0 +1,119 @@ +package jikan.command; + +import jikan.Jikan; +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.exception.MultipleDelimitersException; +import jikan.storage.Storage; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class FilterCommandTest { + + private static final String BASIC_TEST = "tagA"; + private static final String NULL_RESULTS_TEST = "tagZ"; + private static final String MULTI_KEYWORD_TEST = "tagA tagC tagD"; + private static final String FIND_CHAINING_TEST = "subject2 / subject3"; + private static final String CHAINING_TEST = "-s tagA tagB"; + + ActivityList activities = new ActivityList(); + HashSet tags1 = new HashSet<>(); + HashSet tags2 = new HashSet<>(); + ArrayList expected = new ArrayList<>(); + Activity activity1; + Activity activity2; + Activity activity3; + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + activities.storage = new Storage("data/activityList_test.txt"); + activities.activities.clear(); + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags1.add("tagA"); + tags1.add("tagB"); + tags2.add("tagC"); + tags2.add("tagD"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + activity1 = new Activity("subject1 quiz", startTime, endTime, duration, tags1, allocatedTime); + activity2 = new Activity("subject2 quiz", startTime, endTime, duration, tags1, allocatedTime); + activity3 = new Activity("subject3 final", startTime, endTime, duration, tags2, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + void populateExpectedBasic() { + expected.clear(); + expected.add(activity1); + expected.add(activity2); + } + + void populateExpectedNull() { + expected.clear(); + } + + void populateExpectedMultiKeyword() { + expected.clear(); + expected.add(activity1); + expected.add(activity2); + expected.add(activity3); + } + + void populateExpectedChaining() { + expected.clear(); + expected.add(activity2); + } + + @Test + void executeCommand() { + try { + populateActivityList(); + + Command basicTest = new FilterCommand(BASIC_TEST); + basicTest.executeCommand(activities); + populateExpectedBasic(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command nullResultsTest = new FilterCommand(NULL_RESULTS_TEST); + nullResultsTest.executeCommand(activities); + populateExpectedNull(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command multiKeywordTest = new FilterCommand(MULTI_KEYWORD_TEST); + multiKeywordTest.executeCommand(activities); + populateExpectedMultiKeyword(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command find = new FindCommand(FIND_CHAINING_TEST); + Command chainingTest = new FilterCommand(CHAINING_TEST); + find.executeCommand(activities); + chainingTest.executeCommand(activities); + populateExpectedChaining(); + assertEquals(Jikan.lastShownList.activities, expected); + + } catch (InvalidTimeFrameException | EmptyNameException | ExtraParametersException | NameTooLongException e) { + System.out.println("Field error."); + } catch (MultipleDelimitersException e) { + System.out.println("Multiple Delimiters"); + } + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/FindCommandTest.java b/src/test/java/jikan/command/FindCommandTest.java new file mode 100644 index 000000000..75bdbefe9 --- /dev/null +++ b/src/test/java/jikan/command/FindCommandTest.java @@ -0,0 +1,118 @@ +package jikan.command; + +import jikan.Jikan; +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.exception.MultipleDelimitersException; +import jikan.storage.Storage; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class FindCommandTest { + + private static final String BASIC_TEST = "quiz"; + private static final String NULL_RESULTS_TEST = "popquiz"; + private static final String MULTI_KEYWORD_TEST = "subject2 / final"; + private static final String FILTER_CHAINING_TEST = "tagC tagD"; + private static final String CHAINING_TEST = "-s quiz"; + + ActivityList activities = new ActivityList(); + HashSet tagSetVersion1 = new HashSet<>(); + HashSet tagsSetVersion2 = new HashSet<>(); + ArrayList expected = new ArrayList<>(); + Activity activity1; + Activity activity2; + Activity activity3; + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + activities.storage = new Storage("data/activityList_test.txt"); + activities.activities.clear(); + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tagSetVersion1.add("tagA"); + tagSetVersion1.add("tagB"); + tagsSetVersion2.add("tagC"); + tagsSetVersion2.add("tagD"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + activity1 = new Activity("subject1 quiz", startTime, endTime, duration, tagSetVersion1, allocatedTime); + activity2 = new Activity("subject2 quiz", startTime, endTime, duration, tagsSetVersion2, allocatedTime); + activity3 = new Activity("subject1 final", startTime, endTime, duration, tagsSetVersion2, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + void populateExpectedBasic() { + expected.clear(); + expected.add(activity1); + expected.add(activity2); + } + + void populateExpectedNull() { + expected.clear(); + } + + void populateExpectedMultiKeyword() { + expected.clear(); + expected.add(activity2); + expected.add(activity3); + } + + void populateExpectedChaining() { + expected.clear(); + expected.add(activity2); + } + + @Test + void executeCommand() { + try { + populateActivityList(); + + Command basicTest = new FindCommand(BASIC_TEST); + basicTest.executeCommand(activities); + populateExpectedBasic(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command nullResultsTest = new FindCommand(NULL_RESULTS_TEST); + nullResultsTest.executeCommand(activities); + populateExpectedNull(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command multiKeywordTest = new FindCommand(MULTI_KEYWORD_TEST); + multiKeywordTest.executeCommand(activities); + populateExpectedMultiKeyword(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command filter = new FilterCommand(FILTER_CHAINING_TEST); + Command chainingTest = new FindCommand(CHAINING_TEST); + filter.executeCommand(activities); + chainingTest.executeCommand(activities); + populateExpectedChaining(); + assertEquals(Jikan.lastShownList.activities, expected); + + } catch (InvalidTimeFrameException | EmptyNameException | ExtraParametersException | NameTooLongException e) { + System.out.println("Field error."); + } catch (MultipleDelimitersException e) { + System.out.println("Multiple delimiters"); + } + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/GoalCommandTest.java b/src/test/java/jikan/command/GoalCommandTest.java new file mode 100644 index 000000000..1af4368e7 --- /dev/null +++ b/src/test/java/jikan/command/GoalCommandTest.java @@ -0,0 +1,85 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.storage.Storage; +import jikan.ui.Ui; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GoalCommandTest { + Storage storage = new Storage("data/activityList_test.txt"); + ActivityList activities = new ActivityList(storage); + HashSet tags = new HashSet<>(); + Scanner scanner; + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags.add("tag1"); + tags.add("tag2"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + @Test + void executeGoal() throws IOException { + try { + populateActivityList(); + } catch (InvalidTimeFrameException e) { + System.out.println("Invalid time frame."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + String parameters = "tag1 /g 10:10:10"; + String tagName = "tag1"; + Storage tagStorage = new Storage("data/tag_test.txt"); + String testFile = "data/tag_test.txt"; + tagStorage.loadFile(); + assertTrue(tagStorage.dataFile.exists()); + boolean found = false; + Command command = new GoalCommand(parameters, scanner, tagStorage); + try { + command.executeCommand(activities); + if (GoalCommand.checkIfExists(tagName, testFile) == -1) { + found = false; + assertFalse(found); + tagStorage.dataFile.delete(); + } else { + found = true; + assertTrue(found); + tagStorage.dataFile.delete(); + } + } catch (EmptyNameException | ExtraParametersException e) { + System.out.println("Field error."); + } + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/GraphCommandTest.java b/src/test/java/jikan/command/GraphCommandTest.java new file mode 100644 index 000000000..0dacada53 --- /dev/null +++ b/src/test/java/jikan/command/GraphCommandTest.java @@ -0,0 +1,93 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.storage.Storage; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.HashSet; + +import static jikan.Jikan.lastShownList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GraphCommandTest { + ActivityList activities = new ActivityList(); + HashSet tags1 = new HashSet<>(); + HashSet tags2 = new HashSet<>(); + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + activities.storage = new Storage("data/activityList_test.txt"); + activities.activities.clear(); + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags1.add("tag1"); + tags1.add("tag2"); + tags2.add("tag1"); + tags2.add("tag3"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT2H30M"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags1, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags1, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags2, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + @Test + void newCommand() { + ExtraParametersException extraParametersException = assertThrows(ExtraParametersException.class, () -> { + Command command = new GraphCommand("tags 10 extra"); + }); + } + + public void printTest() throws Exception { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + + // After this all System.out.println() statements will come to outContent stream. + + // So, you can normally call, + // print(items); // I will assume items is already initialized properly. + + //Now you have to validate the output. Let's say items had 1 element. + // With name as FirstElement and number as 1. + // String expectedOutput = "Name: FirstElement\nNumber: 1" // Notice the \n for new line. + + // Do the actual assertion. + // assertEquals(expectedOutput, outContent.toString()); + } + + @Test + void extractTags() { + HashMap expected = new HashMap<>(); + expected.put("tag1", Duration.parse("PT6H")); + expected.put("tag2", Duration.parse("PT4H")); + expected.put("tag3", Duration.parse("PT2H")); + try { + populateActivityList(); + HashMap tags = new HashMap<>(); + for (Activity activity : activities.activities) { + GraphCommand.extractTags(tags, activity); + } + assertEquals(expected, tags); + } catch (NameTooLongException | InvalidTimeFrameException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/ListCommandTest.java b/src/test/java/jikan/command/ListCommandTest.java new file mode 100644 index 000000000..aefd49cc8 --- /dev/null +++ b/src/test/java/jikan/command/ListCommandTest.java @@ -0,0 +1,170 @@ +package jikan.command; + +import jikan.Jikan; +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.storage.Storage; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ListCommandTest { + + private static final String DATE_FORMAT_1 = "01/01/2020"; + private static final String DATE_FORMAT_2 = "2020-01-01"; + private static final String DATE_RANGE = "01/01/2020 20/02/2020"; + private static final String DAY_FORMAT_1 = "day"; + private static final String DAY_FORMAT_2 = "daily"; + private static final String WEEK_FORMAT_1 = "week"; + private static final String WEEK_FORMAT_2 = "weekly"; + + private static final LocalDateTime START_TIME_1 = LocalDateTime.parse("2020-01-01T08:00:00"); + private static final LocalDateTime END_TIME_1 = LocalDateTime.parse("2020-01-01T10:00:00"); + private static final LocalDateTime START_TIME_2 = LocalDateTime.parse("2020-01-15T08:00:00"); + private static final LocalDateTime END_TIME_2 = LocalDateTime.parse("2020-01-15T10:00:00"); + private static final LocalDateTime START_TIME_3 = LocalDateTime.parse("2020-03-01T08:00:00"); + private static final LocalDateTime END_TIME_3 = LocalDateTime.parse("2020-03-01T10:00:00"); + + + ActivityList activities = new ActivityList(); + ArrayList expected = new ArrayList<>(); + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime nextWeek = currentTime.plusWeeks(1); + Activity activity1; + Activity activity2; + Activity activity3; + Activity activity4; + Activity activity5; + Activity activity6; + + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + activities.storage = new Storage("data/activityList_test.txt"); + activities.activities.clear(); + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + + HashSet tags = new HashSet<>(); + tags.add("tagA"); + tags.add("tagB"); + + Duration duration = Duration.between(START_TIME_1, END_TIME_1); + Duration allocatedTime = Duration.parse("PT0S"); + activity1 = new Activity("subject1 quiz", START_TIME_1, END_TIME_1, duration, tags, allocatedTime); + activity2 = new Activity("subject2 quiz", START_TIME_1, END_TIME_1, duration, tags, allocatedTime); + activity3 = new Activity("subject3 final", START_TIME_2, END_TIME_2, duration, tags, allocatedTime); + activity4 = new Activity("subject4 final", START_TIME_3, END_TIME_3, duration, tags, allocatedTime); + activity5 = new Activity("subject5 quiz", currentTime, currentTime, duration, tags, allocatedTime); + activity6 = new Activity("subject6 final", currentTime.plusDays(1), + currentTime.plusDays(1), duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + activities.add(activity4); + activities.add(activity5); + activities.add(activity6); + + } + + void populateExpectedBasic() { + expected.clear(); + expected.add(activity1); + expected.add(activity2); + expected.add(activity3); + expected.add(activity4); + expected.add(activity5); + expected.add(activity6); + } + + void populateExpectedSingleDate() { + expected.clear(); + expected.add(activity1); + expected.add(activity2); + } + + void populateExpectedDateRange() { + expected.clear(); + expected.add(activity1); + expected.add(activity2); + expected.add(activity3); + } + + void populateExpectedDay() { + expected.clear(); + expected.add(activity5); + } + + void populateExpectedWeek() { + expected.clear(); + expected.add(activity5); + if (activity6.getDate().getDayOfWeek() != DayOfWeek.MONDAY) { + expected.add(activity6); + } + } + + @Test + void executeCommand() { + try { + populateActivityList(); + + Command basicTest = new ListCommand(null); + basicTest.executeCommand(activities); + populateExpectedBasic(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command dateFormat1Test = new ListCommand(DATE_FORMAT_1); + dateFormat1Test.executeCommand(activities); + populateExpectedSingleDate(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command dateFormat2Test = new ListCommand(DATE_FORMAT_2); + dateFormat2Test.executeCommand(activities); + populateExpectedSingleDate(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command dateRangeTest = new ListCommand(DATE_RANGE); + dateRangeTest.executeCommand(activities); + populateExpectedDateRange(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command dayFormat1Test = new ListCommand(DAY_FORMAT_1); + dayFormat1Test.executeCommand(activities); + populateExpectedDay(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command dayFormat2Test = new ListCommand(DAY_FORMAT_2); + dayFormat2Test.executeCommand(activities); + populateExpectedDay(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command weekFormat1Test = new ListCommand(WEEK_FORMAT_1); + weekFormat1Test.executeCommand(activities); + populateExpectedWeek(); + assertEquals(Jikan.lastShownList.activities, expected); + + Command weekFormat2Test = new ListCommand(WEEK_FORMAT_2); + weekFormat2Test.executeCommand(activities); + populateExpectedWeek(); + assertEquals(Jikan.lastShownList.activities, expected); + + } catch (InvalidTimeFrameException | EmptyNameException | ExtraParametersException | NameTooLongException e) { + System.out.println("Field error."); + } + } +} \ No newline at end of file diff --git a/src/test/java/jikan/command/StartCommandTest.java b/src/test/java/jikan/command/StartCommandTest.java new file mode 100644 index 000000000..d6025b1c8 --- /dev/null +++ b/src/test/java/jikan/command/StartCommandTest.java @@ -0,0 +1,128 @@ +package jikan.command; + +import jikan.activity.Activity; +import jikan.activity.ActivityList; +import jikan.exception.EmptyNameException; +import jikan.exception.ExtraParametersException; +import jikan.exception.InvalidTimeFrameException; +import jikan.exception.NameTooLongException; +import jikan.log.Log; +import jikan.parser.Parser; +import jikan.storage.Storage; +import jikan.ui.Ui; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class StartCommandTest { + ActivityList activities = new ActivityList(); + HashSet tags = new HashSet<>(); + + void populateActivityList() throws InvalidTimeFrameException, NameTooLongException { + activities.storage = new Storage("data/activityList_test.txt"); + activities.activities.clear(); + try { + activities.storage.clearFile(); + } catch (FileNotFoundException e) { + System.out.println("Could not find file."); + } + tags.add("tag1"); + tags.add("tag2"); + LocalDateTime startTime = LocalDateTime.parse("2020-01-01T08:00:00"); + LocalDateTime endTime = LocalDateTime.parse("2020-01-01T10:00:00"); + Duration duration = Duration.between(startTime, endTime); + Duration allocatedTime = Duration.parse("PT0S"); + Activity activity1 = new Activity("Activity1", startTime, endTime, duration, tags, allocatedTime); + Activity activity2 = new Activity("Activity2", startTime, endTime, duration, tags, allocatedTime); + Activity activity3 = new Activity("Activity3", startTime, endTime, duration, tags, allocatedTime); + activities.add(activity1); + activities.add(activity2); + activities.add(activity3); + } + + private void resetFields() { + Parser.startTime = null; + Parser.tags.clear(); + } + + @Test + void executeStart() { + try { + populateActivityList(); + Scanner scanner = new Scanner(System.in); + String parameters = "Activity 3 /t tag tag1"; + Command command = new StartCommand(parameters, scanner); + + HashSet activity3Tags = new HashSet<>(); + activity3Tags.add("tag"); + activity3Tags.add("tag1"); + + command.executeCommand(activities); + assertNotNull(Parser.startTime); + assertEquals(Parser.activityName, "Activity 3"); + assertEquals(activity3Tags, Parser.tags); + + resetFields(); + // end started activity to test continue feature + command = new EndCommand(null); + command.executeCommand(activities); + } catch (EmptyNameException | InvalidTimeFrameException e) { + System.out.println("Field error."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } catch (ExtraParametersException e) { + Ui.printDivider("Field error."); + } + } + + @Test + void executeStartContinued() { + try { + populateActivityList(); + String data = "Yes"; + System.setIn(new ByteArrayInputStream(data.getBytes())); + Scanner scanner = new Scanner(System.in); + String parameters = "Activity1"; + Command command = new StartCommand(parameters, scanner); + System.out.println(Parser.tags); + command.executeCommand(activities); + assertEquals(Parser.activityName, "Activity1"); + assertNotNull(Parser.startTime); + } catch (InvalidTimeFrameException | EmptyNameException | ExtraParametersException e) { + System.out.println("Field error."); + } catch (NameTooLongException e) { + Log.makeInfoLog("Activity name longer than 25 characters"); + Ui.printDivider("Error: activity name is longer than 25 characters."); + } + resetFields(); + } + + @Test + void executeStartNotContinued() { + try { + populateActivityList(); + String data = "No"; + System.setIn(new ByteArrayInputStream(data.getBytes())); + Scanner scanner = new Scanner(System.in); + String parameters = "Activity1"; + Command command = new StartCommand(parameters, scanner); + command.executeCommand(activities); + assertNull(Parser.startTime); + assertNull(Parser.activityName); + } catch (InvalidTimeFrameException | EmptyNameException | ExtraParametersException + | NameTooLongException e) { + System.out.println("Field error."); + } + resetFields(); + } +} \ No newline at end of file diff --git a/src/test/java/jikan/parser/ExceptionTest.java b/src/test/java/jikan/parser/ExceptionTest.java new file mode 100644 index 000000000..8b64e843f --- /dev/null +++ b/src/test/java/jikan/parser/ExceptionTest.java @@ -0,0 +1,44 @@ +package jikan.parser; + +import jikan.activity.ActivityList; +import jikan.command.AbortCommand; +import jikan.command.Command; +import jikan.command.EndCommand; +import jikan.exception.EmptyNameException; +import jikan.exception.NoSuchActivityException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Scanner; + +class ExceptionTest { + + /* + + Scanner scanner = new Scanner(System.in); + ActivityList activityList = new ActivityList(); + + @Test + public void testEmptyNameException() { + Assertions.assertThrows(EmptyNameException.class, () -> { + Parser.tokenizedInputs = new String[]{"start", ""}; + Parser.parseStart(activityList, scanner); + }); + } + + @Test + public void testNoSuchActivityException() { + Assertions.assertThrows(NoSuchActivityException.class, () -> { + Parser.startTime = null; + Command endCommand = new EndCommand(null); + endCommand.executeCommand(activityList); + Command abortCommand = new AbortCommand(null); + abortCommand.executeCommand(activityList); + }); + + } + + */ +} + + diff --git a/src/test/java/jikan/storage/StorageHandlerTest.java b/src/test/java/jikan/storage/StorageHandlerTest.java new file mode 100644 index 000000000..7bd0d4cb9 --- /dev/null +++ b/src/test/java/jikan/storage/StorageHandlerTest.java @@ -0,0 +1,82 @@ +package jikan.storage; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StorageHandlerTest { + + @Test + public void removeLine() throws IOException { + // Generate random suffix for file + // (quick solution to avoid conflicts with tests in StorageTest + int random = (int )(Math.random() * 500 + 1); + String filepath = "data/test" + random + ".txt"; + Storage storage = new Storage(filepath); + storage.loadFile(); + + String line1 = "a"; + String line2 = "b"; + String line3 = "c"; + String writtenString = ""; + + storage.writeToFile(line1); + storage.writeToFile(line2); + storage.writeToFile(line3); + + StorageHandler storageHandler = new StorageHandler(storage); + storageHandler.removeLine(1, storage); + + int i = 0; + Scanner dataScanner = new Scanner(storage.dataFile); + while (dataScanner.hasNext()) { + writtenString = dataScanner.nextLine(); + if (i == 0) { + assertEquals(line1, writtenString); + } else { + assertEquals(line3, writtenString); + } + i++; + } + storage.dataFile.delete(); + } + + @Test + public void test_replaceLine() throws IOException { + List list = Arrays.asList("1. a", "2. b", "10. c"); + + String replace = "This string has been replaced."; + + // Generate random suffix for file + // (quick solution to avoid conflicts with tests in StorageTest) + int random = (int )(Math.random() * 500 + 1); + String filepath = "data/test" + random + ".txt"; + Storage storage = new Storage(filepath); + storage.loadFile(); + + for (int i = 0; i < list.size(); i++) { + storage.writeToFile(list.get(i)); + } + + int j = 0; + Scanner dataScanner = new Scanner(storage.dataFile); + //StorageHandler.replaceLine(2, replace, storage.dataFilePath); + + String replacedString = ""; + + while (dataScanner.hasNext()) { + replacedString = dataScanner.nextLine(); + // Check second line + if (j == 1) { + assertEquals(replace, replacedString); + j++; + } + } + storage.dataFile.delete(); + } +} diff --git a/src/test/java/jikan/storage/StorageTest.java b/src/test/java/jikan/storage/StorageTest.java new file mode 100644 index 000000000..3e2647db5 --- /dev/null +++ b/src/test/java/jikan/storage/StorageTest.java @@ -0,0 +1,74 @@ +package jikan.storage; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StorageTest { + + @Test + public void loadFile_fileDoesNotExist() throws IOException { + Storage storage = new Storage("data/test.txt"); + // First time calling loadFile, so file does not exist yet + assertFalse(storage.loadFile()); + storage.dataFile.delete(); + } + + @Test + public void loadFile_fileExists() throws IOException { + Storage storage = new Storage("data/test.txt"); + + // First time calling loadFile, so file does not exist yet + storage.loadFile(); + + // loadFile was called once already, so now file exists + assertTrue(storage.loadFile()); + + storage.dataFile.delete(); + } + + @Test + public void testCreateDataFile() throws IOException { + Storage storage = new Storage("data/test.txt"); + storage.loadFile(); + assertTrue(storage.dataFile.exists()); + storage.dataFile.delete(); + } + + @Test + public void testCreateDataFile_IoException() throws IOException { + Storage storage = new Storage("//\\-@#4/\\/**3"); + + IOException thrown = assertThrows( + IOException.class, + storage::loadFile, + "IOException during file creation" + ); + storage.dataFile.delete(); + } + + @Test + public void testWriteToFile() throws IOException { + // Generate random suffix for file + // (quick solution to avoid conflicts with other tests) + int random = (int )(Math.random() * 500 + 1); + String filepath = "data/test" + random + ".txt"; + Storage storage = new Storage(filepath); + storage.loadFile(); + String originalString = "This is a test string."; + String writtenString = ""; + storage.writeToFile(originalString); + Scanner dataScanner = new Scanner(storage.dataFile); + while (dataScanner.hasNext()) { + writtenString = dataScanner.nextLine(); + } + assertEquals(originalString, writtenString); + storage.dataFile.delete(); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 495ab98a3..000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} \ No newline at end of file diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae..00611a464 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,74 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +::::::::::: ::::::::::: ::: ::: ::: :::: ::: + :+: :+: :+: :+: :+: :+: :+:+: :+: + +:+ +:+ +:+ +:+ +:+ +:+ :+:+:+ +:+ + +#+ +#+ +#++:++ +#++:++#++: +#+ +:+ +#+ + +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+# +#+# #+# #+# #+# #+# #+# #+# #+# #+#+# + ##### ########### ### ### ### ### ### #### +------------------------------------------------------------------------------------------ + Hello there! I'm Jikan, your trusty time tracker. + What can I do for you today? +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 1 was started. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 1 was ended. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 2 was started. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 2 was ended. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 3 was started. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 3 is ongoing! +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 3 was ended. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 1 was continued. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 1 was ended. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +There is already an activity with this name. Would you like to continue it? +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 1 was continued. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +activity 1 was ended. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +Please specify whether you want to graph activities / tags / allocations. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +Activity name cannot be empty. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +You have not started any activity! +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +You have not started any activity. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +OOPS!!! I'm sorry, but I don't know what that means :-( +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +Activity name cannot be empty. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +Activity named activity 2 has been updated. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ +You have deleted activity 5. +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ + Bye! See you again. +------------------------------------------------------------------------------------------ diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f9..316675c9c 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,21 @@ -James Gosling \ No newline at end of file +start activity 1 /t tag +end +start activity 2 /t tag +end +start activity 3 /t tag1 +start activity 4 +end +continue activity 1 +end +start activity 1 +yes +end +graph +start +end +abort +hi +delete +edit activity 2 /en activity 5 +delete activity 5 +bye \ No newline at end of file diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh index 8d94369a0..29fc8b54d 100755 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -18,4 +18,4 @@ then else echo "Test failed!" exit 1 -fi +fi \ No newline at end of file