diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/StringUtils.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/StringUtils.java | 227 |
1 files changed, 132 insertions, 95 deletions
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 4dec7881b..6dc1ea807 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -18,6 +18,8 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import com.android.inputmethod.keyboard.Keyboard; // For character constants + import java.util.ArrayList; import java.util.Locale; @@ -123,23 +125,6 @@ public final class StringUtils { } /** - * Returns true if cs contains any upper case characters. - * - * @param cs the CharSequence to check - * @return {@code true} if cs contains any upper case characters, {@code false} otherwise. - */ - public static boolean hasUpperCase(final CharSequence cs) { - final int length = cs.length(); - for (int i = 0, cp = 0; i < length; i += Character.charCount(cp)) { - cp = Character.codePointAt(cs, i); - if (Character.isUpperCase(cp)) { - return true; - } - } - return false; - } - - /** * Remove duplicates from an array of strings. * * This method will always keep the first occurrence of all strings at their position @@ -209,19 +194,16 @@ public final class StringUtils { * * @param cs The text that should be checked for caps modes. * @param reqModes The modes to be checked: may be any combination of - * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and - * {@link #CAP_MODE_SENTENCES}. + * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES}. + * @param locale The locale to consider for capitalization rules * * @return Returns the actual capitalization modes that can be in effect * at the current position, which is any combination of - * {@link #CAP_MODE_CHARACTERS}, {@link #CAP_MODE_WORDS}, and - * {@link #CAP_MODE_SENTENCES}. + * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and + * {@link TextUtils#CAP_MODE_SENTENCES}. */ - public static int getCapsMode(CharSequence cs, int reqModes) { - int i; - char c; - int mode = 0; - + public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale) { // Quick description of what we want to do: // CAP_MODE_CHARACTERS is always on. // CAP_MODE_WORDS is on if there is some whitespace before the cursor. @@ -234,14 +216,11 @@ public final class StringUtils { // be immediately preceded by punctuation, or by a string of only letters with single // periods interleaved. - // Step 1 : check for cap mode characters. If it's looked for, it's always on. - if ((reqModes & TextUtils.CAP_MODE_CHARACTERS) != 0) { - mode |= TextUtils.CAP_MODE_CHARACTERS; - } + // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on. if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) { - // Here we are not looking for words or sentences modes, so since we already evaluated - // mode characters, we can return. - return mode; + // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already + // evaluated MODE_CHARACTERS, we can return. + return TextUtils.CAP_MODE_CHARACTERS & reqModes; } // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes @@ -250,9 +229,11 @@ public final class StringUtils { // it may look like a right parenthesis for example. We also include double quote and // single quote since they aren't start punctuation in the unicode sense, but should still // be skipped for English. TODO: does this depend on the language? + int i; for (i = cs.length(); i > 0; i--) { - c = cs.charAt(i - 1); - if (c != '"' && c != '\'' && Character.getType(c) != Character.START_PUNCTUATION) { + final char c = cs.charAt(i - 1); + if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.START_PUNCTUATION) { break; } } @@ -266,79 +247,135 @@ public final class StringUtils { // if the first char that's not a space or tab is a start of line (as in, either \n or // start of text). int j = i; - while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { + while (j > 0 && Character.isWhitespace(cs.charAt(j - 1))) { j--; } - if (j == 0 || cs.charAt(j - 1) == '\n') { - // Here we know we are at the start of a paragraph, so we turn on word mode. - // Note: I think this is entirely buggy. It will return mode words even if the app - // didn't request it, and it will fail to return sentence mode even if this is actually - // the start of a sentence. As it happens, Latin IME client code considers that mode - // word *implies* mode sentence and tests for non-zeroness, so it happens to work. - return mode | TextUtils.CAP_MODE_WORDS; - } - if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) { - // If we don't have to check for mode sentence, then we know all we need to know - // already. Either we have whitespace immediately before index i and we are at the - // start of a word, or we don't and we aren't. But we just went over any whitespace - // just before i and in fact j points before any whitespace, so if i != j that means - // there is such whitespace. In this case, we have mode words. - if (i != j) mode |= TextUtils.CAP_MODE_WORDS; - return mode; + if (j == 0) { + // There is only whitespace between the start of the text and the cursor. Both + // MODE_WORDS and MODE_SENTENCES should be active. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; } if (i == j) { - // Finally, if we don't have whitespace before index i, it means neither mode words + // If we don't have whitespace before index i, it means neither MODE_WORDS // nor mode sentences should be on so we can return right away. - return mode; + return TextUtils.CAP_MODE_CHARACTERS & reqModes; + } + if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) { + // Here we know we have whitespace before the cursor (if not, we returned in the above + // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate + // MODE_SENTENCES so we can return right away. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; } // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above, - // we know that mode sentences is being requested. + // we know that MODE_SENTENCES is being requested. - // Step 4 : Search for sentence mode. - for (; j > 0; j--) { - // Here we look to go over any closing punctuation. This is because in dominant variants - // of English, the final period is placed within double quotes and maybe other closing - // punctuation signs. - // TODO: this is wrong for almost everything except American typography rules for - // English. It's wrong for British typography rules for English, it's wrong for French, - // it's wrong for German, it's wrong for Spanish, and possibly everything else. - // (note that American rules and British rules have nothing to do with en_US and en_GB, - // as both rules are used in both countries - it's merely a name for the set of rules) - c = cs.charAt(j - 1); - if (c != '"' && c != '\'' && Character.getType(c) != Character.END_PUNCTUATION) { - break; + // Step 4 : Search for MODE_SENTENCES. + // English is a special case in that "American typography" rules, which are the most common + // in English, state that a sentence terminator immediately following a quotation mark + // should be swapped with it and de-duplicated (included in the quotation mark), + // e.g. <<Did he say, "let's go home?">> + // No other language has such a rule as far as I know, instead putting inside the quotation + // mark as the exact thing quoted and handling the surrounding punctuation independently, + // e.g. <<Did he say, "let's go home"?>> + // Hence, specifically for English, we treat this special case here. + if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { + for (; j > 0; j--) { + // Here we look to go over any closing punctuation. This is because in dominant + // variants of English, the final period is placed within double quotes and maybe + // other closing punctuation signs. This is generally not true in other languages. + final char c = cs.charAt(j - 1); + if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.END_PUNCTUATION) { + break; + } } } - if (j > 0) { - c = cs.charAt(j - 1); - if (c == '.' || c == '?' || c == '!') { - // Here we found a marker for sentence end (we consider these to be one of - // either . or ? or ! only). So this is probably the end of a sentence, but if we - // found a period, we still want to check the case where this is a abbreviation - // period rather than a full stop. To do this, we look for a period within a word - // before the period we just found; if any, we take that to mean it was an - // abbreviation. - // A typical example of the above is "In the U.S. ", where the last period is - // not a full stop and we should not capitalize. - // TODO: the rule below is broken. In particular it fails for runs of periods, - // whatever the reason. In the example "in the U.S..", the last period is a full - // stop following the abbreviation period, and we should capitalize but we don't. - // Likewise, "I don't know... " should capitalize, but fails to do so. - if (c == '.') { - for (int k = j - 2; k >= 0; k--) { - c = cs.charAt(k); - if (c == '.') { - return mode; - } - if (!Character.isLetter(c)) { - break; - } - } + if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes; + char c = cs.charAt(--j); + + // We found the next interesting chunk of text ; next we need to determine if it's the + // end of a sentence. If we have a question mark or an exclamation mark, it's the end of + // a sentence. If it's neither, the only remaining case is the period so we get the opposite + // case out of the way. + if (c == Keyboard.CODE_QUESTION_MARK || c == Keyboard.CODE_EXCLAMATION_MARK) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; + } + if (c != Keyboard.CODE_PERIOD || j <= 0) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + + // We found out that we have a period. We need to determine if this is a full stop or + // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation + // looks like (\w\.){2,} + // To find out, we will have a simple state machine with the following states : + // START, WORD, PERIOD, ABBREVIATION + // On START : (just before the first period) + // letter => WORD + // whitespace => end with no caps (it was a stand-alone period) + // otherwise => end with caps (several periods/symbols in a row) + // On WORD : (within the word just before the first period) + // letter => WORD + // period => PERIOD + // otherwise => end with caps (it was a word with a full stop at the end) + // On PERIOD : (period within a potential abbreviation) + // letter => LETTER + // otherwise => end with caps (it was not an abbreviation) + // On LETTER : (letter within a potential abbreviation) + // letter => LETTER + // period => PERIOD + // otherwise => end with no caps (it was an abbreviation) + // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This + // should capitalize. + + final int START = 0; + final int WORD = 1; + final int PERIOD = 2; + final int LETTER = 3; + final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; + final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + int state = START; + while (j > 0) { + c = cs.charAt(--j); + switch (state) { + case START: + if (Character.isLetter(c)) { + state = WORD; + } else if (Character.isWhitespace(c)) { + return noCaps; + } else { + return caps; + } + break; + case WORD: + if (Character.isLetter(c)) { + state = WORD; + } else if (c == Keyboard.CODE_PERIOD) { + state = PERIOD; + } else { + return caps; + } + break; + case PERIOD: + if (Character.isLetter(c)) { + state = LETTER; + } else { + return caps; + } + break; + case LETTER: + if (Character.isLetter(c)) { + state = LETTER; + } else if (c == Keyboard.CODE_PERIOD) { + state = PERIOD; + } else { + return noCaps; } - return mode | TextUtils.CAP_MODE_SENTENCES; } } - return mode; + // Here we arrived at the start of the line. This should behave exactly like whitespace. + return (START == state || LETTER == state) ? noCaps : caps; } } |