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.icu.text; 18 19 import android.icu.text.TimeZoneNames; 20 import android.icu.util.TimeZone; 21 import android.icu.util.ULocale; 22 23 import libcore.api.IntraCoreApi; 24 import libcore.util.NonNull; 25 import libcore.util.Nullable; 26 27 import java.util.ArrayList; 28 import java.util.Collection; 29 import java.util.Collections; 30 import java.util.EnumSet; 31 import java.util.List; 32 import java.util.Set; 33 34 /** 35 * Provide extra functionalities on top of {@link TimeZoneNames} public APIs. 36 * 37 * @hide 38 */ 39 @IntraCoreApi 40 public class ExtendedTimeZoneNames { 41 42 private static final Set<TimeZoneNames.NameType> DST_NAME_TYPES = 43 Collections.unmodifiableSet( 44 EnumSet.of( 45 TimeZoneNames.NameType.LONG_DAYLIGHT, 46 TimeZoneNames.NameType.SHORT_DAYLIGHT)); 47 48 private static final EnumSet<TimeZoneNames.NameType> STANDARD_AND_DST_TYPES = 49 EnumSet.of( 50 TimeZoneNames.NameType.SHORT_STANDARD, 51 TimeZoneNames.NameType.LONG_STANDARD, 52 TimeZoneNames.NameType.SHORT_DAYLIGHT, 53 TimeZoneNames.NameType.LONG_DAYLIGHT); 54 55 private final ULocale locale; 56 private final TimeZoneNames timeZoneNames; 57 /** 58 * A class representing the return result of {@link #matchName(CharSequence, int, String)} 59 * 60 * @hide 61 */ 62 @IntraCoreApi 63 public static final class Match { 64 65 private final int matchLength; 66 private final @NonNull String tzId; 67 private final boolean isDst; 68 Match(int matchLength, @NonNull String tzId, boolean isDst)69 private Match(int matchLength, @NonNull String tzId, boolean isDst) { 70 this.matchLength = matchLength; 71 this.tzId = tzId; 72 this.isDst = isDst; 73 } 74 75 /** 76 * Returns the number of chars in the matched name. 77 * 78 * @hide 79 */ 80 @IntraCoreApi getMatchLength()81 public int getMatchLength() { 82 return matchLength; 83 } 84 85 /** 86 * Returns the time zone id associated with the matched name. 87 * 88 * @hide 89 */ 90 @IntraCoreApi getTzId()91 public @NonNull String getTzId() { 92 return tzId; 93 } 94 95 /** 96 * Returns true if the matched name is a display name for daylight saving time. For example, 97 * returns true for "Pacific Daylight Time", but false for "Pacific Standard Time". 98 * 99 * @hide 100 */ 101 @IntraCoreApi isDst()102 public boolean isDst() { 103 return isDst; 104 } 105 } 106 ExtendedTimeZoneNames(ULocale locale)107 private ExtendedTimeZoneNames(ULocale locale) { 108 this.locale = locale; 109 this.timeZoneNames = TimeZoneNames.getInstance(locale); 110 } 111 112 /** 113 * Returns an instance of {@link ExtendedTimeZoneNames}. 114 * 115 * @hide 116 */ 117 @IntraCoreApi getInstance(@onNull ULocale locale)118 public static @NonNull ExtendedTimeZoneNames getInstance(@NonNull ULocale locale) { 119 return new ExtendedTimeZoneNames(locale); 120 } 121 122 /** 123 * Returns the underlying {@link TimeZoneNames} instance. 124 * 125 * @hide 126 */ 127 @IntraCoreApi getTimeZoneNames()128 public @NonNull TimeZoneNames getTimeZoneNames() { 129 return timeZoneNames; 130 } 131 132 /** 133 * Returns {@link Match} if a time zone name in ICU can be matched against the input 134 * CharSequence {@code s}. 135 * The best match is found by the following principles: 136 * <ul> 137 * <li>Length of the matched name. Longest name matched to the given {@code s} has the 138 * highest priority.</li> 139 * <li>The current time zone and meta zones possible in the current country have higher 140 * priority than other zones.</li> 141 * <li>If only meta zones are matched, the country/region in the locale is used to select 142 * a reference time zone. For example, if the name is "Pacific Standard Time" and the country 143 * is US, America/Los_Angeles is returned.</li> 144 * </ul> 145 * 146 * @param text input string to be matched against time zone names in ICU 147 * @param start the begin index in the CharSequence {@code s} 148 * @param currentTzId the time zone ID prioritized to be matched if multiple time zone IDs can 149 * be matched and this is one of the matched IDs. 150 * @return null if no match is found 151 * 152 * @hide 153 */ 154 @IntraCoreApi matchName(@onNull CharSequence text, int start, @NonNull String currentTzId)155 public @Nullable Match matchName(@NonNull CharSequence text, int start, 156 @NonNull String currentTzId) { 157 currentTzId = TimeZone.getCanonicalID(currentTzId); 158 159 Collection<TimeZoneNames.MatchInfo> matchedInfos = 160 timeZoneNames.find(text, start, STANDARD_AND_DST_TYPES); 161 162 if (matchedInfos.isEmpty()) { 163 return null; 164 } 165 166 List<TimeZoneNames.MatchInfo> maxLengthMatchInfos = new ArrayList<>(); 167 int maxMatchedInfoLength = 0; 168 169 for (TimeZoneNames.MatchInfo matchInfo : matchedInfos) { 170 if (matchInfo.matchLength() > maxMatchedInfoLength) { 171 maxMatchedInfoLength = matchInfo.matchLength(); 172 maxLengthMatchInfos.clear(); 173 } 174 175 if (matchInfo.matchLength() >= maxMatchedInfoLength) { 176 maxLengthMatchInfos.add(matchInfo); 177 } 178 } 179 180 Set<String> metaZonesInCurrentZone = timeZoneNames.getAvailableMetaZoneIDs(currentTzId); 181 182 TimeZoneNames.MatchInfo tzMatchInfo = null; 183 184 for (TimeZoneNames.MatchInfo matchInfo : maxLengthMatchInfos) { 185 if (matchInfo.tzID() != null && matchInfo.tzID().equals(currentTzId)) { 186 return matchedTimeZone(matchInfo.tzID(), matchInfo); 187 } 188 if (matchInfo.mzID() != null && metaZonesInCurrentZone.contains(matchInfo.mzID())) { 189 return matchedTimeZone(currentTzId, matchInfo); 190 } 191 192 if (matchInfo.tzID() != null) { 193 tzMatchInfo = matchInfo; 194 } 195 } 196 197 if (tzMatchInfo != null) { 198 return matchedTimeZone(tzMatchInfo.tzID(), tzMatchInfo); 199 } 200 201 String region = locale.getCountry(); 202 if (region == null || region.isEmpty()) { 203 // An UN M49 code to represent the world. See TimeZoneNames#getReferenceZoneID(). 204 region = "001"; 205 } 206 207 for (TimeZoneNames.MatchInfo matchInfo : maxLengthMatchInfos) { 208 if (matchInfo.mzID() != null) { 209 String timeZoneId = timeZoneNames.getReferenceZoneID(matchInfo.mzID(), region); 210 if (timeZoneId != null) { 211 return matchedTimeZone(timeZoneId, matchInfo); 212 } 213 } 214 } 215 216 return null; 217 } 218 matchedTimeZone( String timeZoneId, TimeZoneNames.MatchInfo matchInfo)219 private static Match matchedTimeZone( 220 String timeZoneId, TimeZoneNames.MatchInfo matchInfo) { 221 return new Match( 222 matchInfo.matchLength(), 223 timeZoneId, 224 DST_NAME_TYPES.contains(matchInfo.nameType())); 225 226 } 227 228 } 229