diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index 3f303c5cf..65a8e9cdb 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -14,7 +14,7 @@ image::seanieyap.png[width="150", align="left"] {empty}[https://github.com/seanieyap[Github]] [<>] [https://seanieyap.com[Personal Website]] Role: Team Lead, Developer + -Responsibilities: Add, Edit, list, Delete Slots +Responsibilities: Add, Edit, Delete Commands and handling of Storage. ''' @@ -23,24 +23,24 @@ image::marcus-pzj.png[width="150", align="left"] {empty}[https://github.com/marcus-pzj[Github]] [<>] Role: Developer + -Responsibilities: View, Find, Auto-Complete +Responsibilities: List, Find, Undo, Redo, History commands and Auto-Complete feature. ''' === Julian Lim -image::julianlim.png[width="150", align="left"] +image::macchazuki.png[width="150", align="left"] {empty}[https://github.com/macchazuki[Github]] [<>] Role: Developer + -Responsibilities: Encryption, Import, Export, Generate Summary +Responsibilities: Encryption, Decryption, Import, Export and Generate Summary feature. ''' === Lim Ding Heng -image::limdingheng.png[width="150", align="left"] +image::dingheng4448.png[width="150", align="left"] {empty}[https://github.com/dingheng4448[Github]] [<>] Role: Developer + -Responsibilities: Generate Day, Week, Month Views +Responsibilities: View day, week, month commands and Initialization of calendars. ''' diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 8eeb60f9a..4a4df8ad5 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -15,7 +15,7 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/CS2113-AY1819S2-T08-3/main/tree/master +:repoURL: https://github.com/CS2113-AY1819S2-T08-3/main/blob/master By: `T08-3` Since: `Jan 2019` Licence: `MIT` @@ -113,14 +113,14 @@ When you are ready to start coding, === Architecture .Architecture Diagram -image::Architecture.png[width="100%"] +image::Architecture.png[width="800"] The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component. [TIP] The `.pptx` files used to create diagrams in this document can be found in the link:{repoURL}/docs/diagrams/[diagrams] folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose `Save as picture`. -`Main` has only one class called link:{repoURL}/src/PlanMySem/Main.java[`Main`]. It is responsible for, +`Main` has only one class called link:{repoURL}/src/planmysem/Main.java[`Main`]. It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup method where necessary. @@ -142,7 +142,7 @@ Each of the four components For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `Logic.java` class. .Class Diagram of overall application. -image::OverallClassDiagram.png[width="100%"] +image::OverallClassDiagram.png[width="800"] [discrete] ==== How the architecture components interact with each other @@ -150,7 +150,7 @@ image::OverallClassDiagram.png[width="100%"] The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. .Component interactions for `delete 1` command -image::SDforDeleteSlot.png[width="100%"] +image::SDforDeleteSlot.png[width="800"] The sections below give more details of each component. @@ -160,7 +160,7 @@ The sections below give more details of each component. .Structure of the UI Component image::UiClassDiagram.png[width="800"] -*API* : link:{repoURL}/src/main/PlanMySem/ui/Ui.java[`Ui.java`] +*API* : link:{repoURL}/src/planmysem/ui/Ui.java[`Ui.java`] The UI consists of a `MainWindow` that is made up of just `commandInput` and `outputConsole`. This application is mainly a text-based application, hence here are not much componenets here. @@ -181,7 +181,7 @@ The `UI` component, image::LogicClassDiagram.png[width="800"] *API* : -link:{repoURL}/src/main/java/plamysem/logicManager/Logic.java[`Logic.java`] +link:{repoURL}/src/planmysem/logic/Logic.java[`Logic.java`] . `Logic` uses the `parser` class to parse the user command. . This results in a `Command` object which is executed. @@ -192,20 +192,20 @@ link:{repoURL}/src/main/java/plamysem/logicManager/Logic.java[`Logic.java`] Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. .Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +image::SDforDeleteSlot.png[width="800"] [[Design-Model]] === Model component .Overall structure of the Model Component -image::ModelClassDiagram.png[width="400"] +image::ModelClassDiagram.png[width="600"] -*API* : link:{repoURL}/src/PlanMySem/model/Model.java[`Model.java`] +*API* : link:{repoURL}/src/planmysem/model/Model.java[`Model.java`] [[Design-Planner]] ==== Planner component -*API* : link:{repoURL}/src/PlanMySem/model/Planner.java[`Planner.java`] +*API* : link:{repoURL}/src/planmysem/data/Planner.java[`Planner.java`] The `Planner` component, @@ -216,7 +216,7 @@ The `Planner` component, [[Design-Semester]] ==== Semester component -*API* : link:{repoURL}/src/PlanMySem/model/semester/Semester.java[`Semester.java`] +*API* : link:{repoURL}/src/planmysem/model/semester/Semester.java[`Semester.java`] The `Semester` component, @@ -227,7 +227,7 @@ The `Semester` component, [[Design-Slot]] ==== Slot component -*API* : link:{repoURL}/src/PlanMySem/model/slot/Slot.java[`Slot.java`] +*API* : link:{repoURL}/src/planmysem/model/slot/Slot.java[`Slot.java`] The `Slot` component, @@ -244,7 +244,7 @@ Notice how `Slot` does not hold it's end time but rather it holds the `duration` .Structure of the Storage Component image::StorageClassDiagram.png[width="400"] -*API* : link:{repoURL}/src/PlanMySem/storageFile/Storage.java[`Storage.java`] +*API* : link:{repoURL}/src/planmysem/storage/Storage.java[`Storage.java`] The `Storage` component, @@ -381,10 +381,10 @@ Arguments are then parsed via 2 different methods/techniques according to the fo Hence, parameters in *PlanMySem* can be categorised into 2 categories: -1. Prepended parameters such as `n/NAME`, `st/START_TIME`, `des/DESCRIPTION`, etc. -2. Non-prepended parameters, A.K.A. keywords, such as `INDEX`, `TYPE_OF_VIEW`. etc. +1. Prefixed parameters such as `n/NAME`, `st/START_TIME`, `des/DESCRIPTION`, etc. +2. Non-Prefixed parameters, A.K.A. keywords, such as `INDEX`, `TYPE_OF_VIEW`. etc. -===== Parsing Prepended Parameters +===== Parsing Prefixed Parameters To retrieve the set of parameters, the function `private static HashMap> getParametersWithArguments(String args)` can be called. The function returns a `Hashmap` data structure, specifically `HashMap>`, to hold parameters, allowing for easy, quick and efficient access to specific parameters. @@ -398,9 +398,9 @@ The following are cases in which `IncorrectCommand` is invoked: ===== Parsing Keywords -Here, keywords are thought of as parameters that are not prepended. +Here, keywords are thought of as parameters that are not prefixed. In *PlanMySem*, keywords are utilized in command structures when they are to be used alone or when order of parameters are important. -In such cases, there is no logical need for prepending as the meaning of these parameters can be identified. +In such cases, there is no logical need for prefixing as the meaning of these parameters can be identified. The function `private String getStartingArgument(String args)` provides this functionality. Here, `IncorrectCommand` is invoked due to different circumstances: @@ -469,10 +469,38 @@ The `Edit` and `Delete` command then makes use of the _tagging_ system to then s ==== Design Considerations +===== Aspect: Wrapping of primitive data types in `Slot` +* *Alternative 1 (current choice):* Use of "primitive" data types instead of creating and utilising wrapped objects. + +E.g. `name`, `location` amd `description` are not wrapped but "primitive". +** Pros: Allows for more flexible code to account for flexible parsing (as needed in this application). +** Cons: Bigger code base and duplicated code. + +* *Alternative 2:* Wrap "primitive" data types. +** Pros: Less errors in handling invalid values. +** Cons: May cause inflexibility in writing code to account for flexible parsing. + +In summary, in this cause of *PlanMySme*, there is a need to achieve varied and flexible commands and as such, +it is not necessary to handle invalid values with the innate `Model` objects as these are taken care of when parsing. + +Additionally, doing these actions when parsing, though bloats code, allows for more varied responses to the user and improves user experience. + +===== Aspect: Storing and accessing `Slots` + +* *Alternative 1 (current choice):* Use of `Map`, such as `HashMap` to store `Days` that store `Slots`. +** Pros: `HashMap` allows for easier and faster, O(1) access time, access of particular `Day` according to date. +** Cons: This requires splitting of the calendar into days, as such there is no easy way to account for `Slots` that occur across days. + +* *Alternative 2:* Store `Slots` in a huge list. +** Pros: Allows for easier access by "index" and offers flexibility, for example, in the time of slots. +** Cons: Expensive to access, add and remove items. Furthermore, it is extremely expensive to collect slots that occur in a day, a very important and most likely to be a commonly used feature. ==== Future Implementation +===== Create and utilise an object to hold `Slots` + +===== Create and utilise an object in `Planner` to hold multiple `Semesters` + === List feature @@ -535,6 +563,84 @@ Step 3. The user executes `view month March` command to view the calendar for Ma // tag::undoredo[] === Undo/Redo feature + +==== Current Implementation + +The undo/redo mechanism is facilitated by `VersionedAddressBook`. +It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. +Additionally, it implements the following operations: + +* `VersionedAddressBook#commit()` -- Saves the current address book state in its history. +* `VersionedAddressBook#undo()` -- Restores the previous address book state from its history. +* `VersionedAddressBook#redo()` -- Restores a previously undone address book state from its history. + +These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. + +Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. + +Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. + +image::UndoRedoStartingStateListDiagram.png[width="800"] + +Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. + +image::UndoRedoNewCommand1StateListDiagram.png[width="800"] + +Step 3. The user executes `add n/David ...` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. + +image::UndoRedoNewCommand2StateListDiagram.png[width="800"] + +[NOTE] +If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. + +Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. + +image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] + +[NOTE] +If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. + +The following sequence diagram shows how the undo operation works: + +image::UndoRedoSequenceDiagram.png[width="800"] + +The `redo` command does the opposite -- it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. + +[NOTE] +If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. + +Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. + +image::UndoRedoNewCommand3StateListDiagram.png[width="800"] + +Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. + +image::UndoRedoNewCommand4StateListDiagram.png[width="800"] + +The following activity diagram summarizes what happens when a user executes a new command: + +image::UndoRedoActivityDiagram.png[width="650"] + +==== Design Considerations + +===== Aspect: How undo & redo executes + +* **Alternative 1 (current choice):** Saves the entire address book. +** Pros: Easy to implement. +** Cons: May have performance issues in terms of memory usage. +* **Alternative 2:** Individual command knows how to undo/redo by itself. +** Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). +** Cons: We must ensure that the implementation of each individual command are correct. + +===== Aspect: Data structure to support the undo/redo commands + +* **Alternative 1 (current choice):** Use a list to store the history of address book states. +** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. +** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. +* **Alternative 2:** Use `HistoryManager` for undo/redo +** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. +** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. + ==== Current Implementation The undo/redo mechanism is facilitated by `VersionedPlanner`. diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx deleted file mode 100644 index b27be3ed8..000000000 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and /dev/null differ diff --git a/docs/diagrams/UndoRedoActivityDiagram.pptx b/docs/diagrams/UndoRedoActivityDiagram.pptx index 16fec930c..dd550dde0 100644 Binary files a/docs/diagrams/UndoRedoActivityDiagram.pptx and b/docs/diagrams/UndoRedoActivityDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx b/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx index 6fd31b5f3..0ea85d12e 100644 Binary files a/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx and b/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx index 1f3261976..ef9b09138 100644 Binary files a/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx index e2907d4a9..c54d43d3f 100644 Binary files a/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx index 4ecc659bd..8bf64034e 100644 Binary files a/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx index 16ebf585d..34cebf37b 100644 Binary files a/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoSequenceDiagram.pptx b/docs/diagrams/UndoRedoSequenceDiagram.pptx index 5ccc1042c..1811bc6a8 100644 Binary files a/docs/diagrams/UndoRedoSequenceDiagram.pptx and b/docs/diagrams/UndoRedoSequenceDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoStartingStateListDiagram.pptx b/docs/diagrams/UndoRedoStartingStateListDiagram.pptx index 98ce06764..94b0f748c 100644 Binary files a/docs/diagrams/UndoRedoStartingStateListDiagram.pptx and b/docs/diagrams/UndoRedoStartingStateListDiagram.pptx differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 0c913eaa3..921b318b9 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoActivityDiagram.png b/docs/images/UndoRedoActivityDiagram.png new file mode 100644 index 000000000..fe1ea855f Binary files /dev/null and b/docs/images/UndoRedoActivityDiagram.png differ diff --git a/docs/images/UndoRedoExecuteUndoStateListDiagram.png b/docs/images/UndoRedoExecuteUndoStateListDiagram.png new file mode 100644 index 000000000..92115701d Binary files /dev/null and b/docs/images/UndoRedoExecuteUndoStateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand1StateListDiagram.png b/docs/images/UndoRedoNewCommand1StateListDiagram.png new file mode 100644 index 000000000..08259988e Binary files /dev/null and b/docs/images/UndoRedoNewCommand1StateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand2StateListDiagram.png b/docs/images/UndoRedoNewCommand2StateListDiagram.png new file mode 100644 index 000000000..99571d992 Binary files /dev/null and b/docs/images/UndoRedoNewCommand2StateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand3StateListDiagram.png b/docs/images/UndoRedoNewCommand3StateListDiagram.png new file mode 100644 index 000000000..8886946eb Binary files /dev/null and b/docs/images/UndoRedoNewCommand3StateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand4StateListDiagram.png b/docs/images/UndoRedoNewCommand4StateListDiagram.png new file mode 100644 index 000000000..e077167a1 Binary files /dev/null and b/docs/images/UndoRedoNewCommand4StateListDiagram.png differ diff --git a/docs/images/UndoRedoSequenceDiagram.png b/docs/images/UndoRedoSequenceDiagram.png new file mode 100644 index 000000000..4693b259b Binary files /dev/null and b/docs/images/UndoRedoSequenceDiagram.png differ diff --git a/docs/images/UndoRedoStartingStateListDiagram.png b/docs/images/UndoRedoStartingStateListDiagram.png new file mode 100644 index 000000000..9f989a195 Binary files /dev/null and b/docs/images/UndoRedoStartingStateListDiagram.png differ diff --git a/docs/team/seanieyap.adoc b/docs/team/seanieyap.adoc index f2d6b2771..caad395ce 100644 --- a/docs/team/seanieyap.adoc +++ b/docs/team/seanieyap.adoc @@ -12,8 +12,7 @@ This portfolio documents my contributions to _PlanMySem_, a software engineering module project. This module was taken in my third year during the second semester. - -AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. +*PlanMySem* is a text-based (Command Line Interface) scheduling/calendar application that targets NUS students and staff who prefer to use a desktop application for managing their schedule/calendar. == Summary of contributions diff --git a/src/planmysem/common/Messages.java b/src/planmysem/common/Messages.java index 2fd48dcb8..0f6863607 100644 --- a/src/planmysem/common/Messages.java +++ b/src/planmysem/common/Messages.java @@ -6,6 +6,7 @@ import javafx.util.Pair; import planmysem.model.semester.ReadOnlyDay; +import planmysem.model.semester.WeightedName; import planmysem.model.slot.ReadOnlySlot; /** @@ -44,7 +45,7 @@ public class Messages { */ public static String craftSelectedMessage(Set tags) { StringBuilder sb = new StringBuilder(); - sb.append("Selected Slots containing tags: \n"); + sb.append("Selected Slots containing: \n"); int count = 1; for (String tag : tags) { @@ -82,11 +83,23 @@ public static String craftSelectedMessage(String header, } /** - * Craft selected message without header. + * Craft selected message via weighted Set of Pairs. */ - public static String craftSelectedMessage(Map> selectedSlots) { - return getSelectedMessage(selectedSlots); + public static String craftListMessage(Set pairs) { + StringBuilder sb = new StringBuilder(); + sb.append("Here are the closest matching names/tags: \n"); + + int count = 1; + for (WeightedName p : pairs) { + sb.append(count); + sb.append(".\t"); + sb.append(p.getName()); + sb.append("\n"); + count++; + } + sb.append("\nEnter 'list n/{name} OR t/{tag}' to list all slots related to the name/tag\n"); + + return sb.toString(); } /** diff --git a/src/planmysem/common/Utils.java b/src/planmysem/common/Utils.java index 5d20cced6..c31f4bf96 100644 --- a/src/planmysem/common/Utils.java +++ b/src/planmysem/common/Utils.java @@ -183,11 +183,58 @@ public static LocalTime getEndTime(LocalTime time, int duration) { return time.plusMinutes(duration); } + /** + * Adapted from: + * https://rosettacode.org/wiki/Levenshtein_distance#Java + * + * Computes Levenshtein Distance from strings + */ + public static int getLevenshteinDistance (String lhsIn, String rhsIn) { + String lhs = lhsIn.toLowerCase(); + String rhs = rhsIn.toLowerCase(); + + // the array of distances + int[] cost = new int[lhs.length() + 1]; + int[] newCost = new int[lhs.length() + 1]; + + // initial cost in String lhs + for (int i = 0; i < lhs.length(); i++) { + cost[i] = i; + } + + // cost for transforming each letter in String rhs + for (int j = 1; j < rhs.length() + 1; j++) { + // initial cost in String rhs + newCost[0] = j; + + // transformation cost for each letter in String lhs + for (int i = 1; i < lhs.length() + 1; i++) { + // match current letters in both strings + int match = (lhs.charAt(i - 1) == rhs.charAt(j - 1)) ? 0 : 1; + + // cost for each type of transformation + int costReplace = cost[i - 1] + match; + int costInsert = cost[i] + 1; + int costDelete = newCost[i - 1] + 1; + + // keep minimum cost + newCost[i] = Math.min(Math.min(costInsert, costDelete), costReplace); + } + + // switch cost array with newCost array + int[] temp = cost; + cost = newCost; + newCost = temp; + } + + // the distance is the cost for transforming all letters in both strings + return cost[lhs.length()]; + } + /** * Get the nearest date to a type of day from today. */ public static LocalDate getNearestDayOfWeek(LocalDate date, int day) { return date.with(TemporalAdjusters.next(DayOfWeek.of(day))); } - } diff --git a/src/planmysem/logic/Logic.java b/src/planmysem/logic/Logic.java index 024b46a66..b329f0e66 100644 --- a/src/planmysem/logic/Logic.java +++ b/src/planmysem/logic/Logic.java @@ -1,7 +1,7 @@ package planmysem.logic; import java.time.LocalDate; -import java.util.Map; +import java.util.List; import javafx.collections.ObservableList; import javafx.util.Pair; @@ -29,7 +29,7 @@ public interface Logic { /** * Gets unmodifiable view of the current last shown list. */ - Map> getLastShownSlots(); + List>> getLastShownSlots(); /** * Returns an unmodifiable view of the list of commands entered by the user. diff --git a/src/planmysem/logic/LogicManager.java b/src/planmysem/logic/LogicManager.java index 65c9ed9bd..a20df90d8 100644 --- a/src/planmysem/logic/LogicManager.java +++ b/src/planmysem/logic/LogicManager.java @@ -1,7 +1,7 @@ package planmysem.logic; import java.time.LocalDate; -import java.util.Map; +import java.util.List; import javafx.collections.ObservableList; import javafx.util.Pair; @@ -40,7 +40,7 @@ public String getStorageFilePath() { } @Override - public Map> getLastShownSlots() { + public List>> getLastShownSlots() { return model.getLastShownList(); } diff --git a/src/planmysem/logic/commands/FindCommand.java b/src/planmysem/logic/commands/FindCommand.java index 51b49a9ca..a89a0a786 100644 --- a/src/planmysem/logic/commands/FindCommand.java +++ b/src/planmysem/logic/commands/FindCommand.java @@ -2,21 +2,21 @@ package planmysem.logic.commands; import java.time.LocalDate; +import java.util.Comparator; import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; import java.util.Set; -import java.util.TreeMap; -import java.util.regex.Pattern; +import java.util.TreeSet; -import javafx.util.Pair; import planmysem.common.Messages; +import planmysem.common.Utils; import planmysem.logic.CommandHistory; import planmysem.model.Model; import planmysem.model.semester.Day; -import planmysem.model.semester.ReadOnlyDay; -import planmysem.model.slot.ReadOnlySlot; +import planmysem.model.semester.WeightedName; import planmysem.model.slot.Slot; - /** * Finds all slots in planner whose name contains the argument keyword. * Keyword matching is case sensitive. @@ -25,7 +25,7 @@ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; public static final String COMMAND_WORD_SHORT = "f"; - private static final String MESSAGE_SUCCESS = "%1$s Slots listed.\n%2$s"; + private static final String MESSAGE_SUCCESS = "%1$s Slots found.\n%2$s"; private static final String MESSAGE_SUCCESS_NONE = "0 Slots listed.\n"; public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Finds all slots whose name " + "contains the specified keywords (case-sensitive).\n\t" @@ -35,6 +35,40 @@ public class FindCommand extends Command { private final String keyword; private final boolean isFindByName; + private int minDistance = Integer.MAX_VALUE; + + private Queue weightedNames = new PriorityQueue<>(new Comparator<>() { + @Override + public int compare(WeightedName p1, WeightedName p2) { + String n1 = p1.getName(); + String n2 = p2.getName(); + int d1 = p1.getDist(); + int d2 = p2.getDist(); + + if (d1 != d2) { + return d1 - d2; + } else { + return n1.compareTo(n2); + } + } + }); + + private Set selectedNames = new TreeSet<>(new Comparator() { + @Override + public int compare(WeightedName p1, WeightedName p2) { + String n1 = p1.getName(); + String n2 = p2.getName(); + int d1 = p1.getDist(); + int d2 = p2.getDist(); + + if (d1 != d2) { + return d1 - d2; + } else { + return n1.compareTo(n2); + } + } + }); + public FindCommand(String name, String tag) { this.keyword = (name == null) ? tag.trim() : name.trim(); this.isFindByName = (name != null); @@ -42,31 +76,47 @@ public FindCommand(String name, String tag) { @Override public CommandResult execute(Model model, CommandHistory commandHistory) { - Map> selectedSlots = new TreeMap<>(); - for (Map.Entry entry : model.getDays().entrySet()) { for (Slot slot : entry.getValue().getSlots()) { if (isFindByName) { - if (Pattern.matches(".*" + keyword + ".*", slot.getName())) { - selectedSlots.put(entry.getKey(), new Pair<>(entry.getValue(), slot)); - } + generateDiscoveredNames(keyword, slot.getName()); } else { Set tagSet = slot.getTags(); for (String tag : tagSet) { - if (Pattern.matches(".*" + keyword + ".*", tag)) { - selectedSlots.put(entry.getKey(), new Pair<>(entry.getValue(), slot)); - } + generateDiscoveredNames(keyword, tag); } } } } - if (selectedSlots.isEmpty()) { + if (weightedNames.isEmpty()) { return new CommandResult(MESSAGE_SUCCESS_NONE); } - model.setLastShownList(selectedSlots); - return new CommandResult(String.format(MESSAGE_SUCCESS, selectedSlots.size(), - Messages.craftSelectedMessage(selectedSlots))); + while (!weightedNames.isEmpty() && weightedNames.peek().getDist() < minDistance + 2) { + selectedNames.add(weightedNames.poll()); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, selectedNames.size(), + Messages.craftListMessage(selectedNames))); + } + + /** + * If a slot entry is found, calculates the Levenshtein Distance between the name and the keyword. + * Updates the weightedNames PQ with the new WeightedName pair containing the name and its weight. + */ + private void generateDiscoveredNames(String keyword, String compareString) { + if (compareString == null) { + return; + } + + int dist = Utils.getLevenshteinDistance(keyword, compareString); + if (dist < minDistance) { + minDistance = dist; + } + //System.out.println(compareString + " vs key: " + keyword + " dist: " + dist); + WeightedName distNamePair = new WeightedName(compareString, dist); + + weightedNames.add(distNamePair); } } diff --git a/src/planmysem/logic/commands/ListCommand.java b/src/planmysem/logic/commands/ListCommand.java index e13b7583b..7a502b2f4 100644 --- a/src/planmysem/logic/commands/ListCommand.java +++ b/src/planmysem/logic/commands/ListCommand.java @@ -27,7 +27,7 @@ public class ListCommand extends Command { public static final String MESSAGE_SUCCESS_NONE = "0 Slots listed.\n"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all slots whose name " - + "directly matches the specified keyword (case-sensitive)." + + "directly matches the specified keyword (not case-sensitive)." //+ "\n\tOptional Parameters: [past] [next] [all]" //+ "\n\tDefault: list all" + "\n\tExample: " + COMMAND_WORD + " n/CS1010"; @@ -53,7 +53,6 @@ public CommandResult execute(Model model, CommandHistory commandHistory) { } else { Set tagSet = slot.getTags(); for (String tag : tagSet) { - // if (slot.getTags().contains(keyword)) if (tag.equalsIgnoreCase(keyword)) { selectedSlots.put(entry.getKey(), new Pair<>(entry.getValue(), slot)); } @@ -65,6 +64,7 @@ public CommandResult execute(Model model, CommandHistory commandHistory) { if (selectedSlots.isEmpty()) { return new CommandResult(MESSAGE_SUCCESS_NONE); } + model.setLastShownList(selectedSlots); return new CommandResult(String.format(MESSAGE_SUCCESS, selectedSlots.size(), diff --git a/src/planmysem/logic/commands/RedoCommand.java b/src/planmysem/logic/commands/RedoCommand.java index 34771a1a0..70c95d8e7 100644 --- a/src/planmysem/logic/commands/RedoCommand.java +++ b/src/planmysem/logic/commands/RedoCommand.java @@ -24,7 +24,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.redo(); - model.setLastShownList(null); + model.clearLastShownList(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/planmysem/logic/commands/UndoCommand.java b/src/planmysem/logic/commands/UndoCommand.java index e27a9cb75..e74269b82 100644 --- a/src/planmysem/logic/commands/UndoCommand.java +++ b/src/planmysem/logic/commands/UndoCommand.java @@ -24,7 +24,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.undo(); - model.setLastShownList(null); + model.clearLastShownList(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/planmysem/model/Model.java b/src/planmysem/model/Model.java index 4cd3c15ab..cdc1f2d84 100644 --- a/src/planmysem/model/Model.java +++ b/src/planmysem/model/Model.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -21,8 +22,12 @@ public interface Model { /** * Set last shown list. */ + void setLastShownList(List>> list); + void setLastShownList(Map> list); + void clearLastShownList(); + /** * Saves the current planner state for undo/redo. */ @@ -31,7 +36,7 @@ public interface Model { /** * Get last shown list. */ - Map> getLastShownList(); + List>> getLastShownList(); /** * Get item in last shown list. diff --git a/src/planmysem/model/ModelManager.java b/src/planmysem/model/ModelManager.java index f31ee7a17..27c5b7cf0 100644 --- a/src/planmysem/model/ModelManager.java +++ b/src/planmysem/model/ModelManager.java @@ -2,10 +2,11 @@ import java.time.LocalDate; import java.time.LocalTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeMap; import javafx.util.Pair; import planmysem.model.semester.Day; @@ -18,7 +19,7 @@ * Represents the entire Planner. Contains the model of the Planner. */ public class ModelManager implements Model { - protected Map> lastShownList = new TreeMap<>(); + protected List>> lastShownList = new ArrayList<>(); private final VersionedPlanner versionedPlanner; /** @@ -38,21 +39,37 @@ public ModelManager(ReadOnlyPlanner planner) { } @Override + public void setLastShownList(List>> list) { + lastShownList.clear(); + + if (list != null) { + lastShownList.addAll(list); + } + } + public void setLastShownList(Map> list) { lastShownList.clear(); if (list != null) { - lastShownList.putAll(list); + for (Map.Entry> entry : list.entrySet()) { + lastShownList.add(new Pair<>(entry.getKey(), entry.getValue())); + } } } + @Override + public void clearLastShownList() { + lastShownList.clear(); + } + + @Override public void commit() { versionedPlanner.commit(); } @Override - public Map> getLastShownList() { + public List>> getLastShownList() { return lastShownList; } @@ -62,17 +79,7 @@ public Pair> getLastShownItem(int ind throw new IndexOutOfBoundsException(); } - int adjustedIndex = index - 1; - - int count = 0; - for (Map.Entry> entry : lastShownList.entrySet()) { - if (count == adjustedIndex) { - return new Pair<>(entry.getKey(), entry.getValue()); - } - count++; - } - - throw new IndexOutOfBoundsException(); + return lastShownList.get(index - 1); } @Override diff --git a/src/planmysem/model/semester/WeightedName.java b/src/planmysem/model/semester/WeightedName.java new file mode 100644 index 000000000..a0ee235d4 --- /dev/null +++ b/src/planmysem/model/semester/WeightedName.java @@ -0,0 +1,23 @@ +//@@author marcus-pzj +package planmysem.model.semester; +/** + * WeightedName of integer and string + */ + +public class WeightedName { + private int dist; + private String name; + + public WeightedName(String name, int dist) { + this.name = name; + this.dist = dist; + } + + public int getDist() { + return this.dist; + } + + public String getName() { + return name; + } +} diff --git a/src/planmysem/ui/Gui.java b/src/planmysem/ui/Gui.java index 709e6bbcc..fba5c9f73 100644 --- a/src/planmysem/ui/Gui.java +++ b/src/planmysem/ui/Gui.java @@ -18,7 +18,7 @@ public class Gui implements Ui { */ public static final int DISPLAYED_INDEX_OFFSET = 1; - public static final int INITIAL_WINDOW_WIDTH = 800; + public static final int INITIAL_WINDOW_WIDTH = 1000; public static final int INITIAL_WINDOW_HEIGHT = 600; private final LogicManager logicManager; diff --git a/test/java/planmysem/logic/Commands/AddCommandTest.java b/test/java/planmysem/logic/Commands/AddCommandTest.java index eed8e00fc..3e08ed019 100644 --- a/test/java/planmysem/logic/Commands/AddCommandTest.java +++ b/test/java/planmysem/logic/Commands/AddCommandTest.java @@ -12,6 +12,7 @@ import java.time.LocalTime; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -123,7 +124,12 @@ public void equals() throws Exception { */ private class ModelStub implements Model { @Override - public Map> getLastShownList() { + public List>> getLastShownList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void clearLastShownList() { throw new AssertionError("This method should not be called."); } @@ -132,6 +138,11 @@ public void commit() { throw new AssertionError("This method should not be called."); } + @Override + public void setLastShownList(List>> list) { + throw new AssertionError("This method should not be called."); + } + @Override public void setLastShownList(Map> list) { throw new AssertionError("This method should not be called."); diff --git a/test/java/planmysem/logic/Commands/CommandTestUtil.java b/test/java/planmysem/logic/Commands/CommandTestUtil.java index e8ff6a045..0272c6807 100644 --- a/test/java/planmysem/logic/Commands/CommandTestUtil.java +++ b/test/java/planmysem/logic/Commands/CommandTestUtil.java @@ -3,8 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.LocalDate; -import java.util.Map; -import java.util.TreeMap; +import java.util.ArrayList; +import java.util.List; import javafx.util.Pair; import planmysem.logic.CommandHistory; @@ -104,7 +104,7 @@ public static void assertCommandFailure(Command command, Model actualModel, Comm String expectedMessage) { Planner expectedPlanner = new Planner(actualModel.getPlanner()); actualModel.getDays(); - Map> expectedLastShownList = new TreeMap<>(actualModel.getLastShownList()); + List>> expectedLastShownList = new ArrayList<>(actualModel.getLastShownList()); CommandHistory expectedCommandHistory = new CommandHistory(actualCommandHistory); diff --git a/test/java/planmysem/logic/Commands/EditCommandTest.java b/test/java/planmysem/logic/Commands/EditCommandTest.java index e71be35f6..6250bf0f5 100644 --- a/test/java/planmysem/logic/Commands/EditCommandTest.java +++ b/test/java/planmysem/logic/Commands/EditCommandTest.java @@ -11,7 +11,6 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -98,11 +97,11 @@ public void setup() throws Exception { model.addSlot(LocalDate.of(2019, 02, 03), slotBuilder.generateSlot(3)); model.addSlot(LocalDate.of(2019, 02, 04), slotBuilder.generateSlot(3)); - Map> list = new HashMap<>(); - list.put(pair1.getKey(), pair1.getValue()); - list.put(pair2.getKey(), pair2.getValue()); - list.put(pair3.getKey(), pair3.getValue()); + Map> list = new TreeMap<>(); list.put(pair4.getKey(), pair4.getValue()); + list.put(pair3.getKey(), pair3.getValue()); + list.put(pair2.getKey(), pair2.getValue()); + list.put(pair1.getKey(), pair1.getValue()); model.setLastShownList(list); expectedModel = new ModelManager();