1 /*
2  * Copyright (C) 2020 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.Locale
21 import java.util.TimeZone
22 
23 /**
24  * A read-only domain object representing a city of the world and associated time information. It
25  * also contains static comparators that can be instantiated to order cities in common sort orders.
26  */
27 class City internal constructor(
28     /** A unique identifier for the city.  */
29     val id: String?,
30     /** An optional numeric index used to order cities for display; -1 if no such index exists.  */
31     val index: Int,
32     /** An index string used to order cities for display.  */
33     val indexString: String?,
34     /** The display name of the city.  */
35     val name: String,
36     /** The phonetic name of the city used to order cities for display.  */
37     val phoneticName: String,
38     /** The TimeZone corresponding to the city.  */
39     val timeZone: TimeZone
40 ) {
41 
42     /** A cached upper case form of the [.mName] used in case-insensitive name comparisons.  */
43     private var mNameUpperCase: String? = null
44 
45     /**
46      * A cached upper case form of the [.mName] used in case-insensitive name comparisons
47      * which ignore [.removeSpecialCharacters] special characters.
48      */
49     private var mNameUpperCaseNoSpecialCharacters: String? = null
50 
51     /**
52      * @return the city name converted to upper case
53      */
54     val nameUpperCase: String
55         get() {
56             if (mNameUpperCase == null) {
57                 mNameUpperCase = name.toUpperCase()
58             }
59             return mNameUpperCase!!
60         }
61 
62     /**
63      * @return the city name converted to upper case with all special characters removed
64      */
65     private val nameUpperCaseNoSpecialCharacters: String
66         get() {
67             if (mNameUpperCaseNoSpecialCharacters == null) {
68                 mNameUpperCaseNoSpecialCharacters = removeSpecialCharacters(nameUpperCase)
69             }
70             return mNameUpperCaseNoSpecialCharacters!!
71         }
72 
73     /**
74      * @param upperCaseQueryNoSpecialCharacters search term with all special characters removed
75      * to match against the upper case city name
76      * @return `true` iff the name of this city starts with the given query
77      */
matchesnull78     fun matches(upperCaseQueryNoSpecialCharacters: String): Boolean {
79         // By removing all special characters, prefix matching becomes more liberal and it is easier
80         // to locate the desired city. e.g. "St. Lucia" is matched by "StL", "St.L", "St L", "St. L"
81         return nameUpperCaseNoSpecialCharacters.startsWith(upperCaseQueryNoSpecialCharacters)
82     }
83 
toStringnull84     override fun toString(): String {
85         return String.format(Locale.US,
86                 "City {id=%s, index=%d, indexString=%s, name=%s, phonetic=%s, tz=%s}",
87                 id, index, indexString, name, phoneticName, timeZone.id)
88     }
89 
90     /**
91      * Orders by:
92      *
93      *  1. UTC offset of [timezone][.getTimeZone]
94      *  1. [numeric index][.getIndex]
95      *  1. [.getIndexString] alphabetic index}
96      *  1. [phonetic name][.getPhoneticName]
97      */
98     class UtcOffsetComparator : Comparator<City> {
99         private val mDelegate1: Comparator<City> = UtcOffsetIndexComparator()
100         private val mDelegate2: Comparator<City> = NameComparator()
101 
comparenull102         override fun compare(c1: City, c2: City): Int {
103             var result = mDelegate1.compare(c1, c2)
104 
105             if (result == 0) {
106                 result = mDelegate2.compare(c1, c2)
107             }
108 
109             return result
110         }
111     }
112 
113     /**
114      * Orders by:
115      *
116      *  1. UTC offset of [timezone][.getTimeZone]
117      */
118     class UtcOffsetIndexComparator : Comparator<City> {
119         // Snapshot the current time when the Comparator is created to obtain consistent offsets.
120         private val now = System.currentTimeMillis()
121 
comparenull122         override fun compare(c1: City, c2: City): Int {
123             val utcOffset1 = c1.timeZone.getOffset(now)
124             val utcOffset2 = c2.timeZone.getOffset(now)
125             return utcOffset1.compareTo(utcOffset2)
126         }
127     }
128 
129     /**
130      * This comparator sorts using the city fields that influence natural name sort order:
131      *
132      *  1. [numeric index][.getIndex]
133      *  1. [.getIndexString] alphabetic index}
134      *  1. [phonetic name][.getPhoneticName]
135      */
136     class NameComparator : Comparator<City> {
137         private val mDelegate: Comparator<City> = NameIndexComparator()
138 
139         // Locale-sensitive comparator for phonetic names.
140         private val mNameCollator = Collator.getInstance()
141 
comparenull142         override fun compare(c1: City, c2: City): Int {
143             var result = mDelegate.compare(c1, c2)
144 
145             if (result == 0) {
146                 result = mNameCollator.compare(c1.phoneticName, c2.phoneticName)
147             }
148 
149             return result
150         }
151     }
152 
153     /**
154      * Orders by:
155      *
156      *  1. [numeric index][.getIndex]
157      *  1. [.getIndexString] alphabetic index}
158      */
159     class NameIndexComparator : Comparator<City> {
160         // Locale-sensitive comparator for index strings.
161         private val mNameCollator = Collator.getInstance()
162 
comparenull163         override fun compare(c1: City, c2: City): Int {
164             var result = c1.index.compareTo(c2.index)
165 
166             if (result == 0) {
167                 result = mNameCollator.compare(c1.indexString, c2.indexString)
168             }
169 
170             return result
171         }
172     }
173 
174     companion object {
175         /**
176          * Strips out any characters considered optional for matching purposes. These include spaces,
177          * dashes, periods and apostrophes.
178          *
179          * @param token a city name or search term
180          * @return the given `token` without any characters considered optional when matching
181          */
182         @JvmStatic
removeSpecialCharactersnull183         fun removeSpecialCharacters(token: String): String {
184             return token.replace("[ -.']".toRegex(), "")
185         }
186     }
187 }