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