blob: 77dada4fa1ba0b2f6664280228b582e391a08c47 [file] [log] [blame]
/*
********************************************************************************
* Copyright (C) 2007-2015, Google, International Business Machines Corporation *
* and others. All Rights Reserved. *
********************************************************************************
*/
package com.ibm.icu.dev.test.format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import com.ibm.icu.impl.TZDBTimeZoneNames;
import com.ibm.icu.impl.ZoneMeta;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.text.TimeZoneFormat;
import com.ibm.icu.text.TimeZoneFormat.ParseOption;
import com.ibm.icu.text.TimeZoneFormat.Style;
import com.ibm.icu.text.TimeZoneFormat.TimeType;
import com.ibm.icu.text.TimeZoneNames;
import com.ibm.icu.text.TimeZoneNames.NameType;
import com.ibm.icu.util.BasicTimeZone;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
import com.ibm.icu.util.TimeZoneTransition;
import com.ibm.icu.util.ULocale;
public class TimeZoneFormatTest extends com.ibm.icu.dev.test.TestFmwk {
private static boolean JDKTZ = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK);
private static final Pattern EXCL_TZ_PATTERN = Pattern.compile(".*/Riyadh8[7-9]");
public static void main(String[] args) throws Exception {
new TimeZoneFormatTest().run(args);
}
private static final String[] PATTERNS = {
"z",
"zzzz",
"Z", // equivalent to "xxxx"
"ZZZZ", // equivalent to "OOOO"
"v",
"vvvv",
"O",
"OOOO",
"X",
"XX",
"XXX",
"XXXX",
"XXXXX",
"x",
"xx",
"xxx",
"xxxx",
"xxxxx",
"V",
"VV",
"VVV",
"VVVV"
};
boolean REALLY_VERBOSE_LOG = false;
/*
* Test case for checking if a TimeZone is properly set in the result calendar
* and if the result TimeZone has the expected behavior.
*/
public void TestTimeZoneRoundTrip() {
boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false);
TimeZone unknownZone = new SimpleTimeZone(-31415, "Etc/Unknown");
int badDstOffset = -1234;
int badZoneOffset = -2345;
int[][] testDateData = {
{2007, 1, 15},
{2007, 6, 15},
{1990, 1, 15},
{1990, 6, 15},
{1960, 1, 15},
{1960, 6, 15},
};
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.clear();
// Set up rule equivalency test range
long low, high;
cal.set(1900, 0, 1);
low = cal.getTimeInMillis();
cal.set(2040, 0, 1);
high = cal.getTimeInMillis();
// Set up test dates
Date[] DATES = new Date[testDateData.length];
cal.clear();
for (int i = 0; i < DATES.length; i++) {
cal.set(testDateData[i][0], testDateData[i][1], testDateData[i][2]);
DATES[i] = cal.getTime();
}
// Set up test locales
ULocale[] LOCALES = null;
if (TEST_ALL || getInclusion() > 5) {
LOCALES = ULocale.getAvailableLocales();
} else {
LOCALES = new ULocale[] {new ULocale("en"), new ULocale("en_CA"), new ULocale("fr"), new ULocale("zh_Hant")};
}
String[] tzids;
if (JDKTZ) {
tzids = java.util.TimeZone.getAvailableIDs();
} else {
tzids = TimeZone.getAvailableIDs();
}
int[] inOffsets = new int[2];
int[] outOffsets = new int[2];
// Run the roundtrip test
for (int locidx = 0; locidx < LOCALES.length; locidx++) {
logln("Locale: " + LOCALES[locidx].toString());
String localGMTString = TimeZoneFormat.getInstance(LOCALES[locidx]).formatOffsetLocalizedGMT(0);
for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
logln(" pattern: " + PATTERNS[patidx]);
SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]);
for (int tzidx = 0; tzidx < tzids.length; tzidx++) {
if (EXCL_TZ_PATTERN.matcher(tzids[tzidx]).matches()) {
continue;
}
TimeZone tz = TimeZone.getTimeZone(tzids[tzidx]);
for (int datidx = 0; datidx < DATES.length; datidx++) {
// Format
sdf.setTimeZone(tz);
String tzstr = sdf.format(DATES[datidx]);
// Before parse, set unknown zone to SimpleDateFormat instance
// just for making sure that it does not depends on the time zone
// originally set.
sdf.setTimeZone(unknownZone);
// Parse
ParsePosition pos = new ParsePosition(0);
Calendar outcal = Calendar.getInstance(unknownZone);
outcal.set(Calendar.DST_OFFSET, badDstOffset);
outcal.set(Calendar.ZONE_OFFSET, badZoneOffset);
sdf.parse(tzstr, outcal, pos);
// Check the result
TimeZone outtz = outcal.getTimeZone();
tz.getOffset(DATES[datidx].getTime(), false, inOffsets);
outtz.getOffset(DATES[datidx].getTime(), false, outOffsets);
if (PATTERNS[patidx].equals("V")) {
// Short zone ID - should support roundtrip for canonical CLDR IDs
String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
if (!outtz.getID().equals(canonicalID)) {
if (outtz.getID().equals("Etc/Unknown")) {
// Note that some zones like Asia/Riyadh87 does not have
// short zone ID and "unk" is used as the fallback
if (REALLY_VERBOSE_LOG) {
logln("Canonical round trip failed (probably as expected); tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", outtz=" + outtz.getID());
}
} else {
errln("Canonical round trip failed; tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", outtz=" + outtz.getID());
}
}
} else if (PATTERNS[patidx].equals("VV")) {
// Zone ID - full roundtrip support
if (!outtz.getID().equals(tzids[tzidx])) {
errln("Zone ID round trip failed; tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", outtz=" + outtz.getID());
}
} else if (PATTERNS[patidx].equals("VVV") || PATTERNS[patidx].equals("VVVV")) {
// Location: time zone rule must be preserved except
// zones not actually associated with a specific location.
String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
if (canonicalID != null && !outtz.getID().equals(canonicalID)) {
// Canonical ID did not match - check the rules
boolean bFailure = false;
if ((tz instanceof BasicTimeZone) && (outtz instanceof BasicTimeZone)) {
boolean hasNoLocation = TimeZone.getRegion(tzids[tzidx]).equals("001");
bFailure = !hasNoLocation
&& !((BasicTimeZone)outtz).hasEquivalentTransitions(tz, low, high);
}
if (bFailure) {
errln("Canonical round trip failed; tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", outtz=" + outtz.getID());
} else if (REALLY_VERBOSE_LOG) {
logln("Canonical round trip failed (as expected); tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", outtz=" + outtz.getID());
}
}
} else {
boolean isOffsetFormat = (PATTERNS[patidx].charAt(0) == 'Z'
|| PATTERNS[patidx].charAt(0) == 'O'
|| PATTERNS[patidx].charAt(0) == 'X'
|| PATTERNS[patidx].charAt(0) == 'x');
boolean minutesOffset = false;
if (PATTERNS[patidx].charAt(0) == 'X' || PATTERNS[patidx].charAt(0) == 'x') {
minutesOffset = PATTERNS[patidx].length() <= 3;
}
if (!isOffsetFormat) {
// Check if localized GMT format is used as a fallback of name styles
int numDigits = 0;
for (int n = 0; n < tzstr.length(); n++) {
if (UCharacter.isDigit(tzstr.charAt(n))) {
numDigits++;
}
}
isOffsetFormat = (numDigits > 0);
}
if (isOffsetFormat || tzstr.equals(localGMTString)) {
// Localized GMT or ISO: total offset (raw + dst) must be preserved.
int inOffset = inOffsets[0] + inOffsets[1];
int outOffset = outOffsets[0] + outOffsets[1];
int diff = outOffset - inOffset;
if (minutesOffset) {
diff = (diff / 60000) * 60000;
}
if (diff != 0) {
errln("Offset round trip failed; tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", inOffset=" + inOffset + ", outOffset=" + outOffset);
}
} else {
// Specific or generic: raw offset must be preserved.
if (inOffsets[0] != outOffsets[0]) {
if (JDKTZ && tzids[tzidx].startsWith("SystemV/")) {
// JDK uses rule SystemV for these zones while
// ICU handles these zones as aliases of existing time zones
if (REALLY_VERBOSE_LOG) {
logln("Raw offset round trip failed; tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
}
} else {
errln("Raw offset round trip failed; tz=" + tzids[tzidx]
+ ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
+ ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
+ ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
}
}
}
}
}
}
}
}
}
/*
* Test case of round trip time and text. This test case detects every canonical TimeZone's
* rule transition since 1900 until 2020, then check if time around each transition can
* round trip as expected.
*/
public void TestTimeRoundTrip() {
boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false);
int startYear, endYear;
if (TEST_ALL || getInclusion() > 5) {
startYear = 1900;
} else {
startYear = 1990;
}
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
endYear = cal.get(Calendar.YEAR) + 3;
cal.set(startYear, Calendar.JANUARY, 1);
final long START_TIME = cal.getTimeInMillis();
cal.set(endYear, Calendar.JANUARY, 1);
final long END_TIME = cal.getTimeInMillis();
// These patterns are ambiguous at DST->STD local time overlap
List<String> AMBIGUOUS_DST_DECESSION = Arrays.asList("v", "vvvv", "V", "VV", "VVV", "VVVV");
// These patterns are ambiguous at STD->STD/DST->DST local time overlap
List<String> AMBIGUOUS_NEGATIVE_SHIFT = Arrays.asList("z", "zzzz", "v", "vvvv", "V", "VV", "VVV", "VVVV");
// These patterns only support integer minutes offset
List<String> MINUTES_OFFSET = Arrays.asList("X", "XX", "XXX", "x", "xx", "xxx");
// Regex pattern used for filtering zone IDs without exemplar location
final Pattern LOC_EXCLUSION_PATTERN = Pattern.compile("Etc/.*|SystemV/.*|.*/Riyadh8[7-9]");
final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
ULocale[] LOCALES = null;
// timer for performance analysis
long[] times = new long[PATTERNS.length];
long timer;
if (TEST_ALL) {
// It may take about an hour for testing all locales
LOCALES = ULocale.getAvailableLocales();
} else if (getInclusion() > 5) {
LOCALES = new ULocale[] {
new ULocale("ar_EG"), new ULocale("bg_BG"), new ULocale("ca_ES"), new ULocale("da_DK"), new ULocale("de"),
new ULocale("de_DE"), new ULocale("el_GR"), new ULocale("en"), new ULocale("en_AU"), new ULocale("en_CA"),
new ULocale("en_US"), new ULocale("es"), new ULocale("es_ES"), new ULocale("es_MX"), new ULocale("fi_FI"),
new ULocale("fr"), new ULocale("fr_CA"), new ULocale("fr_FR"), new ULocale("he_IL"), new ULocale("hu_HU"),
new ULocale("it"), new ULocale("it_IT"), new ULocale("ja"), new ULocale("ja_JP"), new ULocale("ko"),
new ULocale("ko_KR"), new ULocale("nb_NO"), new ULocale("nl_NL"), new ULocale("nn_NO"), new ULocale("pl_PL"),
new ULocale("pt"), new ULocale("pt_BR"), new ULocale("pt_PT"), new ULocale("ru_RU"), new ULocale("sv_SE"),
new ULocale("th_TH"), new ULocale("tr_TR"), new ULocale("zh"), new ULocale("zh_Hans"), new ULocale("zh_Hans_CN"),
new ULocale("zh_Hant"), new ULocale("zh_Hant_HK"), new ULocale("zh_Hant_TW")
};
} else {
LOCALES = new ULocale[] {
new ULocale("en"),
};
}
SimpleDateFormat sdfGMT = new SimpleDateFormat(BASEPATTERN);
sdfGMT.setTimeZone(TimeZone.getTimeZone("Etc/GMT"));
long testCounts = 0;
long[] testTimes = new long[4];
boolean[] expectedRoundTrip = new boolean[4];
int testLen = 0;
for (int locidx = 0; locidx < LOCALES.length; locidx++) {
logln("Locale: " + LOCALES[locidx].toString());
for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
logln(" pattern: " + PATTERNS[patidx]);
String pattern = BASEPATTERN + " " + PATTERNS[patidx];
SimpleDateFormat sdf = new SimpleDateFormat(pattern, LOCALES[locidx]);
boolean minutesOffset = MINUTES_OFFSET.contains(PATTERNS[patidx]);
Set<String> ids = null;
if (JDKTZ) {
ids = new TreeSet<String>();
String[] jdkIDs = java.util.TimeZone.getAvailableIDs();
for (String jdkID : jdkIDs) {
if (EXCL_TZ_PATTERN.matcher(jdkID).matches()) {
continue;
}
String tmpID = TimeZone.getCanonicalID(jdkID);
if (tmpID != null) {
ids.add(tmpID);
}
}
} else {
ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);
}
for (String id : ids) {
if (PATTERNS[patidx].equals("V")) {
// Some zones do not have short ID assigned, such as Asia/Riyadh87.
// The time roundtrip will fail for such zones with pattern "V" (short zone ID).
// This is expected behavior.
String shortZoneID = ZoneMeta.getShortID(id);
if (shortZoneID == null) {
continue;
}
} else if (PATTERNS[patidx].equals("VVV")) {
// Some zones are not associated with any region, such as Etc/GMT+8.
// The time roundtrip will fail for such zones with pattern "VVV" (exemplar location).
// This is expected behavior.
if (id.indexOf('/') < 0 || LOC_EXCLUSION_PATTERN.matcher(id).matches()) {
continue;
}
}
if (id.equals("Pacific/Apia") && PATTERNS[patidx].equals("vvvv")
&& logKnownIssue("11052", "Ambiguous zone name - Samoa Time")) {
continue;
}
BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(id, TimeZone.TIMEZONE_ICU);
TimeZone tz = TimeZone.getTimeZone(id);
sdf.setTimeZone(tz);
long t = START_TIME;
TimeZoneTransition tzt = null;
boolean middle = true;
while (t < END_TIME) {
if (tzt == null) {
testTimes[0] = t;
expectedRoundTrip[0] = true;
testLen = 1;
} else {
int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
int delta = toOffset - fromOffset;
if (delta < 0) {
boolean isDstDecession = tzt.getFrom().getDSTSavings() > 0 && tzt.getTo().getDSTSavings() == 0;
testTimes[0] = t + delta - 1;
expectedRoundTrip[0] = true;
testTimes[1] = t + delta;
expectedRoundTrip[1] = isDstDecession ?
!AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) :
!AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]);
testTimes[2] = t - 1;
expectedRoundTrip[2] = isDstDecession ?
!AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) :
!AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]);
testTimes[3] = t;
expectedRoundTrip[3] = true;
testLen = 4;
} else {
testTimes[0] = t - 1;
expectedRoundTrip[0] = true;
testTimes[1] = t;
expectedRoundTrip[1] = true;
testLen = 2;
}
}
for (int testidx = 0; testidx < testLen; testidx++) {
testCounts++;
timer = System.currentTimeMillis();
String text = sdf.format(new Date(testTimes[testidx]));
try {
Date parsedDate = sdf.parse(text);
long restime = parsedDate.getTime();
long timeDiff = restime - testTimes[testidx];
boolean bTimeMatch = minutesOffset ?
(timeDiff/60000)*60000 == 0 : timeDiff == 0;
if (!bTimeMatch) {
StringBuffer msg = new StringBuffer();
msg.append("Time round trip failed for ")
.append("tzid=").append(id)
.append(", locale=").append(LOCALES[locidx])
.append(", pattern=").append(PATTERNS[patidx])
.append(", text=").append(text)
.append(", gmt=").append(sdfGMT.format(new Date(testTimes[testidx])))
.append(", time=").append(testTimes[testidx])
.append(", restime=").append(restime)
.append(", diff=").append(timeDiff);
if (expectedRoundTrip[testidx]
&& !isSpecialTimeRoundTripCase(LOCALES[locidx], id, PATTERNS[patidx], testTimes[testidx])) {
errln("FAIL: " + msg.toString());
} else if (REALLY_VERBOSE_LOG) {
logln(msg.toString());
}
}
} catch (ParseException pe) {
errln("FAIL: " + pe.getMessage() + " tzid=" + id + ", locale=" + LOCALES[locidx] +
", pattern=" + PATTERNS[patidx] + ", text=" + text);
}
times[patidx] += System.currentTimeMillis() - timer;
}
tzt = btz.getNextTransition(t, false);
if (tzt == null) {
break;
}
if (middle) {
// Test the date in the middle of two transitions.
t += (tzt.getTime() - t)/2;
middle = false;
tzt = null;
} else {
t = tzt.getTime();
}
}
}
}
}
long total = 0;
logln("### Elapsed time by patterns ###");
for (int i = 0; i < PATTERNS.length; i++) {
logln(times[i] + "ms (" + PATTERNS[i] + ")");
total += times[i];
}
logln("Total: " + total + "ms");
logln("Iteration: " + testCounts);
}
// Special exclusions in TestTimeZoneRoundTrip.
// These special cases do not round trip time as designed.
private boolean isSpecialTimeRoundTripCase(ULocale loc, String id, String pattern, long time) {
final Object[][] EXCLUSIONS = {
{null, "Asia/Chita", "zzzz", Long.valueOf(1414252800000L)},
{null, "Asia/Chita", "vvvv", Long.valueOf(1414252800000L)},
{null, "Asia/Srednekolymsk", "zzzz", Long.valueOf(1414241999999L)},
{null, "Asia/Srednekolymsk", "vvvv", Long.valueOf(1414241999999L)},
};
boolean isExcluded = false;
for (Object[] excl : EXCLUSIONS) {
if (excl[0] == null || loc.equals((ULocale)excl[0])) {
if (id.equals(excl[1])) {
if (excl[2] == null || pattern.equals((String)excl[2])) {
if (excl[3] == null || ((Long)excl[3]).compareTo(time) == 0) {
isExcluded = true;
break;
}
}
}
}
}
return isExcluded;
}
public void TestParse() {
final Object[][] DATA = {
// text inpos locale style
// parseOptions expected outpos time type
{"Z", 0, "en_US", Style.ISO_EXTENDED_FULL,
null, "Etc/GMT", 1, TimeType.UNKNOWN},
{"Z", 0, "en_US", Style.SPECIFIC_LONG,
null, "Etc/GMT", 1, TimeType.UNKNOWN},
{"Zambia time", 0, "en_US", Style.ISO_EXTENDED_FULL,
EnumSet.of(ParseOption.ALL_STYLES), "Etc/GMT", 1, TimeType.UNKNOWN},
{"Zambia time", 0, "en_US", Style.GENERIC_LOCATION,
null, "Africa/Lusaka", 11, TimeType.UNKNOWN},
{"Zambia time", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL,
EnumSet.of(ParseOption.ALL_STYLES), "Africa/Lusaka", 11, TimeType.UNKNOWN},
{"+00:00", 0, "en_US", Style.ISO_EXTENDED_FULL,
null, "Etc/GMT", 6, TimeType.UNKNOWN},
{"-01:30:45", 0, "en_US", Style.ISO_EXTENDED_FULL,
null, "GMT-01:30:45", 9, TimeType.UNKNOWN},
{"-7", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL,
null, "GMT-07:00", 2, TimeType.UNKNOWN},
{"-2222", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL,
null, "GMT-22:22", 5, TimeType.UNKNOWN},
{"-3333", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL,
null, "GMT-03:33", 4, TimeType.UNKNOWN},
{"XXX+01:30YYY", 3, "en_US", Style.LOCALIZED_GMT,
null, "GMT+01:30", 9, TimeType.UNKNOWN},
{"GMT0", 0, "en_US", Style.SPECIFIC_SHORT,
null, "Etc/GMT", 3, TimeType.UNKNOWN},
{"EST", 0, "en_US", Style.SPECIFIC_SHORT,
null, "America/New_York", 3, TimeType.STANDARD},
{"ESTx", 0, "en_US", Style.SPECIFIC_SHORT,
null, "America/New_York", 3, TimeType.STANDARD},
{"EDTx", 0, "en_US", Style.SPECIFIC_SHORT,
null, "America/New_York", 3, TimeType.DAYLIGHT},
{"EST", 0, "en_US", Style.SPECIFIC_LONG,
null, null, 0, TimeType.UNKNOWN},
{"EST", 0, "en_US", Style.SPECIFIC_LONG,
EnumSet.of(ParseOption.ALL_STYLES), "America/New_York", 3, TimeType.STANDARD},
{"EST", 0, "en_CA", Style.SPECIFIC_SHORT,
null, "America/Toronto", 3, TimeType.STANDARD},
{"CST", 0, "en_US", Style.SPECIFIC_SHORT,
null, "America/Chicago", 3, TimeType.STANDARD},
{"CST", 0, "en_GB", Style.SPECIFIC_SHORT,
null, null, 0, TimeType.UNKNOWN},
{"CST", 0, "en_GB", Style.SPECIFIC_SHORT,
EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "America/Chicago", 3, TimeType.STANDARD},
{"--CST--", 2, "en_GB", Style.SPECIFIC_SHORT,
EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "America/Chicago", 5, TimeType.STANDARD},
{"CST", 0, "zh_CN", Style.SPECIFIC_SHORT,
EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Shanghai", 3, TimeType.STANDARD},
{"AEST", 0, "en_AU", Style.SPECIFIC_SHORT,
EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Australia/Sydney", 4, TimeType.STANDARD},
{"AST", 0, "ar_SA", Style.SPECIFIC_SHORT,
EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Riyadh", 3, TimeType.STANDARD},
{"AQTST", 0, "en", Style.SPECIFIC_LONG,
null, null, 0, TimeType.UNKNOWN},
{"AQTST", 0, "en", Style.SPECIFIC_LONG,
EnumSet.of(ParseOption.ALL_STYLES), null, 0, TimeType.UNKNOWN},
{"AQTST", 0, "en", Style.SPECIFIC_LONG,
EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Aqtobe", 5, TimeType.DAYLIGHT},
};
for (Object[] test : DATA) {
String text = (String)test[0];
int inPos = (Integer)test[1];
ULocale loc = new ULocale((String)test[2]);
Style style = (Style)test[3];
EnumSet<ParseOption> options = (EnumSet<ParseOption>)test[4];
String expID = (String)test[5];
int expPos = (Integer)test[6];
TimeType expType = (TimeType)test[7];
TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc);
Output<TimeType> timeType = new Output<TimeType>(TimeType.UNKNOWN);
ParsePosition pos = new ParsePosition(inPos);
TimeZone tz = tzfmt.parse(style, text, pos, options, timeType);
String errMsg = null;
if (tz == null) {
if (expID != null) {
errMsg = "Parse failure - expected: " + expID;
}
} else if (!tz.getID().equals(expID)) {
errMsg = "Time zone ID: " + tz.getID() + " - expected: " + expID;
} else if (pos.getIndex() != expPos) {
errMsg = "Parsed pos: " + pos.getIndex() + " - expected: " + expPos;
} else if (timeType.value != expType) {
errMsg = "Time type: " + timeType + " - expected: " + expType;
}
if (errMsg != null) {
errln("Fail: " + errMsg +
" [text=" + text + ", pos=" + inPos +
", locale=" + loc + ", style=" + style + "]");
}
}
}
public void TestISOFormat() {
final int[] OFFSET = {
0, // 0
999, // 0.999s
-59999, // -59.999s
60000, // 1m
-77777, // -1m 17.777s
1800000, // 30m
-3600000, // -1h
36000000, // 10h
-37800000, // -10h 30m
-37845000, // -10h 30m 45s
108000000, // 30h
};
final String[][] ISO_STR = {
// 0
{
"Z", "Z", "Z", "Z", "Z",
"+00", "+0000", "+00:00", "+0000", "+00:00",
"+0000"
},
// 999
{
"Z", "Z", "Z", "Z", "Z",
"+00", "+0000", "+00:00", "+0000", "+00:00",
"+0000"
},
// -59999
{
"Z", "Z", "Z", "-000059", "-00:00:59",
"+00", "+0000", "+00:00", "-000059", "-00:00:59",
"-000059"
},
// 60000
{
"+0001", "+0001", "+00:01", "+0001", "+00:01",
"+0001", "+0001", "+00:01", "+0001", "+00:01",
"+0001"
},
// -77777
{
"-0001", "-0001", "-00:01", "-000117", "-00:01:17",
"-0001", "-0001", "-00:01", "-000117", "-00:01:17",
"-000117"
},
// 1800000
{
"+0030", "+0030", "+00:30", "+0030", "+00:30",
"+0030", "+0030", "+00:30", "+0030", "+00:30",
"+0030"
},
// -3600000
{
"-01", "-0100", "-01:00", "-0100", "-01:00",
"-01", "-0100", "-01:00", "-0100", "-01:00",
"-0100"
},
// 36000000
{
"+10", "+1000", "+10:00", "+1000", "+10:00",
"+10", "+1000", "+10:00", "+1000", "+10:00",
"+1000"
},
// -37800000
{
"-1030", "-1030", "-10:30", "-1030", "-10:30",
"-1030", "-1030", "-10:30", "-1030", "-10:30",
"-1030"
},
// -37845000
{
"-1030", "-1030", "-10:30", "-103045", "-10:30:45",
"-1030", "-1030", "-10:30", "-103045", "-10:30:45",
"-103045"
},
// 108000000
{
null, null, null, null, null,
null, null, null, null, null,
null
}
};
final String[] PATTERN = {
"X", "XX", "XXX", "XXXX", "XXXXX", "x", "xx", "xxx", "xxxx", "xxxxx",
"Z", // equivalent to "xxxx"
};
final int[] MIN_OFFSET_UNIT = {
60000, 60000, 60000, 1000, 1000, 60000, 60000, 60000, 1000, 1000,
1000,
};
// Formatting
SimpleDateFormat sdf = new SimpleDateFormat();
Date d = new Date();
for (int i = 0; i < OFFSET.length; i++) {
SimpleTimeZone tz = new SimpleTimeZone(OFFSET[i], "Zone Offset:" + String.valueOf(OFFSET[i]) + "ms");
sdf.setTimeZone(tz);
for (int j = 0; j < PATTERN.length; j++) {
sdf.applyPattern(PATTERN[j]);
try {
String result = sdf.format(d);
if (!result.equals(ISO_STR[i][j])) {
errln("FAIL: pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] + " -> "
+ result + " (expected: " + ISO_STR[i][j] + ")");
}
} catch (IllegalArgumentException e) {
if (ISO_STR[i][j] != null) {
errln("FAIL: IAE thrown for pattern=" + PATTERN[j] + ", offset=" + OFFSET[i]
+ " (expected: " + ISO_STR[i][j] + ")");
}
}
}
}
// Parsing
SimpleTimeZone bogusTZ = new SimpleTimeZone(-1, "Zone Offset: -1ms");
for (int i = 0; i < ISO_STR.length; i++) {
for (int j = 0; j < ISO_STR[i].length; j++) {
if (ISO_STR[i][j] == null) {
continue;
}
ParsePosition pos = new ParsePosition(0);
Calendar outcal = Calendar.getInstance(bogusTZ);
sdf.applyPattern(PATTERN[j]);
sdf.parse(ISO_STR[i][j], outcal, pos);
if (pos.getIndex() != ISO_STR[i][j].length()) {
errln("FAIL: Failed to parse the entire input string: " + ISO_STR[i][j]);
continue;
}
TimeZone outtz = outcal.getTimeZone();
int outOffset = outtz.getRawOffset();
int adjustedOffset = OFFSET[i] / MIN_OFFSET_UNIT[j] * MIN_OFFSET_UNIT[j];
if (outOffset != adjustedOffset) {
errln("FAIL: Incorrect offset:" + outOffset + "ms for input string: " + ISO_STR[i][j]
+ " (expected:" + adjustedOffset + "ms)");
}
}
}
}
public void TestFormat() {
final Date dateJan = new Date(1358208000000L); // 2013-01-15T00:00:00Z
final Date dateJul = new Date(1373846400000L); // 2013-07-15T00:00:00Z
final Object[][] TESTDATA = {
{
"en",
"America/Los_Angeles",
dateJan,
Style.GENERIC_LOCATION,
"Los Angeles Time",
TimeType.UNKNOWN
},
{
"en",
"America/Los_Angeles",
dateJan,
Style.GENERIC_LONG,
"Pacific Time",
TimeType.UNKNOWN
},
{
"en",
"America/Los_Angeles",
dateJan,
Style.SPECIFIC_LONG,
"Pacific Standard Time",
TimeType.STANDARD
},
{
"en",
"America/Los_Angeles",
dateJul,
Style.SPECIFIC_LONG,
"Pacific Daylight Time",
TimeType.DAYLIGHT
},
{
"ja",
"America/Los_Angeles",
dateJan,
Style.ZONE_ID,
"America/Los_Angeles",
TimeType.UNKNOWN
},
{
"fr",
"America/Los_Angeles",
dateJul,
Style.ZONE_ID_SHORT,
"uslax",
TimeType.UNKNOWN
},
{
"en",
"America/Los_Angeles",
dateJan,
Style.EXEMPLAR_LOCATION,
"Los Angeles",
TimeType.UNKNOWN
},
{
"ja",
"Asia/Tokyo",
dateJan,
Style.GENERIC_LONG,
"\u65E5\u672C\u6A19\u6E96\u6642", // "日本標準時"
TimeType.UNKNOWN
},
};
for (Object[] testCase : TESTDATA) {
TimeZone tz = TimeZone.getTimeZone((String)testCase[1]);
Output<TimeType> timeType = new Output<TimeType>();
ULocale uloc = new ULocale((String)testCase[0]);
TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(uloc);
String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) {
errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
+ ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
+ "]; actual [output=" + out + ",type=" + timeType.value + "]");
}
// with equivalent Java Locale
Locale loc = uloc.toLocale();
tzfmt = TimeZoneFormat.getInstance(loc);
out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) {
errln("Format result for [locale(Java)=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
+ ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
+ "]; actual [output=" + out + ",type=" + timeType.value + "]");
}
}
}
public void TestFormatTZDBNames() {
final Date dateJan = new Date(1358208000000L); // 2013-01-15T00:00:00Z
final Date dateJul = new Date(1373846400000L); // 2013-07-15T00:00:00Z
final Object[][] TESTDATA = {
{
"en",
"America/Chicago",
dateJan,
Style.SPECIFIC_SHORT,
"CST",
TimeType.STANDARD
},
{
"en",
"Asia/Shanghai",
dateJan,
Style.SPECIFIC_SHORT,
"CST",
TimeType.STANDARD
},
{
"zh_Hans",
"Asia/Shanghai",
dateJan,
Style.SPECIFIC_SHORT,
"CST",
TimeType.STANDARD
},
{
"en",
"America/Los_Angeles",
dateJul,
Style.SPECIFIC_LONG,
"GMT-07:00", // No long display names
TimeType.DAYLIGHT
},
{
"ja",
"America/Los_Angeles",
dateJul,
Style.SPECIFIC_SHORT,
"PDT",
TimeType.DAYLIGHT
},
{
"en",
"Australia/Sydney",
dateJan,
Style.SPECIFIC_SHORT,
"AEDT",
TimeType.DAYLIGHT
},
{
"en",
"Australia/Sydney",
dateJul,
Style.SPECIFIC_SHORT,
"AEST",
TimeType.STANDARD
},
};
for (Object[] testCase : TESTDATA) {
ULocale loc = new ULocale((String)testCase[0]);
TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc).cloneAsThawed();
TimeZoneNames tzdbNames = TimeZoneNames.getTZDBInstance(loc);
tzfmt.setTimeZoneNames(tzdbNames);
TimeZone tz = TimeZone.getTimeZone((String)testCase[1]);
Output<TimeType> timeType = new Output<TimeType>();
String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) {
errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
+ ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
+ "]; actual [output=" + out + ",type=" + timeType.value + "]");
}
}
}
// This is a test case of Ticket#11487.
// Because the problem is reproduced for the very first time,
// the reported problem cannot be reproduced with regular test
// execution. Run this test alone reproduced the problem before
// the fix was merged.
public void TestTZDBNamesThreading() {
final TZDBTimeZoneNames names = new TZDBTimeZoneNames(ULocale.ENGLISH);
final AtomicInteger found = new AtomicInteger();
List<Thread> threads = new ArrayList<Thread>();
final int numIteration = 1000;
try {
for (int i = 0; i < numIteration; i++) {
Thread thread = new Thread() {
@Override
public void run() {
int resultSize = names.find("GMT", 0, EnumSet.allOf(NameType.class)).size();
if (resultSize > 0) {
found.incrementAndGet();
}
}
};
thread.start();
threads.add(thread);
}
for(Thread thread: threads) {
thread.join();
}
} catch (Throwable t) {
errln(t.toString());
}
if (found.intValue() != numIteration) {
errln("Incorrect count: " + found.toString() + ", expected: " + numIteration);
}
}
}