blob: dc12db1326a37a56c7aa43de12206fafb9782327 [file] [log] [blame]
/*
* 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;
}
}