Home | History | Annotate | Download | only in common
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.latin.common;
     18 
     19 import java.util.HashMap;
     20 import java.util.HashSet;
     21 import java.util.Locale;
     22 
     23 import javax.annotation.Nonnull;
     24 import javax.annotation.Nullable;
     25 
     26 /**
     27  * A class to help with handling Locales in string form.
     28  *
     29  * This file has the same meaning and features (and shares all of its code) with the one with the
     30  * same name in Latin IME. They need to be kept synchronized; for any update/bugfix to
     31  * this file, consider also updating/fixing the version in Latin IME.
     32  */
     33 public final class LocaleUtils {
     34     private LocaleUtils() {
     35         // Intentional empty constructor for utility class.
     36     }
     37 
     38     // Locale match level constants.
     39     // A higher level of match is guaranteed to have a higher numerical value.
     40     // Some room is left within constants to add match cases that may arise necessary
     41     // in the future, for example differentiating between the case where the countries
     42     // are both present and different, and the case where one of the locales does not
     43     // specify the countries. This difference is not needed now.
     44 
     45     // Nothing matches.
     46     public static final int LOCALE_NO_MATCH = 0;
     47     // The languages matches, but the country are different. Or, the reference locale requires a
     48     // country and the tested locale does not have one.
     49     public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3;
     50     // The languages and country match, but the variants are different. Or, the reference locale
     51     // requires a variant and the tested locale does not have one.
     52     public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6;
     53     // The required locale is null or empty so it will accept anything, and the tested locale
     54     // is non-null and non-empty.
     55     public static final int LOCALE_ANY_MATCH = 10;
     56     // The language matches, and the tested locale specifies a country but the reference locale
     57     // does not require one.
     58     public static final int LOCALE_LANGUAGE_MATCH = 15;
     59     // The language and the country match, and the tested locale specifies a variant but the
     60     // reference locale does not require one.
     61     public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20;
     62     // The compared locales are fully identical. This is the best match level.
     63     public static final int LOCALE_FULL_MATCH = 30;
     64 
     65     // The level at which a match is "normally" considered a locale match with standard algorithms.
     66     // Don't use this directly, use #isMatch to test.
     67     private static final int LOCALE_MATCH = LOCALE_ANY_MATCH;
     68 
     69     // Make this match the maximum match level. If this evolves to have more than 2 digits
     70     // when written in base 10, also adjust the getMatchLevelSortedString method.
     71     private static final int MATCH_LEVEL_MAX = 30;
     72 
     73     /**
     74      * Return how well a tested locale matches a reference locale.
     75      *
     76      * This will check the tested locale against the reference locale and return a measure of how
     77      * a well it matches the reference. The general idea is that the tested locale has to match
     78      * every specified part of the required locale. A full match occur when they are equal, a
     79      * partial match when the tested locale agrees with the reference locale but is more specific,
     80      * and a difference when the tested locale does not comply with all requirements from the
     81      * reference locale.
     82      * In more detail, if the reference locale specifies at least a language and the testedLocale
     83      * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the
     84      * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH
     85      * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and
     86      * tested locale agree on the language, but not on the country,
     87      * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country,
     88      * and LOCALE_LANGUAGE_MATCH otherwise.
     89      * If they agree on both the language and the country, but not on the variant,
     90      * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale
     91      * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches,
     92      * LOCALE_FULL_MATCH is returned.
     93      * Examples:
     94      * en <=> en_US  => LOCALE_LANGUAGE_MATCH
     95      * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
     96      * en_US_POSIX <=> en_US_Android  =>  LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
     97      * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH
     98      * sp_US <=> en_US  =>  LOCALE_NO_MATCH
     99      * de <=> de  => LOCALE_FULL_MATCH
    100      * en_US <=> en_US => LOCALE_FULL_MATCH
    101      * "" <=> en_US => LOCALE_ANY_MATCH
    102      *
    103      * @param referenceLocale the reference locale to test against.
    104      * @param testedLocale the locale to test.
    105      * @return a constant that measures how well the tested locale matches the reference locale.
    106      */
    107     public static int getMatchLevel(@Nullable final String referenceLocale,
    108             @Nullable final String testedLocale) {
    109         if (StringUtils.isEmpty(referenceLocale)) {
    110             return StringUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
    111         }
    112         if (null == testedLocale) return LOCALE_NO_MATCH;
    113         final String[] referenceParams = referenceLocale.split("_", 3);
    114         final String[] testedParams = testedLocale.split("_", 3);
    115         // By spec of String#split, [0] cannot be null and length cannot be 0.
    116         if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH;
    117         switch (referenceParams.length) {
    118         case 1:
    119             return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH;
    120         case 2:
    121             if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
    122             if (!referenceParams[1].equals(testedParams[1]))
    123                 return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
    124             if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH;
    125             return LOCALE_FULL_MATCH;
    126         case 3:
    127             if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
    128             if (!referenceParams[1].equals(testedParams[1]))
    129                 return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
    130             if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
    131             if (!referenceParams[2].equals(testedParams[2]))
    132                 return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
    133             return LOCALE_FULL_MATCH;
    134         }
    135         // It should be impossible to come here
    136         return LOCALE_NO_MATCH;
    137     }
    138 
    139     /**
    140      * Return a string that represents this match level, with better matches first.
    141      *
    142      * The strings are sorted in lexicographic order: a better match will always be less than
    143      * a worse match when compared together.
    144      */
    145     public static String getMatchLevelSortedString(final int matchLevel) {
    146         // This works because the match levels are 0~99 (actually 0~30)
    147         // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
    148         return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
    149     }
    150 
    151     /**
    152      * Find out whether a match level should be considered a match.
    153      *
    154      * This method takes a match level as returned by the #getMatchLevel method, and returns whether
    155      * it should be considered a match in the usual sense with standard Locale functions.
    156      *
    157      * @param level the match level, as returned by getMatchLevel.
    158      * @return whether this is a match or not.
    159      */
    160     public static boolean isMatch(final int level) {
    161         return LOCALE_MATCH <= level;
    162     }
    163 
    164     private static final HashMap<String, Locale> sLocaleCache = new HashMap<>();
    165 
    166     /**
    167      * Creates a locale from a string specification.
    168      * @param localeString a string specification of a locale, in a format of "ll_cc_variant" where
    169      * "ll" is a language code, "cc" is a country code.
    170      */
    171     @Nonnull
    172     public static Locale constructLocaleFromString(@Nonnull final String localeString) {
    173         synchronized (sLocaleCache) {
    174             if (sLocaleCache.containsKey(localeString)) {
    175                 return sLocaleCache.get(localeString);
    176             }
    177             final String[] elements = localeString.split("_", 3);
    178             final Locale locale;
    179             if (elements.length == 1) {
    180                 locale = new Locale(elements[0] /* language */);
    181             } else if (elements.length == 2) {
    182                 locale = new Locale(elements[0] /* language */, elements[1] /* country */);
    183             } else { // localeParams.length == 3
    184                 locale = new Locale(elements[0] /* language */, elements[1] /* country */,
    185                         elements[2] /* variant */);
    186             }
    187             sLocaleCache.put(localeString, locale);
    188             return locale;
    189         }
    190     }
    191 
    192     // TODO: Get this information from the framework instead of maintaining here by ourselves.
    193     private static final HashSet<String> sRtlLanguageCodes = new HashSet<>();
    194     static {
    195         // List of known Right-To-Left language codes.
    196         sRtlLanguageCodes.add("ar"); // Arabic
    197         sRtlLanguageCodes.add("fa"); // Persian
    198         sRtlLanguageCodes.add("iw"); // Hebrew
    199         sRtlLanguageCodes.add("ku"); // Kurdish
    200         sRtlLanguageCodes.add("ps"); // Pashto
    201         sRtlLanguageCodes.add("sd"); // Sindhi
    202         sRtlLanguageCodes.add("ug"); // Uyghur
    203         sRtlLanguageCodes.add("ur"); // Urdu
    204         sRtlLanguageCodes.add("yi"); // Yiddish
    205     }
    206 
    207     public static boolean isRtlLanguage(@Nonnull final Locale locale) {
    208         return sRtlLanguageCodes.contains(locale.getLanguage());
    209     }
    210 }
    211