1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
/*
* 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.base.dictionaries.Dictionary;
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 Dictionary.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);
}
}
|