aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
blob: 4bab497baa7df17e5ec4696b7bee7befb2e537a1 (about) (plain) (blame)
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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.android.inputmethod.latin.utils;

import android.text.TextUtils;

import java.util.HashMap;
import java.util.Locale;

import javax.annotation.Nullable;

/**
 * A class to help with handling Locales in string form.
 *
 * This file has the same meaning and features (and shares all of its code) with
 * the one in the dictionary pack. They need to be kept synchronized; for any
 * update/bugfix to this file, consider also updating/fixing the version in the
 * dictionary pack.
 */
public final class LocaleUtils {
    private LocaleUtils() {
        // Intentional empty constructor for utility class.
    }

    // Locale match level constants.
    // A higher level of match is guaranteed to have a higher numerical value.
    // Some room is left within constants to add match cases that may arise necessary
    // in the future, for example differentiating between the case where the countries
    // are both present and different, and the case where one of the locales does not
    // specify the countries. This difference is not needed now.

    // Nothing matches.
    public static final int LOCALE_NO_MATCH = 0;
    // The languages matches, but the country are different. Or, the reference locale requires a
    // country and the tested locale does not have one.
    public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3;
    // The languages and country match, but the variants are different. Or, the reference locale
    // requires a variant and the tested locale does not have one.
    public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6;
    // The required locale is null or empty so it will accept anything, and the tested locale
    // is non-null and non-empty.
    public static final int LOCALE_ANY_MATCH = 10;
    // The language matches, and the tested locale specifies a country but the reference locale
    // does not require one.
    public static final int LOCALE_LANGUAGE_MATCH = 15;
    // The language and the country match, and the tested locale specifies a variant but the
    // reference locale does not require one.
    public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20;
    // The compared locales are fully identical. This is the best match level.
    public static final int LOCALE_FULL_MATCH = 30;

    // The level at which a match is "normally" considered a locale match with standard algorithms.
    // Don't use this directly, use #isMatch to test.
    private static final int LOCALE_MATCH = LOCALE_ANY_MATCH;

    // Make this match the maximum match level. If this evolves to have more than 2 digits
    // when written in base 10, also adjust the getMatchLevelSortedString method.
    private static final int MATCH_LEVEL_MAX = 30;

    /**
     * Return how well a tested locale matches a reference locale.
     *
     * This will check the tested locale against the reference locale and return a measure of how
     * a well it matches the reference. The general idea is that the tested locale has to match
     * every specified part of the required locale. A full match occur when they are equal, a
     * partial match when the tested locale agrees with the reference locale but is more specific,
     * and a difference when the tested locale does not comply with all requirements from the
     * reference locale.
     * In more detail, if the reference locale specifies at least a language and the testedLocale
     * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the
     * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH
     * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and
     * tested locale agree on the language, but not on the country,
     * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country,
     * and LOCALE_LANGUAGE_MATCH otherwise.
     * If they agree on both the language and the country, but not on the variant,
     * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale
     * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches,
     * LOCALE_FULL_MATCH is returned.
     * Examples:
     * en <=> en_US  => LOCALE_LANGUAGE_MATCH
     * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
     * en_US_POSIX <=> en_US_Android  =>  LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
     * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH
     * sp_US <=> en_US  =>  LOCALE_NO_MATCH
     * de <=> de  => LOCALE_FULL_MATCH
     * en_US <=> en_US => LOCALE_FULL_MATCH
     * "" <=> en_US => LOCALE_ANY_MATCH
     *
     * @param referenceLocale the reference locale to test against.
     * @param testedLocale the locale to test.
     * @return a constant that measures how well the tested locale matches the reference locale.
     */
    public static int getMatchLevel(String referenceLocale, String testedLocale) {
        if (TextUtils.isEmpty(referenceLocale)) {
            return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
        }
        if (null == testedLocale) return LOCALE_NO_MATCH;
        String[] referenceParams = referenceLocale.split("_", 3);
        String[] testedParams = testedLocale.split("_", 3);
        // By spec of String#split, [0] cannot be null and length cannot be 0.
        if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH;
        switch (referenceParams.length) {
        case 1:
            return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH;
        case 2:
            if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
            if (!referenceParams[1].equals(testedParams[1]))
                return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
            if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH;
            return LOCALE_FULL_MATCH;
        case 3:
            if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
            if (!referenceParams[1].equals(testedParams[1]))
                return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
            if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
            if (!referenceParams[2].equals(testedParams[2]))
                return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
            return LOCALE_FULL_MATCH;
        }
        // It should be impossible to come here
        return LOCALE_NO_MATCH;
    }

    /**
     * Return a string that represents this match level, with better matches first.
     *
     * The strings are sorted in lexicographic order: a better match will always be less than
     * a worse match when compared together.
     */
    public static String getMatchLevelSortedString(int matchLevel) {
        // This works because the match levels are 0~99 (actually 0~30)
        // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
        return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
    }

    /**
     * Find out whether a match level should be considered a match.
     *
     * This method takes a match level as returned by the #getMatchLevel method, and returns whether
     * it should be considered a match in the usual sense with standard Locale functions.
     *
     * @param level the match level, as returned by getMatchLevel.
     * @return whether this is a match or not.
     */
    public static boolean isMatch(int level) {
        return LOCALE_MATCH <= level;
    }

    private static final HashMap<String, Locale> sLocaleCache = new HashMap<>();

    /**
     * Creates a locale from a string specification.
     */
    @Nullable
    public static Locale constructLocaleFromString(@Nullable final String localeStr) {
        if (localeStr == null) {
            // TODO: Should this be Locale.ROOT?
            return null;
        }
        synchronized (sLocaleCache) {
            Locale retval = sLocaleCache.get(localeStr);
            if (retval != null) {
                return retval;
            }
            final String[] localeParams = localeStr.split("_", 3);
            if (localeParams.length == 1) {
                retval = new Locale(localeParams[0]);
            } else if (localeParams.length == 2) {
                retval = new Locale(localeParams[0], localeParams[1]);
            } else if (localeParams.length == 3) {
                retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
            }
            if (retval != null) {
                sLocaleCache.put(localeStr, retval);
            }
            // TODO: Should return Locale.ROOT instead of null?
            return retval;
        }
    }
}