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