PROJECT: FitBiz

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:

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:

command history 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:

command history unknown command

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:

autocomplete 1

Immediately, you should have noticed three things:

  1. All the parameter prefixes pertaining to the add-c command have been automatically completed for you

  2. Your caret is placed right after the n/ for you to type your parameter

  3. 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:

autocomplete 2

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

  1. First, ensure that you are currently viewing a client by using the view-c command:

    export 1
  2. Now, if the client you are viewing currently has recorded exercises in the Exercise Table, simply execute the export command

  3. The exercises should have been successfully exported, if the following success message is shown:

    export 2
  4. Now, simply use your favourite file explorer to locate the exports folder, which should be created in the same directory as FitBiz.jar. In the exports folder, you will then find your exported CSV file:

    export 3
  5. 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:

    export 4

Common errors/problems

If you find that you are unable to execute this command successfully, there are a few things you can check:

  1. 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.

  2. 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

INDEX

Substitute INDEX with the actual index of the exercise shown on the Exercise Table

Must be a positive integer (eg. 1, 2, 3, …​)

Example

  1. First, ensure that you are currently viewing a client by using the view-c command:

    delete e 1
  2. 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:

    delete e 2
  3. 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:

    delete e 3

Common errors/problems

If you find that you are unable to execute this command successfully, there are a few things you can check:

  1. 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.

  2. 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:

  1. The empty string, "", should not be stored in the history

  2. Commands that are similar to the most recently stored command in the history should not be stored (ie. duplicate commands will not be stored)

  3. All other user input, be it valid or invalid commands, should be stored

  4. 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)

  5. Pressing the arrow key should browse backwards towards the least recently entered commands

  6. Pressing the arrow key should browse forwards towards the most recently entered commands

  7. The caret position should be at the end of the command string when browsing the history

  8. 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:

CommandHistoryClassDiagram
Figure 1. Class Diagram for Command History
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:

CommandHistorySequenceDiagram
Figure 2. Sequence Diagram for Saving a User Entered Command

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 LinkedList) may have to be developed in order to support the retrieval operations that this feature requires while still keeping the retrieval time complexity to O(1).

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:

  1. This number must be small enough to not cause the app to lag when the whole history is being written to storage

  2. 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:

  1. Any unambiguous commands should be immediately completed upon pressing of the Tab key

  2. 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 return add- (the longest common prefix of add-e and add-c) to the user

  3. A list of all similar commands should be presented to the user should he try to autocomplete an ambiguous command

  4. 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:

  1. If the argument word matches no words currently in the Trie: null

  2. If the argument word is unambiguous: the Node whose constructed word (using Node#constructWord) is the longest word contained in Trie that can be formed from word

  3. If the argument word is ambiguous: the Node whose constructed word is the longest common prefix of all words similar to word contained in Trie

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:

CommandAutocompleteClassDiagram
Figure 3. Simplified Class Diagram for Autocomplete
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):

CommandAutocompleteSequenceDiagram
Figure 4. Simplified Sequence Diagram for Command Autocomplete

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.

CommandAutocompleteSequenceDiagramRef
Figure 5. Sequence Diagram for the Autocomplete#execute Method

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:

CommandAutocompleteActivityDiagram
Figure 6. Activity Diagram for the Autocomplete Logic

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.