Skip to content

Commit

Permalink
Merge pull request AY2324S1-CS2103T-W08-1#109 from ivanleekk/filter-a…
Browse files Browse the repository at this point in the history
…pplicants

Add FilterCommand
  • Loading branch information
ivanleekk authored Oct 26, 2023
2 parents 0565aa0 + ffa2dbb commit 7e31d01
Show file tree
Hide file tree
Showing 19 changed files with 727 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ src/test/data/sandbox/
.DS_Store
docs/_site/
docs/_markbind/logs/
/htmlReport/
82 changes: 75 additions & 7 deletions docs/DeveloperGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,19 +397,87 @@ The following diagram summarises what happens when a user executes a Sort comman
other fields when it becomes supported.
- Cons: Applicant#compareTo has to return different values depending on which descriptor has been chosen, causing
some bugs when working with other Java functions as the order of Objects compared to each other is not meant to
change during runtime.
change during runtime.

##### Aspect: Command syntax

- Alternative 1 (current choice): `sort d/ [valid field]`
- Pros: Simple and minimal text fields, with a single prefix required to enable sorting.
- Cons: Only able to sort in ascending order.
- Pros: Simple and minimal text fields, with a single prefix required to enable sorting.
- Cons: Only able to sort in ascending order.
- Alternative 2: `sort d/ [valid field] o/ [a/d]`
- Pros: Able to sort in either ascending or descending order.
- Cons: Requires additional input from the user, slowing down the use of the command.
- Pros: Able to sort in either ascending or descending order.
- Cons: Requires additional input from the user, slowing down the use of the command.
- Alternative 3: `sort d/ [valid field] o/ [a/d]` where `o/` is optional
- Pros: Retains the ability to sort in either order, but also the conciseness of Alternative 1.
- Cons: Users who are not aware of the `o/` feature may not use it.
- Pros: Retains the ability to sort in either order, but also the conciseness of Alternative 1.
- Cons: Users who are not aware of the `o/` feature may not use it.

### Filter feature

#### Implementation

The filter feature works by updating the `Predicate` used in the `FilteredList<Applicant>` of `ModelManager`. Using
the predicate, minimal changes to the implementation of StaffSnap is required.

To create a single predicate that is able to search and filter for multiple fields, a `CustomFilterPredicate` class is
created
It currently contains the following fields and is able to filter for applicants which match all specified fields.

1. Name
2. Phone
3. Email
4. Position
5. Status
6. Less than score
7. Greater than score

When `CustomFilterPredicate#test` is called, it will check if the specified fields are a substring of the same field of
the applicant,
returning true if all specified fields match, and false otherwise.

#### Steps to trigger

1. User opens the app
2. User enters `filter [n/, e/, p/, hp/, s/, lts/, gts/] [term]`, where one or more of the prefixes can be specified to
be filtered by

Once step 2 is complete, the GUI will update and refresh the applicant list with only applicants which match all
specified fields.
The following diagram summarises what happens when a user executes a Filter command:

<puml src="diagrams/FilterCommandActivityDiagram.puml" alt="FilterCommandActivityDiagram" />

### Design considerations

#### Aspect: How to filter applicants

- Alternative 1 (current choice): use a custom predicate and FilteredList, **compare using strings**
- Pros: Current implementation of ModelManager already uses FilteredList, making a custom predicate an easy
extension.
The `CustomFilterPredicate` can easily be extended when more applicant fields are ready to expand the utility of
the
filter command.
- Cons: Comparison of predicate fields to applicant fields are done using string comparison.
- Alternative 2: use a custom predicate and FilteredList, **compare within field classes**
- Pros: Same as alternative 1, and definition of what is considered a match can be defined in the field class (e.g.
Name, Phone, etc).
- Cons: Will require greater complexity than alternative 1 in implementation. May be slower to integrate new classes
in the future.

#### Aspect: Command syntax

- Alternative 1: `filter n/ [Name] e/ [Email] p/ [Position] hp/ [Phone] s/ [Status] lts/ [Score] gts/ [Score]`
- Pros: Unambiguous and forces all fields to be entered, preventing errors.
- Cons: Users cannot filter by single fields. Requires more key presses to enter a filter command.
- Alternative 2: `filter [n/, e/, p/, hp/, s/, lts/, gts/] [term]`, where only one term is allowed
- Pros: Quicker to key in command than alternative 1.
- Cons: Only allows users to filter by one field at a time, limiting utility of filter command.
- Alternative 3 (current choice): `filter [n/, e/, p/, hp/, s/, lts/, gts/] [term]`, where at least one term is required
and the others
are optional
- Pros: Provides flexibility in the filter command to filter by one or more fields, while still retaining the speed
of alternative 2 when few fields are required.
- Cons: Unfamiliar users may not know that fields can be optional anc continue to key in the full command at all
times.

--------------------------------------------------------------------------------------------------------------------

Expand Down
42 changes: 42 additions & 0 deletions docs/diagrams/FilterCommandActivityDiagram.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@startuml
'https://plantuml.com/activity-diagram-beta

start
:User enters filter command syntax;
:ApplicantBookParser and FilterCommandParser
parse arguments;
if (At least 1 argument is provided) then (true)
:Parse provided fields;
if (Provided fields are valid) then (true)
:Create new CustomFilterPredicate
from specified fields;
:Create new FilterCommand
from CustomFilterPredicate;
:FilterCommand updates predicate used in
ModelManager with CustomFilterPredicate;
:Display success message and show updated list in GUI;
stop
else (false)
:Throw ParseException with
invalid command format
message and proper Filter
syntax;
:Display error message;
stop
endif

else (false)
label 1
label 2
label 3

:Throw ParseException with
invalid command format
message and proper Filter
syntax;
:Display error message;
stop
endif

@enduml

24 changes: 22 additions & 2 deletions src/main/java/seedu/staffsnap/commons/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,33 @@ public static boolean containsWordIgnoreCase(String sentence, String word) {
checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty");
checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word");

String preppedSentence = sentence;
String[] wordsInPreppedSentence = preppedSentence.split("\\s+");
String[] wordsInPreppedSentence = sentence.split("\\s+");

return Arrays.stream(wordsInPreppedSentence)
.anyMatch(sentenceWord -> sentenceWord.toLowerCase().contains(preppedWord.toLowerCase()));
}

/**
* Returns true if the {@code sentence} contains the {@code word}.
* Ignores case.
* <br>examples:<pre>
* containsStringIgnoreCase("ABc def", "abc") == true
* containsStringIgnoreCase("ABc def", "DEF") == true
* containsStringIgnoreCase("ABc def", "AB") == true
* containsStringIgnoreCase("ABc def", "acd") == false // "acd" is not a substring in "ABc def"
* </pre>
* @param sentence a String that is not null
* @param word a String that is not empty and is not null
*/
public static boolean containsStringIgnoreCase(String sentence, String word) {
requireNonNull(sentence);
requireNonNull(word);

String preppedWord = word.trim();
checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty");

return sentence.contains(word);
}
/**
* Returns a detailed message of the t, including the stack trace.
*/
Expand Down
80 changes: 80 additions & 0 deletions src/main/java/seedu/staffsnap/logic/commands/FilterCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package seedu.staffsnap.logic.commands;

import static java.util.Objects.requireNonNull;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_EMAIL;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_GREATER_THAN_SCORE;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_LESS_THAN_SCORE;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_PHONE;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_POSITION;
import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_STATUS;

import seedu.staffsnap.commons.util.ToStringBuilder;
import seedu.staffsnap.logic.Messages;
import seedu.staffsnap.model.Model;
import seedu.staffsnap.model.applicant.CustomFilterPredicate;



/**
* Finds and lists all applicants in address book whose name contains any of the argument keywords.
* Keyword matching is case-insensitive.
*/
public class FilterCommand extends Command {

public static final String COMMAND_WORD = "filter";

public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters all applicants who match the descriptor.";
public static final String MESSAGE_FAILURE = "Please add at least one field to filter by. "
+ "Possible fields include:" + "\n"
+ PREFIX_NAME + " [NAME], "
+ PREFIX_EMAIL + " [EMAIL], "
+ PREFIX_POSITION + " [POSITION], "
+ PREFIX_PHONE + " [PHONE], "
+ PREFIX_STATUS + " [STATUS], "
+ PREFIX_LESS_THAN_SCORE + " [SCORE], "
+ PREFIX_GREATER_THAN_SCORE + " [SCORE]";
public static final String MESSAGE_SCORE_PARSE_FAILURE = "Score in lts/ or gts/ has to be a number with up to 1 "
+ "decimal place";

private final CustomFilterPredicate predicate;

public FilterCommand(CustomFilterPredicate predicate) {
this.predicate = predicate;
}

@Override
public CommandResult execute(Model model) {
requireNonNull(model);
model.updateFilteredApplicantList(predicate);
return new CommandResult(
String.format(Messages.MESSAGE_APPLICANTS_LISTED_OVERVIEW, model.getFilteredApplicantList().size()));
}

/**
* Checks if the two FilterCommand objects are equivalent, by comparing the equivalence of their predicates.
* @param other Other FilterCommand.
* @return true if equals, false if not equals.
*/
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}

// instanceof handles nulls
if (!(other instanceof FilterCommand)) {
return false;
}

FilterCommand otherFilterCommand = (FilterCommand) other;
return predicate.equals(otherFilterCommand.predicate);
}

@Override
public String toString() {
return new ToStringBuilder(this)
.add("predicate", predicate)
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class SortCommand extends Command {
+ "Parameters: "
+ PREFIX_DESCRIPTOR + "DESCRIPTOR ";
public static final String MESSAGE_SUCCESS = "Sorted all Applicants";
public static final String MESSAGE_FAILURE = "Please add a descriptor with d/ [name/phone].";

private final Descriptor descriptor;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ public class StatusCommand extends Command {

public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the status of the applicant identified "
+ "by the index number used in the displayed applicant list.\n"
+ "Parameters: INDEX (must be a positive integer) " + "STATUS [u(ndecided)/o(ffered)/r(ejected)].";
+ "Parameters: INDEX (must be a positive integer) " + "s/ [u(ndecided)/o(ffered)/r(ejected)].";

public static final String MESSAGE_EDIT_STATUS_SUCCESS = "Edited Applicant Status: %1$s";
public static final String MESSAGE_NO_STATUS = "Missing Status, please follow the following parameters."
+ "Parameters: INDEX (must be a positive integer) "
+ "STATUS [u(ndecided)/o(ffered)/r(ejected)].";
+ "s/ [u(ndecided)/o(ffered)/r(ejected)].";
public static final String MESSAGE_NO_INDEX = "Missing Index, please follow the following parameters."
+ "Parameters: INDEX (must be a positive integer) "
+ "s/ [u(ndecided)/o(ffered)/r(ejected)].";


private final Index index;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import seedu.staffsnap.logic.commands.EditCommand;
import seedu.staffsnap.logic.commands.EditInterviewCommand;
import seedu.staffsnap.logic.commands.ExitCommand;
import seedu.staffsnap.logic.commands.FilterCommand;
import seedu.staffsnap.logic.commands.FindCommand;
import seedu.staffsnap.logic.commands.HelpCommand;
import seedu.staffsnap.logic.commands.ListCommand;
Expand Down Expand Up @@ -66,7 +67,7 @@ public Command parseCommand(String userInput) throws ParseException {
isConfirmed = isConfirmedNext;
isConfirmedNext = false;

switch (commandWord) {
switch (commandWord.toLowerCase()) {

case AddCommand.COMMAND_WORD:
return new AddCommandParser().parse(arguments);
Expand Down Expand Up @@ -106,6 +107,9 @@ public Command parseCommand(String userInput) throws ParseException {
case AddInterviewCommand.COMMAND_WORD:
return new AddInterviewCommandParser().parse(arguments);

case FilterCommand.COMMAND_WORD:
return new FilterCommandParser().parse(arguments);

case EditInterviewCommand.COMMAND_WORD:
return new EditInterviewCommandParser().parse(arguments);

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/seedu/staffsnap/logic/parser/CliSyntax.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ public class CliSyntax {
public static final Prefix PREFIX_INTERVIEW = new Prefix("i/");
public static final Prefix PREFIX_TYPE = new Prefix("t/");
public static final Prefix PREFIX_RATING = new Prefix("r/");
public static final Prefix PREFIX_STATUS = new Prefix("s/");
public static final Prefix PREFIX_LESS_THAN_SCORE = new Prefix("lts/");
public static final Prefix PREFIX_GREATER_THAN_SCORE = new Prefix("gts/");


}
Loading

0 comments on commit 7e31d01

Please sign in to comment.