/*
* Copyright (c) 2013 Menny Even-Danan
*
* 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.anysoftkeyboard.spellcheck;
import com.anysoftkeyboard.dictionaries.Dictionary;
import com.anysoftkeyboard.dictionaries.Dictionary.WordCallback;
import com.anysoftkeyboard.utils.ArraysCompatUtils;
import com.anysoftkeyboard.utils.IMEUtil;
import com.anysoftkeyboard.utils.Log;
import java.util.ArrayList;
import java.util.Collections;
class SuggestionsGatherer implements WordCallback {
public static class Result {
public final String[] mSuggestions;
public final boolean mHasLikelySuggestions;
public Result(final String[] gatheredSuggestions, final boolean hasLikelySuggestions) {
mSuggestions = gatheredSuggestions;
mHasLikelySuggestions = hasLikelySuggestions;
}
}
private final ArrayList<CharSequence> mSuggestions;
private final int[] mScores;
private final String mOriginalText;
private final double mSuggestionThreshold;
private final double mLikelyThreshold;
private final int mMaxLength;
private int mLength = 0;
// The two following attributes are only ever filled if the requested max length
// is 0 (or less, which is treated the same).
private String mBestSuggestion = null;
private int mBestScore = Integer.MIN_VALUE; // As small as possible
SuggestionsGatherer(final String originalText, final double suggestionThreshold,
final double likelyThreshold, final int maxLength) {
mOriginalText = originalText;
mSuggestionThreshold = suggestionThreshold;
mLikelyThreshold = likelyThreshold;
mMaxLength = maxLength;
mSuggestions = new ArrayList<>(maxLength + 1);
mScores = new int[mMaxLength];
}
@Override
public boolean addWord(char[] word, int wordOffset, int wordLength,
int frequency, Dictionary dictionary) {
final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, frequency);
// binarySearch returns the index if the element exists, and -<insertion index> - 1
// if it doesn't. See documentation for binarySearch.
final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
if (insertIndex == 0 && mLength >= mMaxLength) {
// In the future, we may want to keep track of the best suggestion score even if
// we are asked for 0 suggestions. In this case, we can use the following
// (tested) code to keep it:
// If the maxLength is 0 (should never be less, but if it is, it's treated as 0)
// then we need to keep track of the best suggestion in mBestScore and
// mBestSuggestion. This is so that we know whether the best suggestion makes
// the score cutoff, since we need to know that to return a meaningful
// looksLikeTypo.
// if (0 >= mMaxLength) {
// if (score > mBestScore) {
// mBestScore = score;
// mBestSuggestion = new String(word, wordOffset, wordLength);
// }
// }
return true;
}
if (insertIndex >= mMaxLength) {
// We found a suggestion, but its score is too weak to be kept considering
// the suggestion limit.
return true;
}
// Compute the normalized score and skip this word if it's normalized score does not
// make the threshold.
final String wordString = new String(word, wordOffset, wordLength);
final double normalizedScore =
IMEUtil.calcNormalizedScore(mOriginalText, wordString, frequency);
if (normalizedScore < mSuggestionThreshold) {
if (AnySpellCheckerService.DBG)
Log.i(AnySpellCheckerService.TAG, wordString + " does not make the score threshold");
return true;
}
if (mLength < mMaxLength) {
final int copyLen = mLength - insertIndex;
++mLength;
System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
mSuggestions.add(insertIndex, wordString);
} else {
System.arraycopy(mScores, 1, mScores, 0, insertIndex);
mSuggestions.add(insertIndex, wordString);
mSuggestions.remove(0);
}
mScores[insertIndex] = frequency;
return true;
}
public SuggestionsGatherer.Result getResults(final int capitalizeType) {
final String[] gatheredSuggestions;
final boolean hasLikelySuggestions;
if (0 == mLength) {
// Either we found no suggestions, or we found some BUT the max length was 0.
// If we found some mBestSuggestion will not be null. If it is null, then
// we found none, regardless of the max length.
if (null == mBestSuggestion) {
gatheredSuggestions = null;
hasLikelySuggestions = false;
} else {
gatheredSuggestions = AnySpellCheckerService.EMPTY_STRING_ARRAY;
final double normalizedScore =
IMEUtil.calcNormalizedScore(mOriginalText, mBestSuggestion, mBestScore);
hasLikelySuggestions = (normalizedScore > mLikelyThreshold);
}
} else {
if (AnySpellCheckerService.DBG) {
if (mLength != mSuggestions.size()) {
Log.e(AnySpellCheckerService.TAG, "Suggestion size is not the same as stored mLength");
}
for (int i = mLength - 1; i >= 0; --i) {
Log.i(AnySpellCheckerService.TAG, "" + mScores[i] + " " + mSuggestions.get(i));
}
}
Collections.reverse(mSuggestions);
IMEUtil.removeDupes(mSuggestions);
/*
if (CAPITALIZE_ALL == capitalizeType) {
for (int i = 0; i < mSuggestions.size(); ++i) {
// get(i) returns a CharSequence which is actually a String so .toString()
// should return the same object.
mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale));
}
} else if (CAPITALIZE_FIRST == capitalizeType) {
for (int i = 0; i < mSuggestions.size(); ++i) {
// Likewise
mSuggestions.set(i, Utils.toTitleCase(mSuggestions.get(i).toString(),
locale));
}
}
*/
// This returns a String[], while toArray() returns an Object[] which cannot be cast
// into a String[].
gatheredSuggestions = mSuggestions.toArray(AnySpellCheckerService.EMPTY_STRING_ARRAY);
final int bestScore = mScores[mLength - 1];
final CharSequence bestSuggestion = mSuggestions.get(0);
final double normalizedScore =
IMEUtil.calcNormalizedScore(mOriginalText, bestSuggestion, bestScore);
hasLikelySuggestions = (normalizedScore > mLikelyThreshold);
if (AnySpellCheckerService.DBG) {
Log.i(AnySpellCheckerService.TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore);
Log.i(AnySpellCheckerService.TAG, "Normalized score = " + normalizedScore
+ " (threshold " + mLikelyThreshold
+ ") => hasLikelySuggestions = " + hasLikelySuggestions);
}
}
return new Result(gatheredSuggestions, hasLikelySuggestions);
}
}