This is a followup to a prior question I posted here.
In the MCVE below, I have a TableView
displaying a list of Person
objects. Above the list, I have a single TextField
which I use to filter the listed items in the TableView
.
The Person
class contains 4 fields, but I have my search field only checking for matches in 3 of them: userId
, lastName
, and emailAddress
.
The filtering function works as expected.
However, I now need to rank the results based on which fields were matched and the user Type
.
MCVE CODE
Person.java:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public final class Person {
private StringProperty userType = new SimpleStringProperty();
private IntegerProperty userId = new SimpleIntegerProperty();
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private StringProperty emailAddress = new SimpleStringProperty();
public Person(String type, int id, String firstName, String lastName, String emailAddress) {
this.userType.set(type);
this.userId.set(id);
this.firstName.set(firstName);
this.lastName.set(lastName);
this.emailAddress.set(emailAddress);
}
public String getUserType() {
return userType.get();
}
public void setUserType(String userType) {
this.userType.set(userType);
}
public StringProperty userTypeProperty() {
return userType;
}
public int getUserId() {
return userId.get();
}
public void setUserId(int userId) {
this.userId.set(userId);
}
public IntegerProperty userIdProperty() {
return userId;
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public StringProperty lastNameProperty() {
return lastName;
}
public String getEmailAddress() {
return emailAddress.get();
}
public void setEmailAddress(String emailAddress) {
this.emailAddress.set(emailAddress);
}
public StringProperty emailAddressProperty() {
return emailAddress;
}
}
Main.java:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Comparator;
public class Main extends Application {
TableView<Person> tableView;
private TextField txtSearch;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Create the TableView of data
tableView = new TableView<>();
TableColumn<Person, Integer> colId = new TableColumn<>("ID");
TableColumn<Person, String> colFirstName = new TableColumn<>("First Name");
TableColumn<Person, String> colLastName = new TableColumn<>("Last Name");
TableColumn<Person, String> colEmailAddress = new TableColumn<>("Email Address");
// Set the ValueFactories
colId.setCellValueFactory(new PropertyValueFactory<>("userId"));
colFirstName.setCellValueFactory(new PropertyValueFactory<>("firstName"));
colLastName.setCellValueFactory(new PropertyValueFactory<>("lastName"));
colEmailAddress.setCellValueFactory(new PropertyValueFactory<>("emailAddress"));
// Add columns to the TableView
tableView.getColumns().addAll(colId, colFirstName, colLastName, colEmailAddress);
// Create the filter/search TextField
txtSearch = new TextField();
txtSearch.setPromptText("Search ...");
addSearchFilter(getPersons());
// Add the controls to the layout
root.getChildren().addAll(txtSearch, tableView);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
private void addSearchFilter(ObservableList<Person> list) {
FilteredList<Person> filteredList = new FilteredList<Person>(list);
txtSearch.textProperty().addListener(((observable, oldValue, newValue) ->
filteredList.setPredicate(person -> {
// Clear any currently-selected item from the TableView
tableView.getSelectionModel().clearSelection();
// If search field is empty, show everything
if (newValue == null || newValue.trim().isEmpty()) {
return true;
}
// Grab the trimmed search string
String query = newValue.trim().toLowerCase();
// Convert the query to an array of individual search terms
String[] keywords = query.split("[\\s]+");
// Create a single string containing all the data we will match against
// BONUS QUESTION: Is there a better way to do this?
String matchString =
String.valueOf(person.getUserId())
+ person.getLastName().toLowerCase()
+ person.getEmailAddress().toLowerCase();
// Check if ALL the keywords exist in the matchString; if any are absent, return false;
for (String keyword : keywords) {
if (!matchString.contains(keyword)) return false;
}
// All entered keywords exist in this Person's searchable fields
return true;
})));
SortedList<Person> sortedList = new SortedList<>(filteredList);
// Create the Comparator to allow ranking of search results
Comparator<Person> comparator = new Comparator<Person>() {
@Override
public int compare(Person person, Person t1) {
return 0;
}
};
// Set the comparator and bind list to the TableView
sortedList.setComparator(comparator);
tableView.setItems(sortedList);
}
private ObservableList<Person> getPersons() {
ObservableList<Person> personList = FXCollections.observableArrayList();
personList.add(new Person("DECEASED", 123, "Chrissie", "Watkins", "[email protected]"));
personList.add(new Person("VET", 342, "Matt", "Hooper", "[email protected]"));
personList.add(new Person("VET", 526, "Martin", "Brody", "[email protected]"));
personList.add(new Person("NEW", 817, "Larry", "Vaughn", "[email protected]"));
return personList;
}
}
You'll see I have an empty Comparator
in my Main
class. This is what I need help with. I have created comparators in the past that are able to sort based on one field (from my previous question):
Comparator<DataItem> byName = new Comparator<DataItem>() {
@Override
public int compare(DataItem o1, DataItem o2) {
String searchKey = txtSearch.getText().toLowerCase();
int item1Score = findScore(o1.getName().toLowerCase(), searchKey);
int item2Score = findScore(o2.getName().toLowerCase(), searchKey);
if (item1Score > item2Score) {
return -1;
}
if (item2Score > item1Score) {
return 1;
}
return 0;
}
private int findScore(String item1Name, String searchKey) {
int sum = 0;
if (item1Name.startsWith(searchKey)) {
sum += 2;
}
if (item1Name.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
I am not sure how to adapt this for multiple fields, though. Specifically, I want to be able to choose which fields should be ranked "higher."
For this example, what I want to accomplish is to sort the list in this order:
userId
starts with akeyword
lastName
starts with akeyword
emailAddress
starts with akeyword
lastName
contains akeyword
emailAddress
contains akeyword
- Within matches any
userType = "VET"
should be listed first
I am not looking for Google-level algorithms, but just some way to prioritize matches. I am not very familiar with the Comparator
class and have a hard time understanding the JavaDocs for it, as it applies to my needs.
There are several posts on StackOverflow that deal with sorting by multiple fields, but all those I've found are comparing Person
to Person
. Here, I need to compare Person
fields to the txtSearch.getText()
value.
How would I go about refactoring this Comparator
to set up custom sorting of this nature?
Your scoring concept is close, you just need to come up with factors and follow the rules.
So, here's a simple example:
public int score(Item item, String query) {
int score = 0;
if (item.userId().startsWith(query) {
score += 2000;
}
if (item.lastName().startsWith(query) {
score += 200;
} else if (item.lastName().contains(query) {
score += 100;
}
if (item.email().startsWith(query) {
score += 20;
} else if (item.email().contains(query) {
score += 10;
}
if (item.userType().equals("VET")) {
score += 5;
}
return score;
}
So as you can see, I took each of your criteria and turned them in to different digits within the score, and for the distinction within each criteria, I had different values (10 vs 20, for example). Finally I tacked on 5 for the "VET" type.
The assumption is that the scoring rules are not exclusive (i.e. that each rule refines the scoring, rather than stops it), and the the VET types were tie breakers within each criteria, vs to the top of the list. If VET needs to go to the top of the list (i.e. all VETs will be show before all non-VET), you can change the 5 to 10000, giving it it's own order of magnitude.
Now, using decimal numbers is just easy, but you'll run out of magnitudes after 9 (you'll overflow the int) -- you could also use other bases (base 3 in this example), giving you access to more "bits" in the integer. You could use a long, Or you could use a BigDecimal value and have as many criteria as you like.
But the basics are the same.
Once you have the score, just compare the scores of the two values in your comparator.