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 }