Home | History | Annotate | Download | only in data
      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 
     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 
     66     public String getId() { return mId; }
     67     public int getIndex() { return mIndex; }
     68     public String getName() { return mName; }
     69     public TimeZone getTimeZone() { return mTimeZone; }
     70     public String getIndexString() { return mIndexString; }
     71     public String getPhoneticName() { return mPhoneticName; }
     72 
     73     /**
     74      * @return the city name converted to upper case
     75      */
     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      */
     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      */
     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
    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      */
    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 
    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 
    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
    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
    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 }