Project: Codii
[Codii] is a desktop address book application specially designed for debt collectors to manage debtors in a simple manner. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface).
Debt collectors can store information such as the amount owed, debt borrow date and debt cleared date in addition to debtor’s personal information.
Unique features such as an interest calculator help debt collectors manage debts more efficiently.
Codii is evolved from AddressBook - Level 4, which is a desktop address book application used for teaching Software Engineering principles.
Code contributed: [Functional code] [Test code]
Saving the data
Address book data is saved in the hard disk automatically after any command that changes the data.
There is no need to save manually.
If address book data can be loaded successfully, backup address book data is saved upon starting the program.
Loading the data
If the data file does not exist or cannot be read:
-
Backup data file will be loaded, if available and readable.
-
If backup data is unavailable:
-
You will be given a sample address book.
-
-
If backup data exists but cannot be read :
-
You will be given an empty address book.
-
To quickly revert address book data to the state of last use:
|
End of Extract
Justification
The backup is a safety measure in case the main storage file is corrupted or deleted by accident.
Backup storage mechanism
The backup storage mechanism is facilitated by the StorageManager
. It backs up the address book data automatically each time the application starts up, if there is existing data available. The sequence diagram for this is shown below in Figure 4.3.
Figure 4.3.1: Sequence diagram for backing up address book data
The backupAddressBook
method is called in MainApp#init()
which is called each time the application starts. No backup is made if there is no existing data.
Design Considerations
Aspect: When to create the backup
Alternative 1 (current choice): Create it on application start up
Pros: Guarantees that a backup is made only of a working version of the address book that the user can easily revert to, should he/she mess up the main copy while using the application.
Cons: Not suitable for users who tend to make many changes within one session, as too many changes will not be backed up.
Alternative 2: Implement a command to create backup
Pros: The backup is only created when the user desires to.
Cons: This makes the implementation pointless altogether as it is meant as a safety net for clumsy users. This alternative would only benefit the careful users, who do not require it as much in the first place.
Alternative 3: Create a backup after a fixed number of commands that change the address book
Pros: This will create backups that are guaranteed to be recent.
Cons: It is difficult to determine the ideal number of commands to ensure that the backup is both recent enough, yet outdated enough for the user to want to restore state to should he/she mess up a command at some point.
End of Extract
Sorting all contacts : sort
Sorts all the contacts in the address book in specified order.
Format: sort [ORDERING]
Examples:
-
sort
Sorts the contacts in the address book by name. -
sort cluster
Sorts the contacts in the address book by their postal districts.
End of Extract
Justification
Debt collectors would want to sort their contacts in various ways for easier viewing.
Sorting mechanism
Sorting is done within the UniquePersonList
class.
public void sortBy(String order) throws IllegalArgumentException {
switch (order) {
case "name":
internalList.sort((Person p1, Person p2) -> p1.getName().compareTo(p2.getName()));
break;
case "debt":
internalList.sort((Person p1, Person p2) -> p2.getDebt().compareTo(p1.getDebt()));
break;
case "cluster":
internalList.sort((Person p1, Person p2) -> p1.getCluster().compareTo(p2.getCluster()));
break;
case "deadline":
internalList.sort((Person p1, Person p2) -> p1.getDeadline().compareTo(p2.getDeadline()));
internalList.sort((Person p1, Person p2) -> Boolean.compare(p1.isWhitelisted(), p2.isWhitelisted()));
break;
default:
throw new IllegalArgumentException("Invalid sort ordering");
}
}
The sort
command can take in a String
that determines how the contacts should be sorted. If no ordering is
specified, the contacts will be sorted by ascending lexicographical order by default.
public class SortCommandParser implements Parser<SortCommand> {
public SortCommand parse(String args) throws ParseException {
requireNonNull(args);
String trimmedArgs = args.trim().toLowerCase();
switch (trimmedArgs) {
case "":
case "name":
case "debt":
case "cluster":
case "deadline":
return new SortCommand(trimmedArgs);
default:
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE));
}
}
}
public static final String DEFAULT_ORDERING = "name";
public SortCommand(String order) {
//validity of order to sort is checked in {@code SortCommandParser}
if (order.equals("")) {
order = DEFAULT_ORDERING;
}
this.order = order;
}
public CommandResult execute() throws CommandException {
try {
model.sortBy(order);
} // irrelevant parts of the method omitted for brevity
}
Design considerations
Aspect: Default sort
Alternative 1 (current choice): Sort
by name
by default
Pros: Relatively easy to implement, extremely intuitive.
Alternative 2: Sort
by debt
by default
Pros: Equally easy to implement.
Cons: Slightly less intuitive as sorting by name
is the most prevalent way of sorting contacts.
Alternative 3: No default sort
Pros: Extremely easy to implement.
Cons: Not user-friendly.
End of Extract
Enhancement Added: Cluster field
External behaviour
Clusters
are generated based on based on postal districts.
Justification
An easy way to group contacts is needed for a debt collector to better plan his/her trips.
Cluster mechanism
As a debt collector that operates in all parts of Singapore, it would boost efficiency in deciding debt collection trips
if the contacts can be effectively grouped by clusters. It is determined based on the postal code provided
upon adding a Person
into the address book. This can be seen in the constructors of the Person
class and the
Cluster
class.
public Person(Name name, Phone phone, Email email, Address address, PostalCode postalCode,
Debt debt, Interest interest, Deadline deadline, Set<Tag> tags) {
requireAllNonNull(name, phone, email, address, postalCode, debt, interest, deadline, tags);
// assignment of other fields omitted for brevity
this.cluster = new SimpleObjectProperty<>(new Cluster(postalCode));
}
public Cluster(PostalCode postalCode) {
requireNonNull(postalCode);
if (!isValidPostalCode(postalCode.toString())) {
throw new AssertionError(MESSAGE_POSTAL_CODE_CONSTRAINTS);
}
String cluster = getCluster(postalCode.toString());
clusterNumber = Integer.parseInt(cluster.substring(0, 2));
this.value = cluster.substring(4);
}
The getCluster
method resides in the ClusterUtil
class, and returns the name of the postal district based on the
first two numbers of the postal code that is passed into the method. The postal districts are retrieved from [URA].
The district number is stored as part of the String
for ease of sorting by location.
Part of the code from ClusterUtil
for retrieving the cluster
from a postal code starting with 01
is shown below:
public class ClusterUtil {
public static final String CLUSTER_POSTAL_DISTRICT_01 = "01. Raffles Place, Cecil, Marina, People's Park";
// declaration of other postal districts omitted for brevity
public static final String CLUSTER_POSTAL_DISTRICT_UNKNOWN = "99. Unknown";
public static String getCluster(String postalCode) {
requireNonNull(postalCode);
int postalSector = Integer.parseInt(postalCode.substring(0, 2));
switch (postalSector) {
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
return CLUSTER_POSTAL_DISTRICT_01;
// cases for all other valid postal districts omitted for brevity
default:
return CLUSTER_POSTAL_DISTRICT_UNKNOWN;
}
}
}
Design Considerations
Aspect: Grouping of contacts
Alternative 1 (current choice): Create a field that contains the name and district number of the postal districts, based on postal code
Pros: Easy to implement and extend, requires minimal user input.
Cons: Requires developer to update ClusterUtil
whenever a new postal district is drawn by the government, and
requires users to reset their contacts' cluster
via deletion and addition or via editing their postal codes
.
Alternative 2: Import contacts' location and find their proximity from one another using Google Maps API
Pros: Higher precision of geographical location and proximity.
Cons: Tedious to implement proximity. Either takes up too much time in generating nearby contacts or too much space
in storing them.
Alternative 3: Assign general location to each contact upon addition into the address book or via editing
Pros: Easy to implement.
Cons: Tedious for users. They also have to ensure that they do not make spelling mistakes.
End of Extract
Selecting a nearby person: nearby
Selects the person identified by the index number used in the listing of nearby contacts of currently selected person,
Format: nearby INDEX
Examples:
-
list
select 2
nearby 1
Selects the 1st person in the samecluster
as the previously selected person.
End of Extract
Justification
Debt collectors may want to see details of nearby contacts to plan a more effective visiting schedule
Increasing the debt of a debtor: borrow
Increases the debt of a debtor by the amount entered.
Format: borrow [INDEX] AMOUNT
Examples:
-
borrow 1 500
Increases the debt of the 1st person by $500. -
borrow 2 1000.10
Increases the debt of the 2nd person by $1000.10. -
list
select 2
borrow 234
Increases the debt of the 2nd person by $234.
End of Extract
Justification
Calling commands directly on the currently selected person makes much more intuitive sense than always supplying an
INDEX
each time. This also increases the flexibility and convenience of Codii.
Optional command indexes
It is intuitive to allow commands such as edit
, delete
, borrow
and others to be called on the currently selected
person instead of always having to supply the INDEX
.
Parsers of commands call a constructor of the commands without an index. Take the RepaidCommandParser
and RepaidCommand
for example.
public RepaidCommand parse(String args) throws ParseException {
try {
if (args.trim().equals("")) {
return new RepaidCommand();
} else {
Index index = ParserUtil.parseIndex(args);
return new RepaidCommand(index);
}
} catch (IllegalValueException ive) {
throw new ParseException(
String.format(MESSAGE_INVALID_COMMAND_FORMAT, RepaidCommand.MESSAGE_USAGE));
}
}
public RepaidCommand() throws CommandException {
personToWhitelist = selectPersonForCommand();
}
public RepaidCommand(Index targetIndex) throws CommandException {
personToWhitelist = selectPersonForCommand(targetIndex);
}
The selectPersonForCommand()
and selectPersonForCommand(Index)
methods are placed in the Command
class, and is used by such index-based commands to select
the currently selected person to apply the command on if no index is provided.
public ReadOnlyPerson selectPersonForCommand() throws CommandException {
if (ListObserver.getSelectedPerson() == null) {
throw new CommandException(Messages.MESSAGE_NO_PERSON_SELECTED);
}
return ListObserver.getSelectedPerson();
}
public ReadOnlyPerson selectPersonForCommand(Index index) throws CommandException {
List<ReadOnlyPerson> lastShownList = ListObserver.getCurrentFilteredList();
if (index.getZeroBased() >= lastShownList.size()) {
throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
}
return lastShownList.get(index.getZeroBased());
}
Design Considerations
Aspect: Executing index-based commands without index
Alternative 1 (current choice): The person to apply the commands on are determined in the constructors
Pros: Allows for proper redo
.
Cons: Hard to test as Command Exceptions are being thrown from the constructors.
Alternative 2: Calling a constructor of the command
without an Index
initialises its targetIndex
to null, and the null index is handled as a special value during execution
Pros: Easy to implement, easy to extend.
Cons: Although highly unlikely, it may be possible for a command
to have an unintended null targetIndex
, which
will then cause it to behave as an indexless command instead of an error. May also cause problems with redo
.
End of Extract
Changing themes: theme
Changes between the two available themes shown below in Figures 4.25.1 and 4.25.2 below.
Format: theme
Figure 4.25.1 : Dark theme (default)
Figure 4.25.2 : Bright theme
End of Extract
Justification
The ability to change themes can make the user feel more as though the app belongs to him/her as it is a form of customisation. It allows for a much better user experience.
Theme Changing Mechanism
The changing of themes is done in the MainWindow
class, which is the class that holds all the UI
parts.
private void changeTheme() {
for (String stylesheet : getRoot().getStylesheets()) {
if (stylesheet.endsWith("DarkTheme.css")) {
getRoot().getStylesheets().remove(stylesheet);
getRoot().getStylesheets().add("/view/BrightTheme.css");
break;
} else if (stylesheet.endsWith("BrightTheme.css")) {
getRoot().getStylesheets().remove(stylesheet);
getRoot().getStylesheets().add("/view/DarkTheme.css");
break;
}
}
}
The changeTheme
method is called when a ChangeThemeRequestEvent
is raised from ThemeCommand
.
In MainWindow
:
@Subscribe
private void handleChangeThemeRequestEvent(ChangeThemeRequestEvent event) {
logger.info(LogsCenter.getEventHandlingLogMessage(event));
changeTheme();
}
In ThemeCommand
:
public CommandResult execute() {
EventsCenter.getInstance().post(new ChangeThemeRequestEvent());
return new CommandResult(MESSAGE_SUCCESS);
}
Design Considerations
Aspect: Condition for changing themes
Alternative 1 (current choice): Use String#endsWith()
Pros: Foolproof, guaranteed to work if the theme exists.
Alternative 2: Use file paths
Pros: Likely to work most of the time if handled well.
Cons: When debugging, it was found that the path started in the build
folder instead of the src
folder. Using
this method seemed to cause inconsistencies between running the app from an IDE and from the .jar
file.
Aspect: Switching of themes
Alternative 1 (current choice): Simple toggle between two themes
Pros: Easy to implement.
Cons: Troublesome to extend.
Alternative 2: Use an int
to keep track of the current theme, assign each theme to a number, and use +
and %
to cycle through the themes
Pros: Easy to extend.
Cons: Troublesome to implement considering that we intend to use only two themes.
End of Extract
Other contributions
Miscellaneous
-
Restructure commands (Pull request #296)
-
Raise and fix bugs
-
Assist teammates with debugging tests
-
Report bugs and raise suggestions for another team (Issues #78, #76, #82)
-
Offer a bug fix for reuse (Issue #196)
-
Help others on Slack
-
Suggest alternative for importing Address Book level 4 for those facing issues with gradle
-
Remind others to delete .json files and the data/ folder if they were doing acceptance testing for another team in the same folder that they saved their own .jar file.
-
Project: Dog Mario
Dog Mario is a PC game developed with Tan Bing Hwang as an independent project for CP2106 Independent Software Development Project (Orbital).