Overview
FitBiz is a fitness business management application, specially made for fitness coaches to manage their clients. It is primarily a desktop application where the user interacts via the Command Line Interface (CLI), and views data via the Graphical User Interface (GUI). This project is written in Java 11, packaged using Gradle, and uses JavaFX for the GUI.
Summary of Contributions
-
Major enhancement: developed the Command Autocomplete feature, which allows users to conveniently autocomplete commands and their parameter prefixes using the Tab key.
-
What it does: allows users to input FitBiz’s commands with ease without having to memorise the syntax of every command available. Since our users are fast typists and prefer to use the keyboard, this feature also allows them to efficiently get to the next prefix by hitting Tab, instead of wasting time using their mouse.
-
Justification: previously, users would have to refer to the User Guide frequently (or rely on the
help
command) for the syntax of FitBiz’s commands. This feature greatly lowers the barriers to entry for this application, and also enhances the user experience by providing a much faster way for users to interact with the CLI via the keyboard. -
Highlights: developed a custom
Trie
data structure which was written to be easily extensible for future commands and custom logic to be added. This feature is also contextually aware, and will provide ample and useful feedback to the user (like similar commands found, or the usage of the current autocompleted command).
-
-
Major enhancement: developed the Command History feature, which allows users to store their previously entered commands, and which they can access using the ↑ and ↓ keys.
-
What it does: allows users to efficiently and conveniently access and repeat their previously entered commands (be it valid or invalid) any time within the Command Box.
-
Justification: previously, if the command was successful, users would have to type out the full command again if they had wanted to repeat it. Now, users can easily repeat or tweak their previously entered commands without wasting precious time typing everything again.
-
Highlights: this feature was made to mimic most modern interpreters, and as such, will be very familiar to most users comfortable with using a CLI application. The history was also made to persist on the storage, so users can expect to come back to the application after a previous session and still access their previous command history of said session.
-
-
Minor enhancement: developed the
export
command which allows users to export their clients' recorded exercises into a CSV file which can then be viewed in other spreadsheet software like Microsoft Excel, or easily shared with their clients. -
Minor enhancement: redesigned the overall user interface and enhanced the user experience by providing better visual feedback cues to the GUI.
-
Minor enhancement: added the target and current weight attributes to the
Client
class. -
Overall contributions: 4.5K+ LOC | 260+ commits | 60+ PRs merged | 70+ PRs reviewed
-
Other contributions:
-
Project management:
-
Enhancements to existing/teammates' features:
-
Completely reworked the overall design of the GUI by adhering to the Material Design specification: #123 #208, #217, #218, #234
-
Updated the sample client data, exercises, and schedules: #67, #233
-
Updated unit tests for the
v1.2
release, raising test coverage by 8.3% to 77.8%: #69 -
Added unit tests for the
AddExerciseCommand
class, raising test coverage by 3.2%: #133 -
Wrote a custom insertion sort to support adding of exercises: #233 (justification)
-
-
Documentation:
-
Added the Command History, Command Autocomplete, Export and Delete Exercises Command features to the User and Developer Guides: #201, #237, #244, #267, #271
-
Added and updated the Overview section to the User Guide: #110, #136, #267
-
Reordered sections in the User and Developer Guides for better flow of contents: #213, #223
-
-
Summary:
-
PRs reviewed with non-trivial review comments: #83, #229, #261
-
Issues and bugs reported with non-trivial comments: #104, #105, #129, #138 #235
-
Issues resolved and bugs squashed with non-trivial implementations: #41, #64, #65, #78, #95, #103, #107, #161, #165, #170, #187, #188, #190, #197, #198, #200
-
Solutions contributed to forum discussions: #30, #55, #58, #68
-
-
Contributions to the User Guide
Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users. |
Understanding the Command Line Interface (CLI) --- Aaron Choo
Even though FitBiz comes with a GUI, it is mainly used to display data. Most of the user interaction occurs via the CLI, which in FitBiz, is comprised of the Command Box and the Result Box. We understand that CLIs have higher barriers to entry and may scare some inexperienced users away. As such, we have provided some features common to most modern CLIs to make your user experience with FitBiz much better. In this section, we shall look at the Command History and the Command Autocomplete feature, and learn how to effectively utilise them.
Command History
Similar to most modern CLIs, users of FitBiz can press the ↑ and ↓ arrow keys to cycle through their previously entered commands. If you have prior experience in using a CLI, feel free to skip this section as this should be second nature to you. If not, here is a quick tutorial on how to use this time saving feature.
First, start by typing anything into the Command Box. It need not have to be a valid command (like those shown in [Commands]). In our example, we chose to type Hello World
:
Next, hit the Enter key to enter the command into FitBiz. Notice that whatever you have typed in the Command Box should have disappeared. If you did not enter a valid command (like Hello World
), the border of the Command Box would have turned red, and you would have been prompted by a message saying Unknown command
in the Result Box like shown:
Next, continue entering different commands into the Command Box. You can safely ignore all the Unknown command
prompts for now.
Once you feel like you have entered enough commands into FitBiz, try hitting the ↑ key several times. You should start to see the history of your entered commands displaying in the Command Box in reverse chronological order. If you press the ↑ key enough times (or simply hold down the key), you would realise that the command in the Command Box no longer changes. In our case, it displays Hello World
, our first ever entered command.
Likewise, you can also see your more recent commands by pressing the ↓ key. Again, if you press it enough times, you would realise that the text from the Command Box disappeares (right after displaying your most recent command). This means that you have reached the end of your command history.
If you are not currently browsing the history, you can press the ↓ key to immediately clear what you are currently typing in the Command Box. |
Command Autocomplete
Again, similar to most modern CLIs, users of FitBiz can press the Tab key to autocomplete commands that they have partially typed. If the partially typed letters uniquely identifies a valid command in FitBiz (see [Commands]), the complete command will automatically appear in the Command Box. Otherwise, a list of all commands similar to the ambiguous letters will appear in the Result Box.
Also, we understand that some of FitBiz’s commands may be particularly long and diffcult to remember. In order to remedy this, we have also provided autocompletion of parameter prefixes for some commands, as well as the use of Tab to easily get to the next prefix. When autocompleting commands, the caret position will also be automatically set to the most optimal position corresponding to the completed command.
To see this feature in action, type add-c
into the Command Box and press Tab:
Immediately, you should have noticed three things:
-
All the parameter prefixes pertaining to the
add-c
command have been automatically completed for you -
Your caret is placed right after the
n/
for you to type your parameter -
The Result Box shows you the usage of the
add-c
command
Now, you can also press the Tab key repeatedly to go to the next parameter prefix, instead of wasting time using your mouse.
Note however, that there are some similar commands in FitBiz that have the same few starting letters. For example: both add-c
and add-e
starts with the letter "a". As such, hitting Tab when you have only typed a
in the Command Box will not autocomplete either command (unfortunately, FitBiz cannot read your mind). However, you will find that the command will be completed up till add-
, the point where add-e
and add-c
differs. The Result Box will also prompt you with the list of all similar commands found:
To autocomplete the parameter prefixes like in the first example above, you would just have to complete the command and press Tab once more.
Commands and their parameters in FitBiz are always separated by white spaces (ie. schedule 1 sch/ ). As such, the Tab key will only try to autocomplete your command if your current input in the Command Box is a single word. In other words, if your input is made up of more than one word separated by white spaces, FitBiz will ignore your use of Tab. Do not be surprised when you try to autocomplete more than a single word like add some thing , and yet receive no response from FitBiz.
|
The autocompletion of the parameter prefixes are only for these commands: add-c , add-e , filter-c , graph , and schedule . Autocompletion of prefixes for edit commands are not included as we understand that most likely than not, users would only choose to edit one field at a time.
|
Export a client’s exercises to CSV format: export
--- Aaron Choo
export
allows you to export your client’s recorded exercises into a spreadsheet format which you can then easily save or share with your clients. Note that this will create a CSV file, which you can view and open in other applications like Microsoft Excel, or Google Sheets (as shown in the example later).
format: export
This command can only be used when you have a client in view; make sure you know how to view a client first. Refer to [view-c-command] for more information. |
Quick summary
-
Only the exercises of the current client in view will be exported
-
Exported files will be saved in the
/exports
directory -
The name of the exported file will be the client’s name followed by the
.csv
file extension (eg.Alex Yeoh.csv
)
Example
-
First, ensure that you are currently viewing a client by using the
view-c
command: -
Now, if the client you are viewing currently has recorded exercises in the Exercise Table, simply execute the
export
command -
The exercises should have been successfully exported, if the following success message is shown:
-
Now, simply use your favourite file explorer to locate the
exports
folder, which should be created in the same directory asFitBiz.jar
. In theexports
folder, you will then find your exported CSV file: -
If you have a spreadsheet software (like Microsoft Excel) installed on your computer, you can easily view the CSV file by launching it. In our example, we have imported it into Google Sheets instead:
Common errors/problems
If you find that you are unable to execute this command successfully, there are a few things you can check:
-
Ensure that you are currently viewing a client using the
view-c
command. If you are indeed viewing a client, the Client View should not be empty. -
Ensure that you actually do have exercises recorded for the client currently in view using the
add-e
command. If the client does indeed have recorded exercises, the Exercise Table should not be empty.
Delete a client’s exercise: delete-e
--- Aaron Choo
delete-e
allows you to delete a previously recorded exercise of the client currently in view.
Format: delete-e INDEX
This command can only be used when you have a client in view; make sure you know how to view a client first. Refer to [view-c-command] for more information. |
Deleting an exercise from FitBiz is permanent and cannot be undone. |
Parameters
Parameters | Important points to note |
---|---|
|
Substitute Must be a positive integer (eg. 1, 2, 3, …) |
Example
-
First, ensure that you are currently viewing a client by using the
view-c
command: -
Say for example that you want to delete the fifth exercise found on the Exercise Table (the one named "Bench Press" done on "07-04-2020"), simply enter
delete-e 5
: -
After the command has been successfully executed, the specified exercise should have been deleted. Notice also, in the Personal Best Table that the personal best for "Bench Press" has also been automatically updated to reflect this change:
Common errors/problems
If you find that you are unable to execute this command successfully, there are a few things you can check:
-
Ensure that you are currently viewing a client using the
view-c
command. If you are indeed viewing a client, the Client View should not be empty. -
Ensure that you actually have exercises to delete and that the
INDEX
specified is correct. If the client does indeed have recorded exercises, the Exercise Table should not be empty.
Contributions to the Developer Guide
Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project. |
Command History --- Aaron Choo
This feature serves to improve the user experience by allowing users to browse and retrieve their previously entered commands using the ↑ and ↓ arrow keys, similar to what most modern CLIs offer.
Implementation
This command history mechanism is facilitated by the logic class CommandHistory
, which controls both the model class CommandHistoryState
and the storage utility class StorageReaderWriter
.
Behaviour of this feature
The behaviour of this feature has been implemented to mimic most modern CLIs, namely:
-
The empty string,
""
, should not be stored in the history -
Commands that are similar to the most recently stored command in the history should not be stored (ie. duplicate commands will not be stored)
-
All other user input, be it valid or invalid commands, should be stored
-
Number of commands should only be stored up to a well-defined maximum number (100 in this case, for performance reasons discussed in the later section)
-
Pressing the ↑ arrow key should browse backwards towards the least recently entered commands
-
Pressing the ↓ arrow key should browse forwards towards the most recently entered commands
-
The caret position should be at the end of the command string when browsing the history
-
Persistent storage of the command history should be supported (ie. a user can quit the app and come back to the same history as his previous usage of the app)
How this feature works
Since all user inputs, be it valid or invalid commands, should be stored, and since detection of the ↑ and ↓ arrow keys must occur in the JavaFX’s TextField
class found in CommandBox
, we have decided to let CommandBox
directly interact with CommandHistory
. In other words, CommandBox
will be responsible for calling CommandHistory#addToHistory
, CommandHistory#getNextCommand
, and CommandHistory#getPreviousCommand
. A simplified class diagram of the classes involved in this feature is given below:
CommandHistory depends on FileUtil only because it uses the static method FileUtil#writeToFile .
|
In the following sequence diagram, we trace the execution of the classes involved in storing the user command into the command history. For this example, we assume the user is entering the command list-c
:
When the CommandBox#handleCommandEntered
method is called, CommandBox
simply gets and passes the user input text from TextField
to CommandHistory
. CommandHistory
then adds this text to the internal list within CommandHistoryState
, retrieves the full internal list, converts it to a text-based format, and finally requests FileUtil
to save the text-based command history to storage.
How the Command History is persisted on storage
Each command that the user enters is essentially just a normal string. We simply use the utility class FileUtil
to write these lines of text to a text-based file command.txt
. Note that each new line of text in command.txt
represents one single command.
Whenever FitBiz first launches, we will then try to open and read from this same command.txt
file. If no such file exists, an empty new file will be created for use in the future.
Even if the storage component somehow fails to work, the command history will still be guaranteed to work, albeit without the storage features. In other words, the CommandHistoryState model will continue to function since it is not dependent nor have any association with the utility class FileUtil . This ensure that the command history for the current usage can at least be used.
|
Design Considerations
In designing the model CommandHistoryState
, we had to decide on the underlying data structure to store the user’s command history. We currently use the Java native ArrayList<String>
, where each line of command is stored as an individual entry. Another alternative that we have considered is to store the commands in a LinkedList<String>
:
Considerations | ArrayList (chosen) | LinkedList |
---|---|---|
Time Complexity |
Inserting to the list is O(1). Removal of the first item is O(n). Retrieval of any item is always O(1). |
Inserting to the list is O(1). Removal of the first/last item is O(1). Retrieval of an item that is not the first/last item will require traversal of the list and will be more expensive than O(1). |
Ease of Implementation |
Indices are concrete numbers and thus, are much easier to manipulate than pointers. The use of indices are enough to support the retrieval operations needed by this feature and is efficient since retrieval is always O(1). |
Pointers are arguably harder to keep track of and might be more difficult to implement. A custom linked list (as opposed to just using the native Java |
In the interest of saving developement time and better code readability, we decided to use an ArrayList
to store the commands. Since we have decided to cap the maximum size of the list, should this limit be exceeded, we would then need to remove the first item (or the zeroth index) from the list to free up space. Of course, doing a remove(0)
on a n-item ArrayList
will require that all remaining items in the list be reassigned to new indices, and thus incur an O(n) time operation. However, we found out through extensive testing that this causes no observable nor significant lag when the maximum capacity is reached.
Moreover, there is also a need to overwrite the whole storage file command.txt
whenever this maximum size is reached. Before this maximum size is reached, we can easily append to the existing file the new command that the user has just entered. However, after this limit is exceeded, we must remove the first line stored in command.txt
, shift all remaining lines up, and then append that new line. Hard disk operations like writing to storage is many order of magnitudes slower than memory operations like the reassignment of indices as discussed above. Since the much larger bottleneck is in the storage, this effectively nullifies the time complexity comparison that a LinkedList
is faster than an ArrayList
in removing the first item.
In choosing the maximum size of the command history, we have to take note of some important caveats:
-
This number must be small enough to not cause the app to lag when the whole history is being written to storage
-
This number must be big enough to satisfy the user
Ultimately, we felt that 100 is a very generous estimate given that a user really only needs the past few commands at any point of time.
Command Autocomplete --- Aaron Choo
Similar to the previously mentioned Command History feature, this feature also serves to improve the user experience by allowing users to press the Tab key to autocomplete their partially entered commands.
Implementation
This feature is facilitated by the logic found in the Autocomplete
class. Before we dive into the implementation, let us first define what unambiguous and ambiguous commands are:
Unambiguous Commands | Ambiguous Commands |
---|---|
Can uniquely identify a single command using the sequence of letters that the user has entered |
Cannot uniquely identify a single command using the sequence of letters that the user has entered |
For example, assume we only have 3 commands in our app, add-c , add-e , and edit-c . If the user enters e and tries to autocomplete the command using Tab, we say that this is an unambiguous command since clearly, edit-c can be uniquely identified by e . If instead, the user enters a and presses Tab to autocomplete the command, we say that this is an ambiguous command, since both add-c and add-e are possible choices.
|
Behaviour of this feature
Again, this feature has also been implemented to mimic most modern CLIs, namely:
-
Any unambiguous commands should be immediately completed upon pressing of the Tab key
-
Any ambiguous commands should be completed up till the longest common prefix of all similar commands
-
Using the ambiguous command example in the introduction above, when the user enters
a
and presses Tab, the autocompletion should returnadd-
(the longest common prefix ofadd-e
andadd-c
) to the user
-
-
A list of all similar commands should be presented to the user should he try to autocomplete an ambiguous command
-
Pressing Tab when the command has already been completed will bring the user’s caret to the next prefix delimitter (
/
in our case) with wraparound
How the Trie data structure works
Since Java does not provide a native Trie data structure, we had to implement our own version of it. Moreover, Java also does not allow methods with multiple return values, and thus, we had to create a wrapper class SimilarWordsResult
to store the multiple results returned by Trie#listAllSimilarWords
. In this section, we shall take a more in depth look at the overall implementation of this data structure.
We first look at the Node
class provided in the same package which Trie
relies on. Each Node
object should contain the following attributes:
-
The parent node (
null
if the node is the root of the Trie) -
The current letter it represents
-
The children nodes (if any)
-
A boolean to know whether that node represents a completed word
Since each node stores with it their parent node pointer, we can easily construct the word represented by a node by recursively building the word up letter by letter until the root is reached. This is implemented in Node#constructWord
, as shown here:
public String constructWord() {
if (isRoot()) {
return EMPTY_STRING;
}
return parent.constructWord() + getLetter();
}
Now, let us discuss about how we implemented Trie
to support the behaviours discussed above by first looking at Trie#getLongestPrefixNode
. This method takes in an argument word
and returns in 3 distinct cases:
-
If the argument
word
matches no words currently in theTrie
:null
-
If the argument
word
is unambiguous: theNode
whose constructed word (usingNode#constructWord
) is the longest word contained inTrie
that can be formed fromword
-
If the argument
word
is ambiguous: theNode
whose constructed word is the longest common prefix of all words similar toword
contained inTrie
Refer to Activity Diagram for the Autocomplete Logic given in the next section for the complete sequence of the key decisions. |
Let us move on to Trie#listAllSimilarWords
which makes use of the Node
found by Trie#getLongestPrefixNode
. Cases 1 and 2 discussed above are relatively trivial and we shall not discuss about how they are handled in Trie#listAllSimilarWords
. For case 3, in order for us to find all the similar words, we have chosen to use a Depth-First Search (DFS) approach, starting the search from the Node
returned by Trie#getLongestPrefixNode
, as shown here:
Node subtrie = getLongestPrefixNode(word);
ArrayList<String> similarWords = new ArrayList<>();
Stack<Node> stack = new Stack<>();
stack.push(subtrie);
while (!stack.isEmpty()) {
Node current = stack.pop();
if (current.isWordEnd()) {
similarWords.add(current.constructWord());
} else {
stack.addAll(current.getChildren().values());
}
}
The choice of a DFS approach as opposed to a Breadth-First Search (BFS) approach is arbitrary, both should work as expected. |
How this feature works
Similar to Command History, this feature also relies heavily on the UI class CommandBox
, and thus we have decided to let CommandBox
interact with Autocomplete
directly. A simplified class diagram of the classes involved is shown here:
Autocomplete returns an object of type AutocompleteResult to CommandBox when the Autocomplete#execute is called. As such, both Autocomplete and CommandBox depend on, but are not directly associated with, AutocompleteResult . The same reasoning applies for SimilarWordsResult which have been explained in the earlier section.
|
In the following sequence diagram, we follow the execution for when the user tries to autocomplete his partially entered command gra
(which, in the current application, is an unambiguous command, and will result in the full completion of the graph
command as well as its prefixes):
CommandBox
retrieves the user input command and caret position from the TextField
, and calls the execute
method from Autocomplete
with these information. This execute
method (shown and explained in full in the next sequence diagram) creates an AutocompleteResult
object and returns this to CommandBox
, which retrieves all the information required and sets the TextField
and ResultDisplay
accordingly.
Within the execute
method, Autocomplete
calls the listAllSimilarWords
method from Trie
with the user input text. Trie
, which would already have all the commands stored, finds the longest prefix node, calls the constructWord
method from this node, and checks if this node represents the end of a completed word. Since it is indeed a completed word, Trie
immediately creates a SimilarWordsResult
object to store these information and returns it to Autocomplete
. Then, Autocomplete
retrieves these information, realises that it is dealing with an unambiguous command, and constructs the corresponding prefixes. It then creates a AutocompleteResult
object to store all the information that CommandBox
requires, and finally returns this object to CommandBox
.
Lastly, in order to explain the key decisions that this feature does at each step starting from the point where the user presses Tab, we have provided the following activity diagram:
This feature currently only supports autocompletion of commands and prefixes, and not other fields/parameters like names and addresses that have been used by the user before. Implicitly, since all commands defined in FitBiz do not have empty spaces in them, this allows us to easily determine when to allow users to press Tab to get to the next prefix (behaviour 4): by simply checking for the presence of white spaces from the trimmed user input (like shown in the activity diagram).
Design Considerations
As discussed in the implementation section, we have decided to use a Trie data structure. Of course, we have also considered other much simpler alternatives like simply storing all available commands in a native Java List
. A quick summary of the pros and cons is given here:
Considerations | Trie (chosen) | List |
---|---|---|
Time Complexity |
Searching if a word exists is O(n), where n is the number of letters in the word to search for. Finding the longest common prefix of an ambiguous command is O(n), where n is the number of letters in the original word. |
Searching if a word exists is O(nm), where n is the number of letters in the word to search for, and m is the number of words in the list. Finding the longest common prefix of an ambiguous command is not linear with n and m. |
Ease of Implementation |
Initial development might be more difficult; developers might not be familiar with this data structure as it is not as common. Custom class means that additional, custom logic can be easily added. |
Much faster initial development. Custom logic cannot be easily added. |
As such, the choice of implementing our own Trie data structure is obvious. As this app grows bigger in the forseeable future, the number of commands as well as the number of things we would want to autocomplete would increase. Overall, we felt that the Trie data structure will scale much better as compared to a List.
Exchanging some initial development time for future scalability of our app will ensure that we, or future developers, do not end up wasting time refactoring what could have been done in the first place. Moreover, the Trie data structure is much more effective and computationally inexpensive in finding the longest common prefix of all ambiguous commands. The same cannot be said when using a List.
Also, since we have implemented our own Trie data structure, it would also allow more custom logic to be added later, and allow more creative freedom with respect to the features that we, or future developers would want to add. For example, future version of this application might want to also include the autocompletion of frequently used parameters by the user.