/* * Copyright (C) 2009 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.providers.contacts; import com.android.providers.contacts.util.Hex; import com.google.common.annotations.VisibleForTesting; import java.text.CollationKey; import java.text.Collator; import java.text.RuleBasedCollator; import java.util.Locale; /** * Converts a name to a normalized form by removing all non-letter characters and normalizing * UNICODE according to http://unicode.org/unicode/reports/tr15 */ public class NameNormalizer { private static final Object sCollatorLock = new Object(); private static Locale sCollatorLocale; private static RuleBasedCollator sCachedCompressingCollator; private static RuleBasedCollator sCachedComplexityCollator; /** * Ensure that the cached collators are for the current locale. */ private static void ensureCollators() { final Locale locale = Locale.getDefault(); if (locale.equals(sCollatorLocale)) { return; } sCollatorLocale = locale; sCachedCompressingCollator = (RuleBasedCollator) Collator.getInstance(locale); sCachedCompressingCollator.setStrength(Collator.PRIMARY); sCachedCompressingCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION); sCachedComplexityCollator = (RuleBasedCollator) Collator.getInstance(locale); sCachedComplexityCollator.setStrength(Collator.SECONDARY); } @VisibleForTesting static RuleBasedCollator getCompressingCollator() { synchronized (sCollatorLock) { ensureCollators(); return sCachedCompressingCollator; } } @VisibleForTesting static RuleBasedCollator getComplexityCollator() { synchronized (sCollatorLock) { ensureCollators(); return sCachedComplexityCollator; } } /** * Converts the supplied name to a string that can be used to perform approximate matching * of names. It ignores non-letter, non-digit characters, and removes accents. */ public static String normalize(String name) { CollationKey key = getCompressingCollator().getCollationKey(lettersAndDigitsOnly(name)); return Hex.encodeHex(key.toByteArray(), true); } /** * Compares "complexity" of two names, which is determined by the presence * of mixed case characters, accents and, if all else is equal, length. */ public static int compareComplexity(String name1, String name2) { String clean1 = lettersAndDigitsOnly(name1); String clean2 = lettersAndDigitsOnly(name2); int diff = getComplexityCollator().compare(clean1, clean2); if (diff != 0) { return diff; } // compareTo sorts uppercase first. We know that there are no non-case // differences from the above test, so we can negate here to get the // lowercase-first comparison we really want... diff = -clean1.compareTo(clean2); if (diff != 0) { return diff; } return name1.length() - name2.length(); } /** * Returns a string containing just the letters and digits from the original string. * Returns empty string if the original string is null. */ private static String lettersAndDigitsOnly(String name) { if (name == null) { return ""; } char[] letters = name.toCharArray(); int length = 0; for (int i = 0; i < letters.length; i++) { final char c = letters[i]; if (Character.isLetterOrDigit(c)) { letters[length++] = c; } } if (length != letters.length) { return new String(letters, 0, length); } return name; } }