LocaleUtils.java revision 94027c7201a376107a35ec78cd21db1905662601
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
17package com.android.inputmethod.dictionarypack;
18
19import android.content.res.Configuration;
20import android.content.res.Resources;
21import android.text.TextUtils;
22
23import java.util.HashMap;
24import java.util.Locale;
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 */
33public 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(final String referenceLocale, final String testedLocale) {
108        if (TextUtils.isEmpty(referenceLocale)) {
109            return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
110        }
111        if (null == testedLocale) return LOCALE_NO_MATCH;
112        final String[] referenceParams = referenceLocale.split("_", 3);
113        final String[] testedParams = testedLocale.split("_", 3);
114        // By spec of String#split, [0] cannot be null and length cannot be 0.
115        if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH;
116        switch (referenceParams.length) {
117        case 1:
118            return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH;
119        case 2:
120            if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
121            if (!referenceParams[1].equals(testedParams[1]))
122                return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
123            if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH;
124            return LOCALE_FULL_MATCH;
125        case 3:
126            if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
127            if (!referenceParams[1].equals(testedParams[1]))
128                return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
129            if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
130            if (!referenceParams[2].equals(testedParams[2]))
131                return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
132            return LOCALE_FULL_MATCH;
133        }
134        // It should be impossible to come here
135        return LOCALE_NO_MATCH;
136    }
137
138    /**
139     * Return a string that represents this match level, with better matches first.
140     *
141     * The strings are sorted in lexicographic order: a better match will always be less than
142     * a worse match when compared together.
143     */
144    public static String getMatchLevelSortedString(final int matchLevel) {
145        // This works because the match levels are 0~99 (actually 0~30)
146        // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
147        return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
148    }
149
150    /**
151     * Find out whether a match level should be considered a match.
152     *
153     * This method takes a match level as returned by the #getMatchLevel method, and returns whether
154     * it should be considered a match in the usual sense with standard Locale functions.
155     *
156     * @param level the match level, as returned by getMatchLevel.
157     * @return whether this is a match or not.
158     */
159    public static boolean isMatch(final int level) {
160        return LOCALE_MATCH <= level;
161    }
162
163    /**
164     * Sets the system locale for this process.
165     *
166     * @param res the resources to use. Pass current resources.
167     * @param newLocale the locale to change to.
168     * @return the old locale.
169     */
170    public static Locale setSystemLocale(final Resources res, final Locale newLocale) {
171        final Configuration conf = res.getConfiguration();
172        final Locale saveLocale = conf.locale;
173        conf.locale = newLocale;
174        res.updateConfiguration(conf, res.getDisplayMetrics());
175        return saveLocale;
176    }
177
178    private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
179
180    /**
181     * Creates a locale from a string specification.
182     */
183    public static Locale constructLocaleFromString(final String localeStr) {
184        if (localeStr == null)
185            return null;
186        synchronized (sLocaleCache) {
187            if (sLocaleCache.containsKey(localeStr))
188                return sLocaleCache.get(localeStr);
189            Locale retval = null;
190            String[] localeParams = localeStr.split("_", 3);
191            if (localeParams.length == 1) {
192                retval = new Locale(localeParams[0]);
193            } else if (localeParams.length == 2) {
194                retval = new Locale(localeParams[0], localeParams[1]);
195            } else if (localeParams.length == 3) {
196                retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
197            }
198            if (retval != null) {
199                sLocaleCache.put(localeStr, retval);
200            }
201            return retval;
202        }
203    }
204}
205