package ring.commands;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import ring.world.WorldObject;
import ring.world.WorldObjectMetadata;
/**
* Searches for world objects based on a presented list. This is
* used to select things from rooms, etc.
* @author projectmoon
*
*/
public class WorldObjectSearch {
private List<WorldObject> objects = new ArrayList<WorldObject>();
/**
* Create a WorldObjectSearch with no world objects to search against.
*/
public WorldObjectSearch() {}
/**
* Static utility method to filter a list of WorldObjects by a list of data types
* represented as Class objects. This method is useful for WorldObjects that need
* to implement the produceSearchList method. This method never returns null.
* @param objs
* @param dataTypes
* @return A filtered list with WorldObjects that are of the type(s) contained within the dataTypes parameter.
*/
public static List<WorldObject> filterByDataType(List<WorldObject> objs, Class<?> ... dataTypes) {
ArrayList<WorldObject> results = new ArrayList<WorldObject>(objs.size());
for (WorldObject obj : objs) {
for (Class<?> cls : dataTypes) {
if (cls.isAssignableFrom(obj.getClass())) {
results.add(obj);
break;
}
}
}
results.trimToSize();
return results;
}
/**
* Create a WorldObjectSearch with an initial list of objects to be searched.
* @param objects
*/
public WorldObjectSearch(Collection<? extends WorldObject> objects) {
this.objects.addAll(objects);
}
/**
* Add a list of WorldObjects to be searched when {@link #search(String)} is
* called.
* @param objects A list of {@link ring.world.WorldObject}s.
*/
public void addSearchList(Collection<? extends WorldObject> objects) {
this.objects.addAll(objects);
}
/**
* Searches all world objects in the search lists. The resulting list of
* WorldObjects is a list of relevant world objects, ranked by how closely
* their names match the query. The query is case-insensitive.
* @param name The name to search for.
* @return A list of world objects ranked by relevancy.
*/
public List<WorldObject> search(String name) {
List<WorldObject> results = new ArrayList<WorldObject>(objects.size());
filter(objects, name);
results = relevanceSort(objects, name);
return results;
}
/**
* The first searching step is to filter out any world objects that do
* not even contain our query. This prevents {@link String#compareTo(String)}
* from deciding that a string without the query is more relevant than a string
* with the query.
* @param list
* @param text
*/
private void filter(List<WorldObject> list, String text) {
List<WorldObject> removals = new ArrayList<WorldObject>();
for (WorldObject wo : list) {
WorldObjectMetadata metadata = wo.getMetadata();
if (metadata.getName().toLowerCase().indexOf(text.toLowerCase()) < 0) {
removals.add(wo);
}
}
list.removeAll(removals);
}
/**
* The second searching step is to rank all remaining world objects by relevance.
* This uses {@link String#compareTo(String)} to determine relevancy. It forces
* all compareTo results to negative values, and the closer a number is to 0, the
* higher it is the result set. Special weight is given to names that start with
* the query, as most users would expect that name to be selected first.
* @param list The unsorted list of WorldObjects.
* @param text The text to search for.
* @return The sorted list of WorldObjects.
*/
private List<WorldObject> relevanceSort(List<WorldObject> list, final String text) {
//Ghetto local class for storing search ranking data.
class ResultTuple {
int rank;
WorldObject entry;
}
//Determine relevancy rank and create a list of ResultTuples.
List<ResultTuple> results = new ArrayList<ResultTuple>(list.size());
for (WorldObject wo : list) {
WorldObjectMetadata metadata = wo.getMetadata();
int i = metadata.getName().compareToIgnoreCase(text);
//The closer it is to 0, the more relevant it is.
i = 0 - Math.abs(i);
//Give more weight to names that start with the specified text.
String name = metadata.getName().toLowerCase();
String lcText = text.toLowerCase();
if (name.startsWith(lcText)) {
i += 10;
}
//Create tuple to be sorted.
ResultTuple tuple = new ResultTuple();
tuple.rank = i;
tuple.entry = wo;
results.add(tuple);
}
//The comparator resposible for sorting the list of results
//by relevance.
Comparator<ResultTuple> comp = new Comparator<ResultTuple>() {
@Override
public int compare(ResultTuple o1, ResultTuple o2) {
return o2.rank - o1.rank;
}
};
//Sort the tuples and return a list of WorldObjects sorted by relevance.
Collections.sort(results, comp);
List<WorldObject> sorted = new ArrayList<WorldObject>(list.size());
for (ResultTuple t : results) {
sorted.add(t.entry);
}
return sorted;
}
}