2

I have implemented a search mechanism in my app, such that when I search for a name or email, it displays the string with the matching character. However, there are some accented strings in my list and when I search with a regular character pertaining to that specific accent, say if i have string "àngela" and I search "angela" it doesnt display the string unless i search with exact string " àngela".

I am trying to get it to work regardless of the accent or not, say if i type in "à" , it should show all strings containing " à" and "a" and vice versa. Any idea how to go about this? I looked up a bunch of articles online, for example: How to ignore accent in SQLite query (Android) " and tried normalizer too but it partially works, if i search "a", it does show the accented letters with regular letters aswell but if I search with accented letters, it doesnt show anything.

Here's my code for filter:

 @Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence charSequence) {
                String charString = charSequence.toString();
                if (charString.isEmpty()) {
                    mSearchGuestListResponseListFiltered = mSearchGuestListResponseList;
                } else {
                    List<RegisterGuestList.Guest> filteredList = new ArrayList<>();
                    for (RegisterGuestList.Guest row : mSearchGuestListResponseList) {

                        // name match condition. this might differ depending on your requirement
                        // here we are looking for name or phone number match
                        String firstName = row.getGuestFirstName().toLowerCase();
                        String lastName = row.getGuestLastName().toLowerCase();
                        String name = firstName + " " +lastName;
                        String email = row.getGuestEmail().toLowerCase();
                        if ( name.trim().contains(charString.toLowerCase().trim()) ||
                                email.trim().contains(charString.toLowerCase().trim())){
                            filteredList.add(row);
                            searchText = charString.toLowerCase();
                        }
                    }

                    mSearchGuestListResponseListFiltered = filteredList;
                }

                FilterResults filterResults = new FilterResults();
                filterResults.values = mSearchGuestListResponseListFiltered;
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
                mSearchGuestListResponseListFiltered = (ArrayList<RegisterGuestList.Guest>) filterResults.values;
                notifyDataSetChanged();
            }
        };
    }

Here's the entire adapter class if anyone's interested : https://pastebin.com/VxsWWMiS Here's the corresponding activity view:

searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                mSearchGuestListAdapter.getFilter().filter(query);

                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                mSearchGuestListAdapter.getFilter().filter(newText);
                mSearchGuestListAdapter.notifyDataSetChanged();
                mSearchGuestListAdapter.setFilter(newText);

                if(mSearchGuestListAdapter.getItemCount() == 0){


                    String sourceString = "No match found for <b>" + newText + "</b> ";
                    mNoMatchTextView.setText(Html.fromHtml(sourceString));
                } else {
                    mEmptyRelativeLayout.setVisibility(View.GONE);
                    mRecyclerView.setVisibility(View.VISIBLE);
                }
                return false;
            }
        });

Happy to share any details if necessary. Also, randomly I do get the indexoutofboundexception onBind() method while searching (use recyclerview for list):

java.lang.IndexOutOfBoundsException: Index: 7, Size: 0
        at java.util.ArrayList.get(ArrayList.java:437)

Any idea how to go about this?

1 Answers1

0

In general, I would recommend using a Collator with a strength setting of Collator.PRIMARY to compare strings containing accents and varied cases (e.g., N vs n and é vs e). Unfortunately, Collator does not have a contains() function.

So we will make our own.

private static boolean contains(String source, String target) {
    if (target.length() > source.length()) {
        return false;
    }

    Collator collator = Collator.getInstance();
    collator.setStrength(Collator.PRIMARY);

    int end = source.length() - target.length() + 1;

    for (int i = 0; i < end; i++) {
        String sourceSubstring = source.substring(i, i + target.length());

        if (collator.compare(sourceSubstring, target) == 0) {
            return true;
        }
    }

    return false;
}

This iterates over the source string, and checks whether each substring with the same length as the search target is equal to the search target, as far as the Collator is concerned.

For example, let's imagine our source string is "This is a Tèst" and we are searching for the word "test". This method will iterate over every four-letter substring:

This
his 
is i
s is
 is 
is a
s a 
 a T
a Tè
 Tès
Tèst

And will return true as soon as it finds a match. Since the strength is set to Collator.PRIMARY, the collator considers "Tèst" and "test" to be equal, and so our method returns true.

It's quite possible that there's are more optimizations to be made to this method, but this should be a reasonable starting point.

Edit: One possible optimization is to leverage collation keys as well as known details of the implementation of RuleBasedCollator and RuleBasedCollationKey (assuming you have Google's Guava in your project):

private static boolean containsBytes(String source, String target) {
    Collator collator = Collator.getInstance();
    collator.setStrength(Collator.PRIMARY);

    byte[] sourceBytes = dropLastFour(collator.getCollationKey(source).toByteArray());
    byte[] targetBytes = dropLastFour(collator.getCollationKey(target).toByteArray());

    return Bytes.indexOf(sourceBytes, targetBytes) >= 0;
}

private static byte[] dropLastFour(byte[] in) {
    return Arrays.copyOf(in, in.length - 4);
}

This is considerably more fragile (probably doesn't work for all locales), but in my tests it is somewhere between 2x and 10x faster.

Edit: To support highlighting, you should convert contains() to indexOf(), and then use that information:

private static int indexOf(String source, String target) {
    if (target.length() > source.length()) {
        return -1;
    }

    Collator collator = Collator.getInstance();
    collator.setStrength(Collator.PRIMARY);

    int end = source.length() - target.length() + 1;

    for (int i = 0; i < end; i++) {
        String sourceSubstring = source.substring(i, i + target.length());

        if (collator.compare(sourceSubstring, target) == 0) {
            return i;
        }
    }

    return -1;
}

And then you could apply it like this:

String guestWholeName = guest.getGuestFirstName() + " " + guest.getGuestLastName();
int wholeNameIndex = indexOf(guestWholeName, searchText);

if (wholeNameIndex > -1) {
    Timber.d("guest name first : guest.getGuestFirstName() %s", guest.getGuestFirstName());
    Timber.d("guest name last : guest.getGuestLastName() %s", guest.getGuestLastName());

    int endPos = wholeNameIndex + searchText.length();

    Spannable spannable = new SpannableString(guestWholeName);
    Typeface firstNameFont = Typeface.createFromAsset(context.getAssets(), "fonts/Graphik-Semibold.otf");
    spannable.setSpan(new CustomTypefaceSpan("", firstNameFont), wholeNameIndex, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    Objects.requireNonNull(guestName).setText(spannable);
} else {
    Objects.requireNonNull(guestName).setText(guestWholeName);
}
Ben P.
  • 44,716
  • 5
  • 70
  • 96
  • That does seem to work, however, I get this crash often : java.lang.IndexOutOfBoundsException: Index: 7, Size: 0 at java.util.ArrayList.get(ArrayList.java:437) at xxx.searchGuests.SearchGuestListAdapter$ViewHolder.onBind(SearchGuestListAdapter.java:196) ? I have posted my searchadapter code in the pastebin link. maybe some issue with my logic? –  Oct 26 '19 at 01:29
  • This is also another common crash report i am getting after the update : https://pastebin.com/tN1gciZ7 –  Oct 26 '19 at 01:35
  • I'd be happy to look (and I'm sure others would be, too), but I recommend opening a new question with specifics around that. – Ben P. Oct 26 '19 at 03:01
  • not sure if the changes made to the search filter are triggering this. I can open a new question soon, but wondering if it's something as quick as adding notifydatasetchanged to one of the functions? or updating a line in the filter results. –  Oct 26 '19 at 10:39
  • also, related to this question, how do I use your function to highlight my searched result? The accented results don't highlight, but only normal letters do. what needs to be changed in my onbind function ( where the highlight is applied) to apply the highlights? –  Oct 26 '19 at 10:41
  • added the crash related question here : https://stackoverflow.com/questions/58570090/indexoutofbounexception-in-search-adapter-onbind-method –  Oct 26 '19 at 10:51
  • I have set a bounty to that question too, seems like its a bigger issue than expected. Not sure whats missing, might be a line or 2. Let me know if you have any ideas. thanks! –  Oct 28 '19 at 16:25
  • @AngelaHeely I've updated my answer to include information on highlighting the search matches – Ben P. Oct 28 '19 at 16:53
  • I tried the updated answer, however I keep getting crashes with it as soon as I type in any text : IndexOutOfBoundsException: Inconsistency detected. Invalid item position 15(offset:15).state:25 android.support.v7.widget.RecyclerView{31b59f0 VFED..... ......ID 0,0-1080,4725 #7f090216 app:id/search_guests_recycler_view}, adapter:com.myapp.ui.registervisitor.searchGuests.SearchGuestListAdapter@7ac9b69, layout:android.support.v7.widget.LinearLayoutManager@80e59ee –  Oct 28 '19 at 17:11
  • That crash has to do with the contents of the collection backing the adapter being out of sync with what the adapter was last notified of (e.g. you remove an item but don't call `notifyDataSetChanged()` or `notifyItemRemoved()`) – Ben P. Oct 28 '19 at 17:13
  • can you check out my other question for this crash and let me know how i can resolve it, where it might be happening. I can share my activity code for this aswell) , will update the other question shortly. I can't verify if this change is working without that as with these changes it crashes every single time i try to search something :( –  Oct 28 '19 at 17:23
  • I probably don't have time to look at it today, but I will try to come back to it when I do have time. Good luck. – Ben P. Oct 28 '19 at 17:25
  • thanks, I have posted the activity code in there aswell, might be something small, if you have time today it will be awesome, thanks for the help i appreciate it, sorry i couldnt verify your changes due to that crash –  Oct 28 '19 at 17:27
  • Hi Ben, the issue with the crash is fixed and so your code works great. One last question though, how do you highlight the text even if theres a space before and after the text, say I have lucy carmichael , and i add 2 spaces before lucy and 3 spaces after carmichael? I am adding trim to the filter and it does show the text filtered , but it doesnt highlight the text filtered after spaces are added before and after text –  Oct 29 '19 at 18:27
  • any idea about this one : https://stackoverflow.com/questions/59885424/how-to-stop-incessant-crash-on-search-filter-in-android-recyclerview?noredirect=1#comment105909172_59885424 –  Jan 24 '20 at 23:14