| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.quicksearchbox; |
| |
| import com.android.quicksearchbox.util.LevenshteinDistance; |
| import com.android.quicksearchbox.util.LevenshteinDistance.Token; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import android.text.SpannableString; |
| import android.text.Spanned; |
| import android.util.Log; |
| |
| /** |
| * Suggestion formatter using the Levenshtein distance (minumum edit distance) to calculate the |
| * formatting. |
| */ |
| public class LevenshteinSuggestionFormatter extends SuggestionFormatter { |
| private static final boolean DBG = false; |
| private static final String TAG = "QSB.LevenshteinSuggestionFormatter"; |
| |
| public LevenshteinSuggestionFormatter(TextAppearanceFactory spanFactory) { |
| super(spanFactory); |
| } |
| |
| @Override |
| public Spanned formatSuggestion(String query, String suggestion) { |
| if (DBG) Log.d(TAG, "formatSuggestion('" + query + "', '" + suggestion + "')"); |
| query = normalizeQuery(query); |
| final Token[] queryTokens = tokenize(query); |
| final Token[] suggestionTokens = tokenize(suggestion); |
| final int[] matches = findMatches(queryTokens, suggestionTokens); |
| if (DBG){ |
| Log.d(TAG, "source = " + queryTokens); |
| Log.d(TAG, "target = " + suggestionTokens); |
| Log.d(TAG, "matches = " + matches); |
| } |
| final SpannableString str = new SpannableString(suggestion); |
| |
| final int matchesLen = matches.length; |
| for (int i = 0; i < matchesLen; ++i) { |
| final Token t = suggestionTokens[i]; |
| int sourceLen = 0; |
| int thisMatch = matches[i]; |
| if (thisMatch >= 0) { |
| sourceLen = queryTokens[thisMatch].length(); |
| } |
| applySuggestedTextStyle(str, t.mStart + sourceLen, t.mEnd); |
| applyQueryTextStyle(str, t.mStart, t.mStart + sourceLen); |
| } |
| |
| return str; |
| } |
| |
| private String normalizeQuery(String query) { |
| return query.toLowerCase(); |
| } |
| |
| /** |
| * Finds which tokens in the target match tokens in the source. |
| * |
| * @param source List of source tokens (i.e. user query) |
| * @param target List of target tokens (i.e. suggestion) |
| * @return The indices into source which target tokens correspond to. A non-negative value n at |
| * position i means that target token i matches source token n. A negative value means that |
| * the target token i does not match any source token. |
| */ |
| @VisibleForTesting |
| int[] findMatches(Token[] source, Token[] target) { |
| final LevenshteinDistance table = new LevenshteinDistance(source, target); |
| table.calculate(); |
| final int targetLen = target.length; |
| final int[] result = new int[targetLen]; |
| LevenshteinDistance.EditOperation[] ops = table.getTargetOperations(); |
| for (int i = 0; i < targetLen; ++i) { |
| if (ops[i].getType() == LevenshteinDistance.EDIT_UNCHANGED) { |
| result[i] = ops[i].getPosition(); |
| } else { |
| result[i] = -1; |
| } |
| } |
| return result; |
| } |
| |
| @VisibleForTesting |
| Token[] tokenize(final String seq) { |
| int pos = 0; |
| final int len = seq.length(); |
| final char[] chars = seq.toCharArray(); |
| // There can't be more tokens than characters, make an array that is large enough |
| Token[] tokens = new Token[len]; |
| int tokenCount = 0; |
| while (pos < len) { |
| while (pos < len && (chars[pos] == ' ' || chars[pos] == '\t')) { |
| pos++; |
| } |
| int start = pos; |
| while (pos < len && !(chars[pos] == ' ' || chars[pos] == '\t')) { |
| pos++; |
| } |
| int end = pos; |
| if (start != end) { |
| tokens[tokenCount++] = new Token(chars, start, end); |
| } |
| } |
| // Create a token array of the right size and return |
| Token[] ret = new Token[tokenCount]; |
| System.arraycopy(tokens, 0, ret, 0, tokenCount); |
| return ret; |
| } |
| |
| } |