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 }