From 1802a0e02241ce6ea698530e1cc15989f9b0e464 Mon Sep 17 00:00:00 2001 From: pegasko Date: Mon, 24 Jun 2024 00:42:34 +0300 Subject: [PATCH] word-based tag search with respect to order --- .../ui/activity/EventEditTagsAdapter.java | 166 ++++++++++++++---- 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/EventEditTagsAdapter.java b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/EventEditTagsAdapter.java index fa1b493..db781b3 100644 --- a/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/EventEditTagsAdapter.java +++ b/Yeeemp/app/src/main/java/art/pegasko/yeeemp/ui/activity/EventEditTagsAdapter.java @@ -27,34 +27,37 @@ import android.widget.TextView; import java.util.ArrayList; +import art.pegasko.yeeemp.base.Tag; import art.pegasko.yeeemp.base.TagStat; public class EventEditTagsAdapter extends ArrayAdapter { public static final String TAG = EventEditTagsAdapter.class.getSimpleName(); - private ArrayList tags; + private ArrayList tags; // used by parent as container for filtered elements, changes as suer types + private ArrayList tagsAll; // used as container with backup of all unfiltered tags, does not change private LayoutInflater inflater; private final int viewResourceId; private final int viewFieldId; - @SuppressWarnings("unchecked") public EventEditTagsAdapter(Context context, int viewResourceId, int viewFieldId, ArrayList tags) { super(context, viewResourceId, tags); this.tags = tags; + this.tagsAll = (ArrayList) tags.clone(); this.viewResourceId = viewResourceId; this.viewFieldId = viewFieldId; this.inflater = LayoutInflater.from(context); } - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) convertView = inflater.inflate(this.viewResourceId, parent, false); + public View getView(int position, View tagView, ViewGroup parent) { + if (tagView == null) + tagView = inflater.inflate(this.viewResourceId, parent, false); TextView text; try { if (this.viewFieldId == 0) { - text = (TextView) convertView; + text = (TextView) tagView; } else { - text = convertView.findViewById(this.viewFieldId); + text = tagView.findViewById(this.viewFieldId); } if (text == null) { @@ -68,7 +71,7 @@ public class EventEditTagsAdapter extends ArrayAdapter { text.setText(tags.get(position).tag.getName() + " (" + tags.get(position).count + ")"); - return convertView; + return tagView; } @Override @@ -80,47 +83,150 @@ public class EventEditTagsAdapter extends ArrayAdapter { // TODO: Stop using mutable global // Reusable filter result private ArrayList tagsSuggestions = new ArrayList(); + private String prevConstraint = null; + @Override public String convertResultToString(Object resultValue) { - String str = ((TagStat) (resultValue)).tag.getName(); - return str; + return ((TagStat) (resultValue)).tag.getName(); + } + + /** + * 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 protected FilterResults performFiltering(CharSequence constraint) { - synchronized (tagsSuggestions) { + synchronized (this) { if (constraint != null) { - tagsSuggestions.clear(); - for (TagStat tag : tags) { - 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; + prepareTagsSuggestions(constraint); + return makeFilterResults(); } else { - return new FilterResults(); + resetTagsSuggestions(); + return makeEmptyFilterResults(); } } } @Override protected void publishResults(CharSequence constraint, FilterResults results) { - if (results.values == null) - return; + synchronized (this) { + if (results == null) + return; - synchronized (results.values) { - ArrayList filteredList = (ArrayList) results.values; - if (results.count > 0) { + if (results.values == null) { clear(); - for (TagStat c : filteredList) { - add(c); - } notifyDataSetChanged(); + return; } + + // TODO: Do not republish results if no data changed + ArrayList filteredList = (ArrayList) 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; } } };