1 /* 2 * Copyright (C) 2015 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.deskclock.data; 18 19 import java.text.Collator; 20 import java.util.Comparator; 21 import java.util.Locale; 22 import java.util.TimeZone; 23 24 /** 25 * A read-only domain object representing a city of the world and associated time information. It 26 * also contains static comparators that can be instantiated to order cities in common sort orders. 27 */ 28 public final class City { 29 30 /** A unique identifier for the city. */ 31 private final String mId; 32 33 /** An optional numeric index used to order cities for display; -1 if no such index exists. */ 34 private final int mIndex; 35 36 /** An index string used to order cities for display. */ 37 private final String mIndexString; 38 39 /** The display name of the city. */ 40 private final String mName; 41 42 /** The phonetic name of the city used to order cities for display. */ 43 private final String mPhoneticName; 44 45 /** The TimeZone corresponding to the city. */ 46 private final TimeZone mTimeZone; 47 48 /** A cached upper case form of the {@link #mName} used in case-insensitive name comparisons. */ 49 private String mNameUpperCase; 50 51 /** 52 * A cached upper case form of the {@link #mName} used in case-insensitive name comparisons 53 * which ignore {@link #removeSpecialCharacters(String)} special characters. 54 */ 55 private String mNameUpperCaseNoSpecialCharacters; 56 City(String id, int index, String indexString, String name, String phoneticName, TimeZone tz)57 City(String id, int index, String indexString, String name, String phoneticName, TimeZone tz) { 58 mId = id; 59 mIndex = index; 60 mIndexString = indexString; 61 mName = name; 62 mPhoneticName = phoneticName; 63 mTimeZone = tz; 64 } 65 getId()66 public String getId() { return mId; } getIndex()67 public int getIndex() { return mIndex; } getName()68 public String getName() { return mName; } getTimeZone()69 public TimeZone getTimeZone() { return mTimeZone; } getIndexString()70 public String getIndexString() { return mIndexString; } getPhoneticName()71 public String getPhoneticName() { return mPhoneticName; } 72 73 /** 74 * @return the city name converted to upper case 75 */ getNameUpperCase()76 public String getNameUpperCase() { 77 if (mNameUpperCase == null) { 78 mNameUpperCase = mName.toUpperCase(); 79 } 80 return mNameUpperCase; 81 } 82 83 /** 84 * @return the city name converted to upper case with all special characters removed 85 */ getNameUpperCaseNoSpecialCharacters()86 private String getNameUpperCaseNoSpecialCharacters() { 87 if (mNameUpperCaseNoSpecialCharacters == null) { 88 mNameUpperCaseNoSpecialCharacters = removeSpecialCharacters(getNameUpperCase()); 89 } 90 return mNameUpperCaseNoSpecialCharacters; 91 } 92 93 /** 94 * @param upperCaseQueryNoSpecialCharacters search term with all special characters removed 95 * to match against the upper case city name 96 * @return {@code true} iff the name of this city starts with the given query 97 */ matches(String upperCaseQueryNoSpecialCharacters)98 public boolean matches(String upperCaseQueryNoSpecialCharacters) { 99 // By removing all special characters, prefix matching becomes more liberal and it is easier 100 // to locate the desired city. e.g. "St. Lucia" is matched by "StL", "St.L", "St L", "St. L" 101 return getNameUpperCaseNoSpecialCharacters().startsWith(upperCaseQueryNoSpecialCharacters); 102 } 103 104 @Override toString()105 public String toString() { 106 return String.format(Locale.US, 107 "City {id=%s, index=%d, indexString=%s, name=%s, phonetic=%s, tz=%s}", 108 mId, mIndex, mIndexString, mName, mPhoneticName, mTimeZone.getID()); 109 } 110 111 /** 112 * Strips out any characters considered optional for matching purposes. These include spaces, 113 * dashes, periods and apostrophes. 114 * 115 * @param token a city name or search term 116 * @return the given {@code token} without any characters considered optional when matching 117 */ removeSpecialCharacters(String token)118 public static String removeSpecialCharacters(String token) { 119 return token.replaceAll("[ -.']", ""); 120 } 121 122 /** 123 * Orders by: 124 * 125 * <ol> 126 * <li>UTC offset of {@link #getTimeZone() timezone}</li> 127 * <li>{@link #getIndex() numeric index}</li> 128 * <li>{@link #getIndexString()} alphabetic index}</li> 129 * <li>{@link #getPhoneticName() phonetic name}</li> 130 * </ol> 131 */ 132 public static final class UtcOffsetComparator implements Comparator<City> { 133 134 private final Comparator<City> mDelegate1 = new UtcOffsetIndexComparator(); 135 136 private final Comparator<City> mDelegate2 = new NameComparator(); 137 compare(City c1, City c2)138 public int compare(City c1, City c2) { 139 int result = mDelegate1.compare(c1, c2); 140 141 if (result == 0) { 142 result = mDelegate2.compare(c1, c2); 143 } 144 145 return result; 146 } 147 } 148 149 /** 150 * Orders by: 151 * 152 * <ol> 153 * <li>UTC offset of {@link #getTimeZone() timezone}</li> 154 * </ol> 155 */ 156 public static final class UtcOffsetIndexComparator implements Comparator<City> { 157 158 // Snapshot the current time when the Comparator is created to obtain consistent offsets. 159 private final long now = System.currentTimeMillis(); 160 compare(City c1, City c2)161 public int compare(City c1, City c2) { 162 final int utcOffset1 = c1.getTimeZone().getOffset(now); 163 final int utcOffset2 = c2.getTimeZone().getOffset(now); 164 return Integer.compare(utcOffset1, utcOffset2); 165 } 166 } 167 168 /** 169 * This comparator sorts using the city fields that influence natural name sort order: 170 * 171 * <ol> 172 * <li>{@link #getIndex() numeric index}</li> 173 * <li>{@link #getIndexString()} alphabetic index}</li> 174 * <li>{@link #getPhoneticName() phonetic name}</li> 175 * </ol> 176 */ 177 public static final class NameComparator implements Comparator<City> { 178 179 private final Comparator<City> mDelegate = new NameIndexComparator(); 180 181 // Locale-sensitive comparator for phonetic names. 182 private final Collator mNameCollator = Collator.getInstance(); 183 184 @Override compare(City c1, City c2)185 public int compare(City c1, City c2) { 186 int result = mDelegate.compare(c1, c2); 187 188 if (result == 0) { 189 result = mNameCollator.compare(c1.getPhoneticName(), c2.getPhoneticName()); 190 } 191 192 return result; 193 } 194 } 195 196 /** 197 * Orders by: 198 * 199 * <ol> 200 * <li>{@link #getIndex() numeric index}</li> 201 * <li>{@link #getIndexString()} alphabetic index}</li> 202 * </ol> 203 */ 204 public static final class NameIndexComparator implements Comparator<City> { 205 206 // Locale-sensitive comparator for index strings. 207 private final Collator mNameCollator = Collator.getInstance(); 208 209 @Override compare(City c1, City c2)210 public int compare(City c1, City c2) { 211 int result = Integer.compare(c1.getIndex(), c2.getIndex()); 212 213 if (result == 0) { 214 result = mNameCollator.compare(c1.getIndexString(), c2.getIndexString()); 215 } 216 217 return result; 218 } 219 } 220 }