summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2017-10-27 16:01:28 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2017-10-27 16:01:28 +0000
commitd2085949cd61c7252ff1ba15aa914f6decd1a56c (patch)
treed0af33300a3ced9515f5bf9f8bfd70c19ca449f3
parent58e96b621a6d730259db5c45b92dce0e6a3edf65 (diff)
parente041041b665cfdb4a13b420cf151102d144d9bf9 (diff)
Merge changes from topic "tz_catchup2"
* changes: Add lookupTimeZoneIdsByCountry Make use of the default time zone for a country
-rw-r--r--luni/src/main/java/libcore/util/TimeZoneFinder.java291
-rw-r--r--luni/src/test/java/libcore/icu/ICUTest.java56
-rw-r--r--luni/src/test/java/libcore/util/TimeZoneFinderTest.java218
3 files changed, 475 insertions, 90 deletions
diff --git a/luni/src/main/java/libcore/util/TimeZoneFinder.java b/luni/src/main/java/libcore/util/TimeZoneFinder.java
index babe5b2d23..fa15086823 100644
--- a/luni/src/main/java/libcore/util/TimeZoneFinder.java
+++ b/luni/src/main/java/libcore/util/TimeZoneFinder.java
@@ -36,6 +36,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
/**
@@ -49,15 +50,15 @@ public class TimeZoneFinder {
private static final String COUNTRY_ZONES_ELEMENT = "countryzones";
private static final String COUNTRY_ELEMENT = "country";
private static final String COUNTRY_CODE_ATTRIBUTE = "code";
+ private static final String DEFAULT_TIME_ZONE_ID_ATTRIBUTE = "default";
private static final String ID_ELEMENT = "id";
private static TimeZoneFinder instance;
private final ReaderSupplier xmlSource;
- // Cached fields for the last country looked up.
- private String lastCountryIso;
- private List<TimeZone> lastCountryTimeZones;
+ // Cached field for the last country looked up.
+ private CountryTimeZones lastCountryTimeZones;
private TimeZoneFinder(ReaderSupplier xmlSource) {
this.xmlSource = xmlSource;
@@ -153,9 +154,7 @@ public class TimeZoneFinder {
findRequiredStartTag(parser, TIMEZONES_ELEMENT);
return parser.getAttributeValue(null /* namespace */, IANA_VERSION_ATTRIBUTE);
- } catch (XmlPullParserException e) {
- return null;
- } catch (IOException e) {
+ } catch (XmlPullParserException | IOException e) {
return null;
}
}
@@ -173,14 +172,14 @@ public class TimeZoneFinder {
public TimeZone lookupTimeZoneByCountryAndOffset(
String countryIso, int offsetSeconds, boolean isDst, long whenMillis, TimeZone bias) {
+ countryIso = normalizeCountryIso(countryIso);
List<TimeZone> candidates = lookupTimeZonesByCountry(countryIso);
if (candidates == null || candidates.isEmpty()) {
return null;
}
TimeZone firstMatch = null;
- for (int i = 0; i < candidates.size(); i++) {
- TimeZone match = candidates.get(i);
+ for (TimeZone match : candidates) {
if (!offsetMatchesAtTime(match, offsetSeconds, isDst, whenMillis)) {
continue;
}
@@ -223,45 +222,88 @@ public class TimeZoneFinder {
}
/**
+ * Returns a "default" time zone ID known to be used in the specified country. This is
+ * the time zone ID that can be used if only the country code is known and can be presumed to be
+ * the "best" choice in the absence of other information. For countries with more than one zone
+ * the time zone will not be correct for everybody.
+ *
+ * <p>If the country code is not recognized or there is an error during lookup this can return
+ * null.
+ */
+ public String lookupDefaultTimeZoneIdByCountry(String countryIso) {
+ countryIso = normalizeCountryIso(countryIso);
+ CountryTimeZones countryTimeZones = findCountryTimeZones(countryIso);
+ return countryTimeZones == null ? null : countryTimeZones.getDefaultTimeZoneId();
+ }
+
+ /**
* Returns an immutable list of frozen ICU time zones known to be used in the specified country.
* If the country code is not recognized or there is an error during lookup this can return
* null. The TimeZones returned will never contain {@link TimeZone#UNKNOWN_ZONE}. This method
- * can return an empty list in a case when the underlying configuration references only unknown
+ * can return an empty list in a case when the underlying data files reference only unknown
* zone IDs.
*/
public List<TimeZone> lookupTimeZonesByCountry(String countryIso) {
- synchronized(this) {
- if (countryIso.equals(lastCountryIso)) {
+ countryIso = normalizeCountryIso(countryIso);
+ CountryTimeZones countryTimeZones = findCountryTimeZones(countryIso);
+ return countryTimeZones == null ? null : countryTimeZones.getTimeZones();
+ }
+
+ /**
+ * Returns an immutable list of time zone IDs known to be used in the specified country.
+ * If the country code is not recognized or there is an error during lookup this can return
+ * null. The IDs returned will all be valid for use with
+ * {@link java.util.TimeZone#getTimeZone(String)} and
+ * {@link android.icu.util.TimeZone#getTimeZone(String)}. This method can return an empty list
+ * in a case when the underlying data files reference only unknown zone IDs.
+ */
+ public List<String> lookupTimeZoneIdsByCountry(String countryIso) {
+ countryIso = normalizeCountryIso(countryIso);
+ CountryTimeZones countryTimeZones = findCountryTimeZones(countryIso);
+ return countryTimeZones == null ? null : countryTimeZones.getTimeZoneIds();
+ }
+
+ /**
+ * Returns a {@link CountryTimeZones} object associated with the specified country code.
+ * Caching is handled as needed. If the country code is not recognized or there is an error
+ * during lookup this can return null.
+ */
+ private CountryTimeZones findCountryTimeZones(String countryIso) {
+ synchronized (this) {
+ if (lastCountryTimeZones != null
+ && lastCountryTimeZones.getCountryIso().equals(countryIso)) {
return lastCountryTimeZones;
}
}
- CountryZonesExtractor extractor = new CountryZonesExtractor(countryIso);
- List<TimeZone> countryTimeZones = null;
+ SelectiveCountryTimeZonesExtractor extractor =
+ new SelectiveCountryTimeZonesExtractor(countryIso);
try {
processXml(extractor);
- countryTimeZones = extractor.getMatchedZones();
- } catch (IOException e) {
- System.logW("Error reading country zones ", e);
- // Clear the cached code so we will try again next time.
- countryIso = null;
- } catch (XmlPullParserException e) {
+ CountryTimeZones countryTimeZones = extractor.getValidatedCountryTimeZones();
+ if (countryTimeZones == null) {
+ // None matched. Return the null but don't change the cached value.
+ return null;
+ }
+
+ // Update the cached value.
+ synchronized (this) {
+ lastCountryTimeZones = countryTimeZones;
+ }
+ return countryTimeZones;
+ } catch (XmlPullParserException | IOException e) {
System.logW("Error reading country zones ", e);
- // We want to cache the null. This won't get better over time.
- }
- synchronized(this) {
- lastCountryIso = countryIso;
- lastCountryTimeZones = countryTimeZones;
+ // Error - don't change the cached value.
+ return null;
}
- return countryTimeZones;
}
/**
* Processes the XML, applying the {@link CountryZonesProcessor} to the &lt;countryzones&gt;
* element. Processing can terminate early if the
- * {@link CountryZonesProcessor#process(String, List, String)} returns
+ * {@link CountryZonesProcessor#process(String, String, List, String)} returns
* {@link CountryZonesProcessor#HALT} or it throws an exception.
*/
private void processXml(CountryZonesProcessor processor)
@@ -277,12 +319,12 @@ public class TimeZoneFinder {
* The expected XML structure is:
* <timezones ianaversion="2017b">
* <countryzones>
- * <country code="us">
+ * <country code="us" default="America/New_York">
* <id>America/New_York"</id>
* ...
* <id>America/Los_Angeles</id>
* </country>
- * <country code="gb">
+ * <country code="gb" default="Europe/London">
* <id>Europe/London</id>
* </country>
* </countryzones>
@@ -331,10 +373,16 @@ public class TimeZoneFinder {
throw new XmlPullParserException(
"Unable to find country code: " + parser.getPositionDescription());
}
+ String defaultTimeZoneId = parser.getAttributeValue(
+ null /* namespace */, DEFAULT_TIME_ZONE_ID_ATTRIBUTE);
+ if (defaultTimeZoneId == null || defaultTimeZoneId.isEmpty()) {
+ throw new XmlPullParserException("Unable to find default time zone ID: "
+ + parser.getPositionDescription());
+ }
String debugInfo = parser.getPositionDescription();
List<String> timeZoneIds = parseZoneIds(parser);
- if (processor.process(code, timeZoneIds, debugInfo)
+ if (processor.process(code, defaultTimeZoneId, timeZoneIds, debugInfo)
== CountryZonesProcessor.HALT) {
return CountryZonesProcessor.HALT;
}
@@ -512,79 +560,80 @@ public class TimeZoneFinder {
* should stop (but without considering this an error). Problems with parser are reported as
* an exception.
*/
- boolean process(String countryCode, List<String> timeZoneIds, String debugInfo)
- throws XmlPullParserException;
+ boolean process(String countryIso, String defaultTimeZoneId, List<String> timeZoneIds,
+ String debugInfo) throws XmlPullParserException;
}
/**
- * Validates &lt;countryzones&gt; elements. To be valid the country ISO code must be unique
- * and it must not be empty.
+ * Validates &lt;countryzones&gt; elements. Intended to be used before a proposed installation
+ * of new data. To be valid the country ISO code must be normalized, unique, the default time
+ * zone ID must be one of the time zones IDs and the time zone IDs list must not be empty. The
+ * IDs themselves are not checked against other data to see if they are recognized because other
+ * classes will not have been updated with the associated new time zone data yet and so will not
+ * be aware of newly added IDs.
*/
private static class CountryZonesValidator implements CountryZonesProcessor {
private final Set<String> knownCountryCodes = new HashSet<>();
@Override
- public boolean process(String countryCode, List<String> timeZoneIds, String debugInfo)
- throws XmlPullParserException {
- if (knownCountryCodes.contains(countryCode)) {
- throw new XmlPullParserException("Second entry for country code: " + countryCode
+ public boolean process(String countryIso, String defaultTimeZoneId,
+ List<String> timeZoneIds, String debugInfo) throws XmlPullParserException {
+ if (!normalizeCountryIso(countryIso).equals(countryIso)) {
+ throw new XmlPullParserException("Country code: " + countryIso
+ + " is not normalized at " + debugInfo);
+ }
+ if (knownCountryCodes.contains(countryIso)) {
+ throw new XmlPullParserException("Second entry for country code: " + countryIso
+ " at " + debugInfo);
}
if (timeZoneIds.isEmpty()) {
- throw new XmlPullParserException("No time zone IDs for country code: " + countryCode
+ throw new XmlPullParserException("No time zone IDs for country code: " + countryIso
+ " at " + debugInfo);
}
-
- // We don't validate the zone IDs - they may be new and we can't easily check them
- // against other timezone data that may be associated with this file.
-
- knownCountryCodes.add(countryCode);
+ if (!timeZoneIds.contains(defaultTimeZoneId)) {
+ throw new XmlPullParserException("defaultTimeZoneId for country code: "
+ + countryIso + " is not one of the zones " + timeZoneIds + " at "
+ + debugInfo);
+ }
+ knownCountryCodes.add(countryIso);
return CONTINUE;
}
}
/**
- * Extracts the zones associated with a country code, halting when the country code is matched
- * and making them available via {@link #getMatchedZones()}.
+ * Extracts <em>validated</em> time zones information associated with a specific country code.
+ * Processing is halted when the country code is matched and the validated result is also made
+ * available via {@link #getValidatedCountryTimeZones()}.
*/
- private static class CountryZonesExtractor implements CountryZonesProcessor {
+ private static class SelectiveCountryTimeZonesExtractor implements CountryZonesProcessor {
private final String countryCodeToMatch;
- private List<TimeZone> matchedZones;
+ private CountryTimeZones validatedCountryTimeZones;
- private CountryZonesExtractor(String countryCodeToMatch) {
+ private SelectiveCountryTimeZonesExtractor(String countryCodeToMatch) {
this.countryCodeToMatch = countryCodeToMatch;
}
@Override
- public boolean process(String countryCode, List<String> timeZoneIds, String debugInfo) {
- if (!countryCodeToMatch.equals(countryCode)) {
+ public boolean process(String countryIso, String defaultTimeZoneId,
+ List<String> countryTimeZoneIds, String debugInfo) {
+ countryIso = normalizeCountryIso(countryIso);
+ if (!countryCodeToMatch.equals(countryIso)) {
return CONTINUE;
}
+ validatedCountryTimeZones = createValidatedCountryTimeZones(countryIso,
+ defaultTimeZoneId, countryTimeZoneIds, debugInfo);
- List<TimeZone> timeZones = new ArrayList<>();
- for (String zoneIdString : timeZoneIds) {
- TimeZone tz = TimeZone.getTimeZone(zoneIdString);
- if (tz.getID().equals(TimeZone.UNKNOWN_ZONE_ID)) {
- System.logW("Skipping invalid zone: " + zoneIdString + " at " + debugInfo);
- } else {
- // The zone is frozen to prevent mutation by callers.
- timeZones.add(tz.freeze());
- }
- }
- matchedZones = Collections.unmodifiableList(timeZones);
return HALT;
}
/**
- * Returns the matched zones, or {@code null} if there were no matches. Unknown zone IDs are
- * ignored so the list can be empty if there were no zones or the zone IDs were not
- * recognized.
+ * Returns the CountryTimeZones that matched, or {@code null} if there were no matches.
*/
- List<TimeZone> getMatchedZones() {
- return matchedZones;
+ CountryTimeZones getValidatedCountryTimeZones() {
+ return validatedCountryTimeZones;
}
}
@@ -610,4 +659,114 @@ public class TimeZoneFinder {
return () -> new StringReader(xml);
}
}
+
+ /**
+ * Information about a country's time zones.
+ */
+ // VisibleForTesting
+ public static class CountryTimeZones {
+ private final String countryIso;
+ private final String defaultTimeZoneId;
+ private final List<String> timeZoneIds;
+
+ // Memoized frozen ICU TimeZone objects for the timeZoneIds.
+ private List<TimeZone> timeZones;
+
+ public CountryTimeZones(String countryIso, String defaultTimeZoneId,
+ List<String> timeZoneIds) {
+ this.countryIso = countryIso;
+ this.defaultTimeZoneId = defaultTimeZoneId;
+ // Create a defensive copy of the IDs list.
+ this.timeZoneIds = Collections.unmodifiableList(new ArrayList<>(timeZoneIds));
+ }
+
+ public String getCountryIso() {
+ return countryIso;
+ }
+
+ /**
+ * Returns the default time zone ID for a country. Can return null in extreme cases when
+ * invalid data is found.
+ */
+ public String getDefaultTimeZoneId() {
+ return defaultTimeZoneId;
+ }
+
+ /**
+ * Returns an ordered list of time zone IDs for a country in an undefined but "priority"
+ * order for a country. The list can be empty if there were no zones configured or the
+ * configured zone IDs were not recognized.
+ */
+ public List<String> getTimeZoneIds() {
+ return timeZoneIds;
+ }
+
+ /**
+ * Returns an ordered list of time zones for a country in an undefined but "priority"
+ * order for a country. The list can be empty if there were no zones configured or the
+ * configured zone IDs were not recognized.
+ */
+ public synchronized List<TimeZone> getTimeZones() {
+ if (timeZones == null) {
+ ArrayList<TimeZone> mutableList = new ArrayList<>(timeZoneIds.size());
+ for (String timeZoneId : timeZoneIds) {
+ TimeZone timeZone = getValidFrozenTimeZoneOrNull(timeZoneId);
+ // This shouldn't happen given the validation that takes place in
+ // createValidatedCountryTimeZones().
+ if (timeZone == null) {
+ System.logW("Skipping invalid zone: " + timeZoneId);
+ continue;
+ }
+ mutableList.add(timeZone);
+ }
+ timeZones = Collections.unmodifiableList(mutableList);
+ }
+ return timeZones;
+ }
+
+ private static TimeZone getValidFrozenTimeZoneOrNull(String timeZoneId) {
+ TimeZone timeZone = TimeZone.getFrozenTimeZone(timeZoneId);
+ if (timeZone.getID().equals(TimeZone.UNKNOWN_ZONE_ID)) {
+ return null;
+ }
+ return timeZone;
+ }
+ }
+
+ private static String normalizeCountryIso(String countryIso) {
+ // Lowercase ASCII is normalized for the purposes of the input files and the code in this
+ // class.
+ return countryIso.toLowerCase(Locale.US);
+ }
+
+ // VisibleForTesting
+ public static CountryTimeZones createValidatedCountryTimeZones(String countryIso,
+ String defaultTimeZoneId, List<String> countryTimeZoneIds, String debugInfo) {
+
+ // We rely on ZoneInfoDB to tell us what the known valid time zone IDs are. ICU may
+ // recognize more but we want to be sure that zone IDs can be used with java.util as well as
+ // android.icu and ICU is expected to have a superset.
+ String[] validTimeZoneIdsArray = ZoneInfoDB.getInstance().getAvailableIDs();
+ HashSet<String> validTimeZoneIdsSet = new HashSet<>(Arrays.asList(validTimeZoneIdsArray));
+ List<String> validCountryTimeZoneIds = new ArrayList<>();
+ for (String countryTimeZoneId : countryTimeZoneIds) {
+ if (!validTimeZoneIdsSet.contains(countryTimeZoneId)) {
+ System.logW("Skipping invalid zone: " + countryTimeZoneId + " at " + debugInfo);
+ } else {
+ validCountryTimeZoneIds.add(countryTimeZoneId);
+ }
+ }
+
+ // We don't get too strict at runtime about whether the defaultTimeZoneId must be
+ // one of the country's time zones because this is the data we have to use (we also
+ // assume the data was validated by earlier steps). The default time zone ID must just
+ // be a recognized zone ID: if it's not valid we leave it null.
+ if (!validTimeZoneIdsSet.contains(defaultTimeZoneId)) {
+ System.logW("Invalid default time zone ID: " + defaultTimeZoneId
+ + " at " + debugInfo);
+ defaultTimeZoneId = null;
+ }
+
+ return new CountryTimeZones(countryIso, defaultTimeZoneId, validCountryTimeZoneIds);
+ }
}
diff --git a/luni/src/test/java/libcore/icu/ICUTest.java b/luni/src/test/java/libcore/icu/ICUTest.java
index 0409f0cd40..a8e4d591e2 100644
--- a/luni/src/test/java/libcore/icu/ICUTest.java
+++ b/luni/src/test/java/libcore/icu/ICUTest.java
@@ -16,10 +16,15 @@
package libcore.icu;
+import android.icu.util.TimeZone;
+
import java.text.BreakIterator;
import java.text.Collator;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
+import java.util.Set;
import libcore.util.TimeZoneFinder;
import libcore.util.ZoneInfoDB;
@@ -269,4 +274,55 @@ public class ICUTest extends junit.framework.TestCase {
String tzLookupTzVersion = TimeZoneFinder.getInstance().getIanaVersion();
assertEquals(icu4jTzVersion, tzLookupTzVersion);
}
+
+ /**
+ * Confirms that ICU can recognize all the time zone IDs used by the ZoneInfoDB data.
+ * ICU's IDs may be a superset.
+ */
+ public void testTimeZoneIdLookup() {
+ String[] zoneInfoDbAvailableIds = ZoneInfoDB.getInstance().getAvailableIDs();
+
+ // ICU has a known set of IDs. We want ANY because we don't want to filter to ICU's canonical
+ // IDs only.
+ Set<String> icuAvailableIds = android.icu.util.TimeZone.getAvailableIDs(
+ TimeZone.SystemTimeZoneType.ANY, null /* region */, null /* rawOffset */);
+
+ List<String> nonIcuAvailableIds = new ArrayList<>();
+ List<String> creationFailureIds = new ArrayList<>();
+ List<String> noCanonicalLookupIds = new ArrayList<>();
+ List<String> nonSystemIds = new ArrayList<>();
+ for (String zoneInfoDbId : zoneInfoDbAvailableIds) {
+
+ if (zoneInfoDbId.equals("Asia/Hanoi")) {
+ // Known ICU lookup issue: http://b/30277331
+ continue;
+ }
+
+ if (!icuAvailableIds.contains(zoneInfoDbId)) {
+ nonIcuAvailableIds.add(zoneInfoDbId);
+ }
+
+ boolean[] isSystemId = new boolean[1];
+ String canonicalId = android.icu.util.TimeZone.getCanonicalID(zoneInfoDbId, isSystemId);
+ if (canonicalId == null) {
+ noCanonicalLookupIds.add(zoneInfoDbId);
+ }
+ if (!isSystemId[0]) {
+ nonSystemIds.add(zoneInfoDbId);
+ }
+
+ android.icu.util.TimeZone icuTimeZone = android.icu.util.TimeZone.getTimeZone(zoneInfoDbId);
+ if (icuTimeZone.getID().equals(TimeZone.UNKNOWN_ZONE_ID)) {
+ creationFailureIds.add(zoneInfoDbId);
+ }
+ }
+ assertTrue("Non-ICU available IDs: " + nonIcuAvailableIds
+ + ", creation failed IDs: " + creationFailureIds
+ + ", non-system IDs: " + nonSystemIds
+ + ", ids without canonical IDs: " + noCanonicalLookupIds,
+ nonIcuAvailableIds.isEmpty()
+ && creationFailureIds.isEmpty()
+ && nonSystemIds.isEmpty()
+ && noCanonicalLookupIds.isEmpty());
+ }
}
diff --git a/luni/src/test/java/libcore/util/TimeZoneFinderTest.java b/luni/src/test/java/libcore/util/TimeZoneFinderTest.java
index dc93a80ebe..f675150fcd 100644
--- a/luni/src/test/java/libcore/util/TimeZoneFinderTest.java
+++ b/luni/src/test/java/libcore/util/TimeZoneFinderTest.java
@@ -30,6 +30,8 @@ import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -39,6 +41,7 @@ import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class TimeZoneFinderTest {
@@ -122,17 +125,20 @@ public class TimeZoneFinderTest {
TimeZoneFinder file1ThenFile2 =
TimeZoneFinder.createInstanceWithFallback(validFile1, validFile2);
assertEquals("2017c", file1ThenFile2.getIanaVersion());
+ assertEquals(list("Europe/London"), file1ThenFile2.lookupTimeZoneIdsByCountry("gb"));
assertZonesEqual(zones("Europe/London"), file1ThenFile2.lookupTimeZonesByCountry("gb"));
TimeZoneFinder missingFileThenFile1 =
TimeZoneFinder.createInstanceWithFallback(missingFile, validFile1);
assertEquals("2017c", missingFileThenFile1.getIanaVersion());
+ assertEquals(list("Europe/London"), missingFileThenFile1.lookupTimeZoneIdsByCountry("gb"));
assertZonesEqual(zones("Europe/London"),
missingFileThenFile1.lookupTimeZonesByCountry("gb"));
TimeZoneFinder file2ThenFile1 =
TimeZoneFinder.createInstanceWithFallback(validFile2, validFile1);
assertEquals("2017b", file2ThenFile1.getIanaVersion());
+ assertEquals(list("Europe/Paris"), file2ThenFile1.lookupTimeZoneIdsByCountry("gb"));
assertZonesEqual(zones("Europe/Paris"), file2ThenFile1.lookupTimeZonesByCountry("gb"));
// We assume the file has been validated so an invalid file is not checked ahead of time.
@@ -140,12 +146,14 @@ public class TimeZoneFinderTest {
TimeZoneFinder invalidThenValid =
TimeZoneFinder.createInstanceWithFallback(invalidFile, validFile1);
assertNull(invalidThenValid.getIanaVersion());
+ assertNull(invalidThenValid.lookupTimeZoneIdsByCountry("gb"));
assertNull(invalidThenValid.lookupTimeZonesByCountry("gb"));
// This is not a normal case: It would imply a define shipped without a file in /system!
TimeZoneFinder missingFiles =
TimeZoneFinder.createInstanceWithFallback(missingFile, missingFile);
assertNull(missingFiles.getIanaVersion());
+ assertNull(missingFiles.lookupTimeZoneIdsByCountry("gb"));
assertNull(missingFiles.lookupTimeZonesByCountry("gb"));
}
@@ -182,7 +190,7 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
// This is a crazy comment, but also helps prove that TEXT nodes are coalesced by the
// parser.
@@ -193,7 +201,7 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
}
@Test
@@ -207,7 +215,8 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -217,7 +226,8 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -227,7 +237,8 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -238,8 +249,9 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London", "Europe/Paris"),
- finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London", "Europe/Paris"),
+ finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -249,7 +261,8 @@ public class TimeZoneFinderTest {
+ " " + unexpectedElement
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
// This test is important because it ensures we can extend the format in future with
// more information.
@@ -261,7 +274,8 @@ public class TimeZoneFinderTest {
+ " </countryzones>\n"
+ " " + unexpectedElement
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
}
@Test
@@ -275,7 +289,8 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -285,7 +300,8 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -295,7 +311,8 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -306,8 +323,9 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London", "Europe/Paris"),
- finder.lookupTimeZonesByCountry("gb"));
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ assertEquals(list("Europe/London", "Europe/Paris"),
+ finder.lookupTimeZoneIdsByCountry("gb"));
}
@Test
@@ -361,6 +379,7 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
}
@@ -368,7 +387,18 @@ public class TimeZoneFinderTest {
public void xmlParsing_missingCountryCode() throws Exception {
checkValidateThrowsParserException("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
- + " <country>\n"
+ + " <country default=\"Europe/London\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ }
+
+ @Test
+ public void xmlParsing_missingDefault() throws Exception {
+ checkValidateThrowsParserException("<timezones ianaversion=\"2017b\">\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ " <id>Europe/London</id>\n"
+ " </country>\n"
+ " </countryzones>\n"
@@ -381,6 +411,7 @@ public class TimeZoneFinderTest {
+ " <countryzones>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
+ assertNull(finder.lookupTimeZoneIdsByCountry("gb"));
assertNull(finder.lookupTimeZonesByCountry("gb"));
}
@@ -399,10 +430,87 @@ public class TimeZoneFinderTest {
assertImmutableList(gbList);
assertImmutableTimeZone(gbList.get(0));
+ // Check country code normalization works too.
+ assertEquals(1, finder.lookupTimeZonesByCountry("GB").size());
+
assertNull(finder.lookupTimeZonesByCountry("unknown"));
}
@Test
+ public void lookupTimeZoneIdsByCountry_structuresAreImmutable() throws Exception {
+ TimeZoneFinder finder = validate("<timezones ianaversion=\"2017b\">\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\" default=\"Europe/London\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ List<String> gbList = finder.lookupTimeZoneIdsByCountry("gb");
+ assertEquals(1, gbList.size());
+ assertImmutableList(gbList);
+
+ // Check country code normalization works too.
+ assertEquals(1, finder.lookupTimeZoneIdsByCountry("GB").size());
+
+ assertNull(finder.lookupTimeZoneIdsByCountry("unknown"));
+ }
+
+ @Test
+ public void lookupDefaultTimeZoneIdByCountry() throws Exception {
+ TimeZoneFinder finder = validate("<timezones ianaversion=\"2017b\">\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\" default=\"Europe/London\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+
+ // Check country code normalization works too.
+ assertEquals("Europe/London", finder.lookupDefaultTimeZoneIdByCountry("GB"));
+ }
+
+ /**
+ * At runtime we don't validate too much since there's nothing we can do if the data is
+ * incorrect.
+ */
+ @Test
+ public void lookupDefaultTimeZoneIdByCountry_notCountryTimeZoneButValid() throws Exception {
+ String xml = "<timezones ianaversion=\"2017b\">\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\" default=\"America/New_York\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n";
+ // validate() should fail because America/New_York is not one of the "gb" zones listed.
+ checkValidateThrowsParserException(xml);
+
+ // But it should still work at runtime.
+ TimeZoneFinder finder = TimeZoneFinder.createInstanceForTests(xml);
+ assertEquals("America/New_York", finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ }
+
+ @Test
+ public void lookupDefaultTimeZoneIdByCountry_invalidDefault() throws Exception {
+ String xml = "<timezones ianaversion=\"2017b\">\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\" default=\"Moon/Tranquility_Base\">\n"
+ + " <id>Europe/London</id>\n"
+ + " <id>Moon/Tranquility_Base</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n";
+ // validate() should pass because the IDs all match.
+ TimeZoneFinder finder = validate(xml);
+
+ // But "Moon/Tranquility_Base" is not a valid time zone ID so should not be used.
+ assertNull(finder.lookupDefaultTimeZoneIdByCountry("gb"));
+ }
+
+ @Test
public void lookupTimeZoneByCountryAndOffset_unknownCountry() throws Exception {
TimeZoneFinder finder = validate("<timezones ianaversion=\"2017b\">\n"
+ " <countryzones>\n"
@@ -417,6 +525,11 @@ public class TimeZoneFinderTest {
finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
true /* isDst */, WHEN_DST, null /* bias */));
+ // Check country code normalization works too.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("XX", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, null /* bias */));
+
// Test with an unknown country.
String unknownCountryCode = "yy";
assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode,
@@ -664,15 +777,16 @@ public class TimeZoneFinderTest {
// Android uses lower case, IANA uses upper.
countryCode = countryCode.toLowerCase();
- List<String> ianaZoneIds = countryEntry.getValue().stream().sorted()
- .collect(Collectors.toList());
+ List<String> androidZoneIds = timeZoneFinder.lookupTimeZoneIdsByCountry(countryCode);
List<TimeZone> androidZones = timeZoneFinder.lookupTimeZonesByCountry(countryCode);
- List<String> androidZoneIds =
- androidZones.stream().map(TimeZone::getID).sorted()
- .collect(Collectors.toList());
+ List<String> androidIdsFromZones =
+ androidZones.stream().map(TimeZone::getID).collect(Collectors.toList());
+ assertEquals("lookupTimeZonesByCountry and lookupTimeZoneIdsByCountry differ for "
+ + countryCode, androidZoneIds, androidIdsFromZones);
+ Collection<String> ianaZoneIds = countryEntry.getValue();
assertEquals("Android zones for " + countryCode + " do not match IANA data",
- ianaZoneIds, androidZoneIds);
+ sort(ianaZoneIds), sort(androidZoneIds));
}
}
@@ -687,7 +801,7 @@ public class TimeZoneFinderTest {
+ " </country>\n"
+ " </countryzones>\n"
+ "</timezones>\n");
- assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ assertEquals(list("Europe/London"), finder.lookupTimeZoneIdsByCountry("gb"));
assertNull(finder.getIanaVersion());
}
@@ -703,6 +817,53 @@ public class TimeZoneFinderTest {
assertEquals(expectedIanaVersion, finder.getIanaVersion());
}
+ @Test
+ public void createValidatedCountryTimeZones_filtersBadIds() throws Exception {
+ String countryIso = "iso";
+ String knownTimeZoneId1 = "Europe/London";
+ String knownTimeZoneId2 = "America/Los_Angeles";
+ String knownTimeZoneId3 = "America/New_York";
+ String unknownTimeZoneId = "Moon/Tranquility_Base";
+
+ List<String> countryZoneIds = list(
+ knownTimeZoneId1, knownTimeZoneId2, unknownTimeZoneId, knownTimeZoneId3);
+ TimeZoneFinder.CountryTimeZones countryTimeZones =
+ TimeZoneFinder.createValidatedCountryTimeZones(countryIso, knownTimeZoneId1,
+ countryZoneIds, "debugInfoIgnored");
+
+ assertEquals(countryIso, countryTimeZones.getCountryIso());
+
+ assertEquals(knownTimeZoneId1, countryTimeZones.getDefaultTimeZoneId());
+ assertEquals(knownTimeZoneId1, countryTimeZones.getDefaultTimeZoneId());
+
+ // Validation should have filtered the unknown ID.
+ String[] expectedTimeZoneIds = { knownTimeZoneId1, knownTimeZoneId2, knownTimeZoneId3 };
+ assertEquals(list(expectedTimeZoneIds), countryTimeZones.getTimeZoneIds());
+ List<TimeZone> timeZones = countryTimeZones.getTimeZones();
+ for (int i = 0; i < timeZones.size(); i++) {
+ TimeZone timeZone = timeZones.get(i);
+ assertEquals(expectedTimeZoneIds[i], timeZone.getID());
+ assertTrue(timeZone.isFrozen());
+ }
+ }
+
+ @Test
+ public void createValidatedCountryTimeZones_filtersBadDefaultId() throws Exception {
+ String countryIso = "iso";
+ String unknownTimeZoneId = "Moon/Tranquility_Base";
+
+ List<String> countryZoneIds = list(unknownTimeZoneId);
+ TimeZoneFinder.CountryTimeZones countryTimeZones =
+ TimeZoneFinder.createValidatedCountryTimeZones(countryIso, unknownTimeZoneId,
+ countryZoneIds, "debugInfoIgnored");
+
+ assertEquals(countryIso, countryTimeZones.getCountryIso());
+
+ assertNull(countryTimeZones.getDefaultTimeZoneId());
+ assertEquals(Collections.emptyList(), countryTimeZones.getTimeZoneIds());
+ assertEquals(Collections.emptyList(), countryTimeZones.getTimeZones());
+ }
+
private void assertImmutableTimeZone(TimeZone timeZone) {
try {
timeZone.setRawOffset(1000);
@@ -711,9 +872,9 @@ public class TimeZoneFinderTest {
}
}
- private static void assertImmutableList(List<TimeZone> timeZones) {
+ private static <X> void assertImmutableList(List<X> list) {
try {
- timeZones.add(null);
+ list.add(null);
fail();
} catch (UnsupportedOperationException expected) {
}
@@ -743,6 +904,15 @@ public class TimeZoneFinderTest {
return timeZoneFinder;
}
+ private static <X> List<X> list(X... values) {
+ return Arrays.asList(values);
+ }
+
+ private static <X> List<X> sort(Collection<X> value) {
+ return value.stream().sorted()
+ .collect(Collectors.toList());
+ }
+
private static List<TimeZone> zones(String... ids) {
return Arrays.stream(ids).map(TimeZone::getTimeZone).collect(Collectors.toList());
}