| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.tzdata.mts; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.icu.text.TimeZoneNames; |
| |
| import org.junit.Test; |
| |
| import java.time.ZoneId; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.TimeZone; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Tests relating to time zone rules that could be changed by the time zone data module. These are |
| * intended to prove that a time zone data module update hasn't broken behavior. Since time zone |
| * rule mutate over time this test could be quite brittle, so it is suggested that only a few |
| * examples are tested. |
| */ |
| public class TimeZoneRulesTest { |
| |
| @Test |
| public void preHistoricInDaylightTime() { |
| // A zone that lacks an explicit transition at Integer.MIN_VALUE with zic 2019a and 2019a |
| // data. |
| TimeZone tz = TimeZone.getTimeZone("CET"); |
| |
| long firstTransitionTimeMillis = -1693706400000L; // Apr 30, 1916 22:00:00 GMT |
| assertEquals(7200000L, tz.getOffset(firstTransitionTimeMillis)); |
| assertTrue(tz.inDaylightTime(new Date(firstTransitionTimeMillis))); |
| |
| long beforeFirstTransitionTimeMillis = firstTransitionTimeMillis - 1; |
| assertEquals(3600000L, tz.getOffset(beforeFirstTransitionTimeMillis)); |
| assertFalse(tz.inDaylightTime(new Date(beforeFirstTransitionTimeMillis))); |
| } |
| |
| @Test |
| public void getDisplayNameShort_nonHourOffsets() { |
| TimeZone iranTz = TimeZone.getTimeZone("Asia/Tehran"); |
| assertEquals("GMT+03:30", iranTz.getDisplayName(false, TimeZone.SHORT, Locale.UK)); |
| assertEquals("GMT+04:30", iranTz.getDisplayName(true, TimeZone.SHORT, Locale.UK)); |
| } |
| |
| @Test |
| public void minimalTransitionZones() throws Exception { |
| // Zones with minimal transitions, historical or future, seem ideal for testing. |
| // UTC is also included, although it may be implemented differently from the others. |
| String[] ids = new String[] { "Africa/Bujumbura", "Indian/Cocos", "Pacific/Wake", "UTC" }; |
| for (String id : ids) { |
| TimeZone tz = TimeZone.getTimeZone(id); |
| assertFalse(tz.useDaylightTime()); |
| assertFalse(tz.inDaylightTime(new Date(Integer.MIN_VALUE))); |
| assertFalse(tz.inDaylightTime(new Date(0))); |
| assertFalse(tz.inDaylightTime(new Date(Integer.MAX_VALUE))); |
| int currentOffset = tz.getOffset(new Date(0).getTime()); |
| assertEquals(currentOffset, tz.getOffset(new Date(Integer.MIN_VALUE).getTime())); |
| assertEquals(currentOffset, tz.getOffset(new Date(Integer.MAX_VALUE).getTime())); |
| } |
| } |
| |
| @Test |
| public void getDSTSavings() throws Exception { |
| assertEquals(0, TimeZone.getTimeZone("UTC").getDSTSavings()); |
| assertEquals(3600000, TimeZone.getTimeZone("America/Los_Angeles").getDSTSavings()); |
| assertEquals(1800000, TimeZone.getTimeZone("Australia/Lord_Howe").getDSTSavings()); |
| } |
| |
| // http://b/7955614 and http://b/8026776. |
| @Test |
| public void displayNames() throws Exception { |
| checkDisplayNames(Locale.US); |
| } |
| |
| @Test |
| public void displayNames_nonUS() throws Exception { |
| // run checkDisplayNames with an arbitrary set of Locales. |
| checkDisplayNames(Locale.CHINESE); |
| checkDisplayNames(Locale.FRENCH); |
| checkDisplayNames(Locale.forLanguageTag("bn-BD")); |
| } |
| |
| private static void checkDisplayNames(Locale locale) throws Exception { |
| // Check that there are no time zones that use DST but have the same display name for |
| // both standard and daylight time. |
| StringBuilder failures = new StringBuilder(); |
| for (String id : TimeZone.getAvailableIDs()) { |
| TimeZone tz = TimeZone.getTimeZone(id); |
| String longDst = tz.getDisplayName(true, TimeZone.LONG, locale); |
| String longStd = tz.getDisplayName(false, TimeZone.LONG, locale); |
| String shortDst = tz.getDisplayName(true, TimeZone.SHORT, locale); |
| String shortStd = tz.getDisplayName(false, TimeZone.SHORT, locale); |
| |
| if (tz.useDaylightTime()) { |
| // The long std and dst strings must differ! |
| if (longDst.equals(longStd)) { |
| failures.append(String.format("\n%20s: LD='%s' LS='%s'!", |
| id, longDst, longStd)); |
| } |
| // The short std and dst strings must differ! |
| if (shortDst.equals(shortStd)) { |
| failures.append(String.format("\n%20s: SD='%s' SS='%s'!", |
| id, shortDst, shortStd)); |
| } |
| |
| // If the short std matches the long dst, or the long std matches the short dst, |
| // it probably means we have a time zone that icu4c doesn't believe has ever |
| // observed dst. |
| if (shortStd.equals(longDst)) { |
| failures.append(String.format("\n%20s: SS='%s' LD='%s'!", |
| id, shortStd, longDst)); |
| } |
| if (longStd.equals(shortDst)) { |
| failures.append(String.format("\n%20s: LS='%s' SD='%s'!", |
| id, longStd, shortDst)); |
| } |
| |
| // The long and short dst strings must differ! |
| if (longDst.equals(shortDst) && !longDst.startsWith("GMT")) { |
| failures.append(String.format("\n%20s: LD='%s' SD='%s'!", |
| id, longDst, shortDst)); |
| } |
| } |
| |
| // Confidence check that whenever a display name is just a GMT string that it's the |
| // right GMT string. |
| String gmtDst = formatGmtString(tz, true); |
| String gmtStd = formatGmtString(tz, false); |
| if (isGmtString(longDst) && !longDst.equals(gmtDst)) { |
| failures.append(String.format("\n%s: LD %s", id, longDst)); |
| } |
| if (isGmtString(longStd) && !longStd.equals(gmtStd)) { |
| failures.append(String.format("\n%s: LS %s", id, longStd)); |
| } |
| if (isGmtString(shortDst) && !shortDst.equals(gmtDst)) { |
| failures.append(String.format("\n%s: SD %s", id, shortDst)); |
| } |
| if (isGmtString(shortStd) && !shortStd.equals(gmtStd)) { |
| failures.append(String.format("\n%s: SS %s", id, shortStd)); |
| } |
| } |
| assertEquals("", failures.toString()); |
| } |
| |
| private static boolean isGmtString(String s) { |
| return s.startsWith("GMT+") || s.startsWith("GMT-"); |
| } |
| |
| private static String formatGmtString(TimeZone tz, boolean daylight) { |
| int offset = tz.getRawOffset(); |
| if (daylight) { |
| offset += tz.getDSTSavings(); |
| } |
| offset /= 60000; |
| char sign = '+'; |
| if (offset < 0) { |
| sign = '-'; |
| offset = -offset; |
| } |
| return String.format("GMT%c%02d:%02d", sign, offset / 60, offset % 60); |
| } |
| |
| /** |
| * This test is to catch issues with the rules update process that could let the |
| * "negative DST" scheme enter the Android data set for either java.util.TimeZone or |
| * android.icu.util.TimeZone. |
| */ |
| @Test |
| public void dstMeansSummer() { |
| // Ireland was the original example that caused the default IANA upstream tzdata to contain |
| // a zone where DST is in the Winter (since tzdata 2018e, though it was tried in 2018a |
| // first). This change was made to historical and future transitions. |
| // |
| // The upstream reasoning went like this: "Irish *Standard* Time" is summer, so the other |
| // time must be the DST. So, DST is considered to be in the winter and the associated DST |
| // adjustment is negative from the standard time. In the old scheme "Irish Standard Time" / |
| // summer was just modeled as the DST in common with all other global time zones. |
| // |
| // Unfortunately, various users of formatting APIs assume standard and DST times are |
| // consistent and (effectively) that "DST" means "summer". We likely cannot adopt the |
| // concept of a winter DST without risking app compat issues. |
| // |
| // For example, getDisplayName(boolean daylight) has always returned the winter time for |
| // false, and the summer time for true. If we change this then it should be changed on a |
| // major release boundary, with improved APIs (e.g. a version of getDisplayName() that takes |
| // a millis), existing API behavior made dependent on target API version, and after fixing |
| // any platform code that makes incorrect assumptions about DST meaning "1 hour forward". |
| |
| final String timeZoneId = "Europe/Dublin"; |
| final Locale locale = Locale.UK; |
| // 26 Oct 2015 01:00:00 GMT - one day after the start of "Greenwich Mean Time" in |
| // Europe/Dublin in 2015. An arbitrary historical example of winter in Ireland. |
| final long winterTimeMillis = 1445821200000L; |
| final String winterTimeName = "Greenwich Mean Time"; |
| final int winterOffsetRawMillis = 0; |
| final int winterOffsetDstMillis = 0; |
| |
| // 30 Mar 2015 01:00:00 GMT - one day after the start of "Irish Standard Time" in |
| // Europe/Dublin in 2015. An arbitrary historical example of summer in Ireland. |
| final long summerTimeMillis = 1427677200000L; |
| final String summerTimeName = "Irish Standard Time"; |
| final int summerOffsetRawMillis = 0; |
| final int summerOffsetDstMillis = (int) TimeUnit.HOURS.toMillis(1); |
| |
| // There is no common interface between java.util.TimeZone and android.icu.util.TimeZone |
| // so the tests are for each are effectively duplicated. |
| |
| // java.util.TimeZone |
| { |
| java.util.TimeZone timeZone = java.util.TimeZone.getTimeZone(timeZoneId); |
| assertTrue(timeZone.useDaylightTime()); |
| |
| assertFalse(timeZone.inDaylightTime(new Date(winterTimeMillis))); |
| assertTrue(timeZone.inDaylightTime(new Date(summerTimeMillis))); |
| |
| assertEquals(winterOffsetRawMillis + winterOffsetDstMillis, |
| timeZone.getOffset(winterTimeMillis)); |
| assertEquals(summerOffsetRawMillis + summerOffsetDstMillis, |
| timeZone.getOffset(summerTimeMillis)); |
| assertEquals(winterTimeName, |
| timeZone.getDisplayName(false /* daylight */, java.util.TimeZone.LONG, |
| locale)); |
| assertEquals(summerTimeName, |
| timeZone.getDisplayName(true /* daylight */, java.util.TimeZone.LONG, |
| locale)); |
| } |
| |
| // android.icu.util.TimeZone |
| { |
| android.icu.util.TimeZone timeZone = android.icu.util.TimeZone.getTimeZone(timeZoneId); |
| assertTrue(timeZone.useDaylightTime()); |
| |
| assertFalse(timeZone.inDaylightTime(new Date(winterTimeMillis))); |
| assertTrue(timeZone.inDaylightTime(new Date(summerTimeMillis))); |
| |
| assertEquals(winterOffsetRawMillis + winterOffsetDstMillis, |
| timeZone.getOffset(winterTimeMillis)); |
| assertEquals(summerOffsetRawMillis + summerOffsetDstMillis, |
| timeZone.getOffset(summerTimeMillis)); |
| |
| // These methods show the trouble we'd have if callers were to take the output from |
| // inDaylightTime() and pass it to getDisplayName(). |
| assertEquals(winterTimeName, |
| timeZone.getDisplayName(false /* daylight */, android.icu.util.TimeZone.LONG, |
| locale)); |
| assertEquals(summerTimeName, |
| timeZone.getDisplayName(true /* daylight */, android.icu.util.TimeZone.LONG, |
| locale)); |
| |
| // APIs not identical to java.util.TimeZone tested below. |
| int[] offsets = new int[2]; |
| timeZone.getOffset(winterTimeMillis, false /* local */, offsets); |
| assertEquals(winterOffsetRawMillis, offsets[0]); |
| assertEquals(winterOffsetDstMillis, offsets[1]); |
| |
| timeZone.getOffset(summerTimeMillis, false /* local */, offsets); |
| assertEquals(summerOffsetRawMillis, offsets[0]); |
| assertEquals(summerOffsetDstMillis, offsets[1]); |
| } |
| |
| // icu TimeZoneNames |
| TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale); |
| // getDisplayName: date = winterTimeMillis |
| assertEquals(winterTimeName, timeZoneNames.getDisplayName( |
| timeZoneId, TimeZoneNames.NameType.LONG_STANDARD, winterTimeMillis)); |
| assertEquals(summerTimeName, timeZoneNames.getDisplayName( |
| timeZoneId, TimeZoneNames.NameType.LONG_DAYLIGHT, winterTimeMillis)); |
| // getDisplayName: date = summerTimeMillis |
| assertEquals(winterTimeName, timeZoneNames.getDisplayName( |
| timeZoneId, TimeZoneNames.NameType.LONG_STANDARD, summerTimeMillis)); |
| assertEquals(summerTimeName, timeZoneNames.getDisplayName( |
| timeZoneId, TimeZoneNames.NameType.LONG_DAYLIGHT, summerTimeMillis)); |
| } |
| |
| /** |
| * ICU's time zone IDs may be a superset of IDs available via other APIs. |
| */ |
| @Test |
| public void timeZoneIdsKnown() { |
| // java.util |
| List<String> zoneInfoDbAvailableIds = Arrays.asList(java.util.TimeZone.getAvailableIDs()); |
| checkZoneIdsAreKnownToIcu(zoneInfoDbAvailableIds); |
| |
| // java.time |
| checkZoneIdsAreKnownToIcu(ZoneId.getAvailableZoneIds()); |
| } |
| |
| private static void checkZoneIdsAreKnownToIcu(Collection<String> zoneInfoDbAvailableIds) { |
| // 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( |
| android.icu.util.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 (!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(android.icu.util.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()); |
| } |
| } |