word-based tag search with respect to order

This commit is contained in:
pegasko 2024-06-24 00:42:34 +03:00
parent 862160d1ef
commit 1802a0e022

View file

@ -27,34 +27,37 @@ import android.widget.TextView;
import java.util.ArrayList; import java.util.ArrayList;
import art.pegasko.yeeemp.base.Tag;
import art.pegasko.yeeemp.base.TagStat; import art.pegasko.yeeemp.base.TagStat;
public class EventEditTagsAdapter extends ArrayAdapter<TagStat> { public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
public static final String TAG = EventEditTagsAdapter.class.getSimpleName(); public static final String TAG = EventEditTagsAdapter.class.getSimpleName();
private ArrayList<TagStat> tags; private ArrayList<TagStat> tags; // used by parent as container for filtered elements, changes as suer types
private ArrayList<TagStat> tagsAll; // used as container with backup of all unfiltered tags, does not change
private LayoutInflater inflater; private LayoutInflater inflater;
private final int viewResourceId; private final int viewResourceId;
private final int viewFieldId; private final int viewFieldId;
@SuppressWarnings("unchecked")
public EventEditTagsAdapter(Context context, int viewResourceId, int viewFieldId, ArrayList<TagStat> tags) { public EventEditTagsAdapter(Context context, int viewResourceId, int viewFieldId, ArrayList<TagStat> tags) {
super(context, viewResourceId, tags); super(context, viewResourceId, tags);
this.tags = tags; this.tags = tags;
this.tagsAll = (ArrayList<TagStat>) tags.clone();
this.viewResourceId = viewResourceId; this.viewResourceId = viewResourceId;
this.viewFieldId = viewFieldId; this.viewFieldId = viewFieldId;
this.inflater = LayoutInflater.from(context); this.inflater = LayoutInflater.from(context);
} }
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View tagView, ViewGroup parent) {
if (convertView == null) convertView = inflater.inflate(this.viewResourceId, parent, false); if (tagView == null)
tagView = inflater.inflate(this.viewResourceId, parent, false);
TextView text; TextView text;
try { try {
if (this.viewFieldId == 0) { if (this.viewFieldId == 0) {
text = (TextView) convertView; text = (TextView) tagView;
} else { } else {
text = convertView.findViewById(this.viewFieldId); text = tagView.findViewById(this.viewFieldId);
} }
if (text == null) { if (text == null) {
@ -68,7 +71,7 @@ public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
text.setText(tags.get(position).tag.getName() + " (" + tags.get(position).count + ")"); text.setText(tags.get(position).tag.getName() + " (" + tags.get(position).count + ")");
return convertView; return tagView;
} }
@Override @Override
@ -80,47 +83,150 @@ public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
// TODO: Stop using mutable global // TODO: Stop using mutable global
// Reusable filter result // Reusable filter result
private ArrayList<TagStat> tagsSuggestions = new ArrayList<TagStat>(); private ArrayList<TagStat> tagsSuggestions = new ArrayList<TagStat>();
private String prevConstraint = null;
@Override
public String convertResultToString(Object resultValue) { public String convertResultToString(Object resultValue) {
String str = ((TagStat) (resultValue)).tag.getName(); return ((TagStat) (resultValue)).tag.getName();
return str; }
/**
* Check if constraint is reducing previous.
*
* Constraint (C) reduces previous (P) if `len(C) >= len(P)` and `C.startsWith(P)`.
*
* This is means that user has typed some text to the end of input field and matching items can be filtered
* from previous filter result.
* In the opposite case, the set of matching items grows and required full rebuild from scratch.
*/
private boolean checkIfPossibleReduceToConstraint(String prevConstraint, String constraint) {
if (prevConstraint == null)
return false;
return (constraint.length() >= prevConstraint.length()) && (constraint.startsWith(prevConstraint));
}
private String makeConstraintFromUserInput(CharSequence userInput) {
return userInput.toString().toLowerCase().trim();
}
private boolean hasChangedConstraint(String prevConstraint, String newConstraint) {
if (prevConstraint == null || newConstraint == null)
return true;
return !prevConstraint.equals(newConstraint);
}
private void reduceTagsSuggestions(TagMatcher matcher) {
for (int index = 0; index < tagsSuggestions.size(); ) {
if (!matcher.isMatch(tagsSuggestions.get(index).tag))
tagsSuggestions.remove(index);
else
index += 1;
}
}
private void filterTagsSuggestions(TagMatcher matcher) {
tagsSuggestions.clear();
for (TagStat tag : tagsAll) {
if (matcher.isMatch(tag.tag)) {
tagsSuggestions.add(tag);
}
}
}
private void prepareTagsSuggestions(CharSequence constraint) {
String newConstraint = makeConstraintFromUserInput(constraint);
TagMatcher matcher = new TagMatcher(newConstraint);
// TODO: Use better search strategy and optimize search in non-reducing mode
if (hasChangedConstraint(prevConstraint, newConstraint)) {
if (checkIfPossibleReduceToConstraint(prevConstraint, newConstraint))
reduceTagsSuggestions(matcher);
else
filterTagsSuggestions(matcher);
prevConstraint = newConstraint;
}
}
private void resetTagsSuggestions() {
prevConstraint = null;
tagsSuggestions.clear();
}
private FilterResults makeFilterResults() {
FilterResults filterResults = new FilterResults();
// TODO: Spank me for using global mutable instance for returning immutable result (causes concurrent modification when publishResults())
filterResults.values = tagsSuggestions;
filterResults.count = tagsSuggestions.size();
return filterResults;
}
private FilterResults makeEmptyFilterResults() {
return new FilterResults();
} }
@Override @Override
protected FilterResults performFiltering(CharSequence constraint) { protected FilterResults performFiltering(CharSequence constraint) {
synchronized (tagsSuggestions) { synchronized (this) {
if (constraint != null) { if (constraint != null) {
tagsSuggestions.clear(); prepareTagsSuggestions(constraint);
for (TagStat tag : tags) { return makeFilterResults();
if (tag.tag.getName().contains(constraint.toString().toLowerCase())) {
tagsSuggestions.add(tag);
}
}
FilterResults filterResults = new FilterResults();
// TODO: Spank me for using global mutable instance for returning immutable result (causes concurrent modification when publishResults())
filterResults.values = tagsSuggestions;
filterResults.count = tagsSuggestions.size();
return filterResults;
} else { } else {
return new FilterResults(); resetTagsSuggestions();
return makeEmptyFilterResults();
} }
} }
} }
@Override @Override
protected void publishResults(CharSequence constraint, FilterResults results) { protected void publishResults(CharSequence constraint, FilterResults results) {
if (results.values == null) synchronized (this) {
return; if (results == null)
return;
synchronized (results.values) { if (results.values == null) {
ArrayList<TagStat> filteredList = (ArrayList<TagStat>) results.values;
if (results.count > 0) {
clear(); clear();
for (TagStat c : filteredList) {
add(c);
}
notifyDataSetChanged(); notifyDataSetChanged();
return;
} }
// TODO: Do not republish results if no data changed
ArrayList<TagStat> filteredList = (ArrayList<TagStat>) results.values;
clear();
for (TagStat c : filteredList) {
add(c);
}
notifyDataSetChanged();
}
}
/**
* Matcher to filter tags based on user-input constraint
*/
class TagMatcher {
private final String[] constraintParts;
public TagMatcher(String constraint) {
this.constraintParts = constraint.split(" +");
}
public boolean isMatch(Tag tag) {
String tagName = tag.getName();
int lastIndex = 0;
for (String seq : constraintParts) {
if (lastIndex >= tagName.length())
return false;
int newIndex = tagName.indexOf(seq, lastIndex);
if (newIndex == -1)
return false;
lastIndex = newIndex + seq.length();
}
return true;
} }
} }
}; };