Simple Searching in Java
I recently worked on a GUI project that included a long list of item names within a list box. Since the list wasn't sorted alphabetically, but instead by popularity, it was difficult to find items by name. To resolve this, I allowed the user to enter search text, and I actively filtered the list with each keystroke. I wrote code like the following to loop through the list of entries on each keystroke as the user typed, and filtered out the entries that didn't contain the text the user entered:
public void handleSearchByKey(String oldVal, String newVal) {
// If the number of characters in the text box is less than last time
// it must be because the user pressed delete
if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
// Restore the lists original set of entries
// and start from the beginning
list.setItems( entries );
}
// Change to upper case so that case is not an issue
newVal = newVal.toUpperCase();
// Filter out the entries that don't contain the entered text
ObservableList<String> subentries = FXCollections.observableArrayList();
for ( Object entry: list.getItems() ) {
String entryText = (String)entry;
if ( entryText.toUpperCase().contains(newVal) ) {
subentries.add(entryText);
}
}
list.setItems(subentries);
}
The result was quite dramatic, although the code is actually very simple. Here are the steps (not in order):
- Loop through all the items in the list
- Create a sublist of items, where each item contains the text entered
- If the user hits the backspace key, restore the list to its full set of original entries, and search from the top again
- Replace list's entries with the sublist just created
As you type, you continue to refine the search, and the list entries become fewer. For instance, assume the list contains the following four entries:
Eric J. Bruno
Joseph Bruno
Ashley Bruno
Brandon Bruno
If you type " Eric J. Bruno
j," the list will contain the following items:
Joseph Bruno
However, if you follow with another key press so the search text is now "jo," you will have only one entry as you'd expect:
Joseph Bruno
Of course you wouldn't really need to search this way for such a short list, but remember this is only an example. However, this code has one flaw: it's not flexible enough to enter your search terms out of order. For instance, to find "Joseph Bruno" in the list, you cannot type "bruno j" as your search term. This is important if you're looking through a list of entries that contain multiple words, but you're not entirely sure of the spellings or combinations of words.
A Google-Like Improvement
Let's improve our search to be more Google-like, or at least more flexible in terms of the ordering of search terms:
public void handleSearchByKey(String oldVal, String newVal) {
// If the number of characters in the text box is less than last time
// it must be because the user pressed delete
if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
// Restore the lists original set of entries
// and start from the beginning
list.setItems( entries );
}
// Break out all of the parts of the search text
// by splitting on white space
String[] parts = newVal.toUpperCase().split(" ");
// Filter out the entries that don't contain the entered text
ObservableList<String> subentries = FXCollections.observableArrayList();
for ( Object entry: list.getItems() ) {
boolean match = true;
String entryText = (String)entry;
for ( String part: parts ) {
// The entry needs to contain all portions of the
// search string *but* in any order
if ( ! entryText.toUpperCase().contains(part) ) {
match = false;
break;
}
}
if ( match ) {
subentries.add(entryText);
}
}
list.setItems(subentries);
}
The change here is again simple: Break the search text into parts separated by spaces. The split() method of the String class accomplishes this. The result is an array containing the individual words in the overall search text. Next, loop over the entries in the list, and if an entry contains each of the words, it gets to stay in the list. Since it's a loop, the ordering of the individual words doesn't matter.
As a result, you can now type "bruno j," and the list will contain the following entries:
Eric J. Bruno
Joseph Bruno
Why? Because both entries contain "bruno" and "j." Change the search text to "bruno jo" and you will refine the list down to a single entry, as expected: "Joseph Bruno." By the way, this would have yielded no results with the first implementation. It is clearly an improvement!
Again, this is a simplified example of a real-world scenario where it can often be helpful to provide flexibility when searching through long lists of text. And, this flexibility need not be difficult to implement. The code for a full JavaFX 2.0 example (written in pure Java, of course) is below. Run it yourself to see it in action.
Happy coding!
— EJB
package javafxapplication1;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* @author ericjbruno
*/
public class JavaFXApplication1 extends Application {
ObservableList<String> entries = FXCollections.observableArrayList();
ListView list = new ListView();
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Simple Search");
Button btn = new Button();
btn.setText("Choose");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent event) {
System.exit(0);
}
});
TextField txt = new TextField();
txt.setPromptText("Search");
txt.textProperty().addListener(
new ChangeListener() {
public void changed(ObservableValue observable,
Object oldVal, Object newVal) {
handleSearchByKey2((String)oldVal, (String)newVal);
}
});
// Set up the ListView
list.setMaxHeight(180);
// Populate the list's entries
for ( int i = 0; i < 100; i++ ) {
entries.add("Item " + i);
}
entries.add("Eric J. Bruno");
entries.add("Joseph Bruno");
entries.add("Ashley Bruno");
entries.add("Brandon Bruno");
list.setItems( entries );
VBox root = new VBox();
root.setPadding(new Insets(10,10,10,10));
root.setSpacing(2);
root.getChildren().addAll(txt,list,btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
public void handleSearchByKey(String oldVal, String newVal) {
// If the number of characters in the text box is less than last time
// it must be because the user pressed delete
if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
// Restore the lists original set of entries
// and start from the beginning
list.setItems( entries );
}
// Change to upper case so that case is not an issue
newVal = newVal.toUpperCase();
// Filter out the entries that don't contain the entered text
ObservableList<String> subentries = FXCollections.observableArrayList();
for ( Object entry: list.getItems() ) {
String entryText = (String)entry;
if ( entryText.toUpperCase().contains(newVal) ) {
subentries.add(entryText);
}
}
list.setItems(subentries);
}
public void handleSearchByKey2(String oldVal, String newVal) {
// If the number of characters in the text box is less than last time
// it must be because the user pressed delete
if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
// Restore the lists original set of entries
// and start from the beginning
list.setItems( entries );
}
// Break out all of the parts of the search text
// by splitting on white space
String[] parts = newVal.toUpperCase().split(" ");
// Filter out the entries that don't contain the entered text
ObservableList<String> subentries = FXCollections.observableArrayList();
for ( Object entry: list.getItems() ) {
boolean match = true;
String entryText = (String)entry;
for ( String part: parts ) {
// The entry needs to contain all portions of the
// search string *but* in any order
if ( ! entryText.toUpperCase().contains(part) ) {
match = false;
break;
}
}
if ( match ) {
subentries.add(entryText);
}
}
list.setItems(subentries);
}
}

