1/*
2 * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24/*
25 * @test
26 * @library /java/text/testlib
27 * @summary test Time Zone Boundary
28 */
29
30import java.text.*;
31import java.util.*;
32
33/**
34 * A test which discovers the boundaries of DST programmatically and verifies
35 * that they are correct.
36 */
37public class TimeZoneBoundaryTest extends IntlTest
38{
39    static final int ONE_SECOND = 1000;
40    static final int ONE_MINUTE = 60*ONE_SECOND;
41    static final int ONE_HOUR = 60*ONE_MINUTE;
42    static final long ONE_DAY = 24*ONE_HOUR;
43    static final long ONE_YEAR = (long)(365.25 * ONE_DAY);
44    static final long SIX_MONTHS = ONE_YEAR / 2;
45
46    static final int MONTH_LENGTH[] = {31,29,31,30,31,30,31,31,30,31,30,31};
47
48    // These values are empirically determined to be correct
49    static final long PST_1997_BEG  = 860320800000L;
50    static final long PST_1997_END  = 877856400000L;
51
52    // Minimum interval for binary searches in ms; should be no larger
53    // than 1000.
54    static final long INTERVAL = 10; // Milliseconds
55
56    static final String AUSTRALIA = "Australia/Adelaide";
57    static final long AUSTRALIA_1997_BEG = 877797000000L;
58    static final long AUSTRALIA_1997_END = 859653000000L;
59
60    public static void main(String[] args) throws Exception {
61        new TimeZoneBoundaryTest().run(args);
62    }
63
64    /**
65     * Date.toString().substring() Boundary Test
66     * Look for a DST changeover to occur within 6 months of the given Date.
67     * The initial Date.toString() should yield a string containing the
68     * startMode as a SUBSTRING.  The boundary will be tested to be
69     * at the expectedBoundary value.
70     */
71    void findDaylightBoundaryUsingDate(Date d, String startMode, long expectedBoundary)
72    {
73        // Given a date with a year start, find the Daylight onset
74        // and end.  The given date should be 1/1/xx in some year.
75
76        if (d.toString().indexOf(startMode) == -1)
77        {
78            logln("Error: " + startMode + " not present in " + d);
79        }
80
81        // Use a binary search, assuming that we have a Standard
82        // time at the midpoint.
83        long min = d.getTime();
84        long max = min + SIX_MONTHS;
85
86        while ((max - min) >  INTERVAL)
87        {
88            long mid = (min + max) >> 1;
89            String s = new Date(mid).toString();
90            // logln(s);
91            if (s.indexOf(startMode) != -1)
92            {
93                min = mid;
94            }
95            else
96            {
97                max = mid;
98            }
99        }
100
101        logln("Date Before: " + showDate(min));
102        logln("Date After:  " + showDate(max));
103        long mindelta = expectedBoundary - min;
104        long maxdelta = max - expectedBoundary;
105        if (mindelta >= 0 && mindelta <= INTERVAL &&
106            mindelta >= 0 && mindelta <= INTERVAL)
107            logln("PASS: Expected boundary at " + expectedBoundary);
108        else
109            errln("FAIL: Expected boundary at " + expectedBoundary);
110    }
111
112    void findDaylightBoundaryUsingTimeZone(Date d, boolean startsInDST, long expectedBoundary)
113    {
114        findDaylightBoundaryUsingTimeZone(d, startsInDST, expectedBoundary,
115                                          TimeZone.getDefault());
116    }
117
118    void findDaylightBoundaryUsingTimeZone(Date d, boolean startsInDST,
119                                           long expectedBoundary, TimeZone tz)
120    {
121        // Given a date with a year start, find the Daylight onset
122        // and end.  The given date should be 1/1/xx in some year.
123
124        // Use a binary search, assuming that we have a Standard
125        // time at the midpoint.
126        long min = d.getTime();
127        long max = min + SIX_MONTHS;
128
129        if (tz.inDaylightTime(d) != startsInDST)
130        {
131            errln("FAIL: " + tz.getID() + " inDaylightTime(" +
132                  d + ") != " + startsInDST);
133            startsInDST = !startsInDST; // Flip over; find the apparent value
134        }
135
136        if (tz.inDaylightTime(new Date(max)) == startsInDST)
137        {
138            errln("FAIL: " + tz.getID() + " inDaylightTime(" +
139                  (new Date(max)) + ") != " + (!startsInDST));
140            return;
141        }
142
143        while ((max - min) >  INTERVAL)
144        {
145            long mid = (min + max) >> 1;
146            boolean isIn = tz.inDaylightTime(new Date(mid));
147            if (isIn == startsInDST)
148            {
149                min = mid;
150            }
151            else
152            {
153                max = mid;
154            }
155        }
156
157        logln(tz.getID() + " Before: " + showDate(min, tz));
158        logln(tz.getID() + " After:  " + showDate(max, tz));
159
160        long mindelta = expectedBoundary - min;
161        long maxdelta = max - expectedBoundary;
162        if (mindelta >= 0 && mindelta <= INTERVAL &&
163            mindelta >= 0 && mindelta <= INTERVAL)
164            logln("PASS: Expected boundary at " + expectedBoundary);
165        else
166            errln("FAIL: Expected boundary at " + expectedBoundary);
167    }
168
169    private static String showDate(long l)
170    {
171        return showDate(new Date(l));
172    }
173
174    @SuppressWarnings("deprecation")
175    private static String showDate(Date d)
176    {
177        return "" + d.getYear() + "/" + showNN(d.getMonth()+1) + "/" + showNN(d.getDate()) +
178            " " + showNN(d.getHours()) + ":" + showNN(d.getMinutes()) +
179            " \"" + d + "\" = " +
180            d.getTime();
181    }
182
183    private static String showDate(long l, TimeZone z)
184    {
185        return showDate(new Date(l), z);
186    }
187
188    @SuppressWarnings("deprecation")
189    private static String showDate(Date d, TimeZone zone)
190    {
191        DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
192        fmt.setTimeZone(zone);
193        return "" + d.getYear() + "/" + showNN(d.getMonth()+1) + "/" + showNN(d.getDate()) +
194            " " + showNN(d.getHours()) + ":" + showNN(d.getMinutes()) +
195            " \"" + d + "\" = " +
196            fmt.format(d);
197    }
198
199    private static String showNN(int n)
200    {
201        return ((n < 10) ? "0" : "") + n;
202    }
203
204    /**
205     * Given a date, a TimeZone, and expected values for inDaylightTime,
206     * useDaylightTime, zone and DST offset, verify that this is the case.
207     */
208    void verifyDST(Date d, TimeZone time_zone,
209                   boolean expUseDaylightTime, boolean expInDaylightTime,
210                   int expZoneOffset, int expDSTOffset)
211    {
212        logln("-- Verifying time " + d +
213              " in zone " + time_zone.getID());
214
215        if (time_zone.inDaylightTime(d) == expInDaylightTime)
216            logln("PASS: inDaylightTime = " + time_zone.inDaylightTime(d));
217        else
218            errln("FAIL: inDaylightTime = " + time_zone.inDaylightTime(d));
219
220        if (time_zone.useDaylightTime() == expUseDaylightTime)
221            logln("PASS: useDaylightTime = " + time_zone.useDaylightTime());
222        else
223            errln("FAIL: useDaylightTime = " + time_zone.useDaylightTime());
224
225        if (time_zone.getRawOffset() == expZoneOffset)
226            logln("PASS: getRawOffset() = " + expZoneOffset/(double)ONE_HOUR);
227        else
228            errln("FAIL: getRawOffset() = " + time_zone.getRawOffset()/(double)ONE_HOUR +
229                  "; expected " + expZoneOffset/(double)ONE_HOUR);
230
231        GregorianCalendar gc = new GregorianCalendar(time_zone);
232        gc.setTime(d);
233        int offset = time_zone.getOffset(gc.get(gc.ERA), gc.get(gc.YEAR), gc.get(gc.MONTH),
234                                         gc.get(gc.DAY_OF_MONTH), gc.get(gc.DAY_OF_WEEK),
235                                         ((gc.get(gc.HOUR_OF_DAY) * 60 +
236                                           gc.get(gc.MINUTE)) * 60 +
237                                          gc.get(gc.SECOND)) * 1000 +
238                                         gc.get(gc.MILLISECOND));
239        if (offset == expDSTOffset)
240            logln("PASS: getOffset() = " + offset/(double)ONE_HOUR);
241        else
242            errln("FAIL: getOffset() = " + offset/(double)ONE_HOUR +
243                  "; expected " + expDSTOffset/(double)ONE_HOUR);
244    }
245
246    @SuppressWarnings("deprecation")
247    public void TestBoundaries()
248    {
249        TimeZone pst = TimeZone.getTimeZone("PST");
250        TimeZone save = TimeZone.getDefault();
251        try {
252            TimeZone.setDefault(pst);
253
254            // DST changeover for PST is 4/6/1997 at 2 hours past midnight
255            Date d = new Date(97,Calendar.APRIL,6);
256
257            // i is minutes past midnight standard time
258            for (int i=60; i<=180; i+=15)
259            {
260                boolean inDST = (i >= 120);
261                Date e = new Date(d.getTime() + i*60*1000);
262                verifyDST(e, pst, true, inDST, -8*ONE_HOUR,
263                          inDST ? -7*ONE_HOUR : -8*ONE_HOUR);
264            }
265
266            logln("========================================");
267            findDaylightBoundaryUsingDate(new Date(97,0,1), "PST", PST_1997_BEG);
268            logln("========================================");
269            findDaylightBoundaryUsingDate(new Date(97,6,1), "PDT", PST_1997_END);
270
271            // Southern hemisphere test
272            logln("========================================");
273            TimeZone z = TimeZone.getTimeZone(AUSTRALIA);
274            findDaylightBoundaryUsingTimeZone(new Date(97,0,1), true, AUSTRALIA_1997_END, z);
275
276            logln("========================================");
277            findDaylightBoundaryUsingTimeZone(new Date(97,0,1), false, PST_1997_BEG);
278            logln("========================================");
279            findDaylightBoundaryUsingTimeZone(new Date(97,6,1), true, PST_1997_END);
280        } finally {
281            TimeZone.setDefault(save);
282        }
283    }
284
285    void testUsingBinarySearch(SimpleTimeZone tz, Date d, long expectedBoundary)
286    {
287        // Given a date with a year start, find the Daylight onset
288        // and end.  The given date should be 1/1/xx in some year.
289
290        // Use a binary search, assuming that we have a Standard
291        // time at the midpoint.
292        long min = d.getTime();
293        long max = min + (long)(365.25 / 2 * ONE_DAY);
294
295        // First check the boundaries
296        boolean startsInDST = tz.inDaylightTime(d);
297
298        if (tz.inDaylightTime(new Date(max)) == startsInDST)
299        {
300            logln("Error: inDaylightTime(" + (new Date(max)) + ") != " + (!startsInDST));
301        }
302
303        while ((max - min) >  INTERVAL)
304        {
305            long mid = (min + max) >> 1;
306            if (tz.inDaylightTime(new Date(mid)) == startsInDST)
307            {
308                min = mid;
309            }
310            else
311            {
312                max = mid;
313            }
314        }
315
316        logln("Binary Search Before: " + showDate(min));
317        logln("Binary Search After:  " + showDate(max));
318
319        long mindelta = expectedBoundary - min;
320        long maxdelta = max - expectedBoundary;
321        if (mindelta >= 0 && mindelta <= INTERVAL &&
322            mindelta >= 0 && mindelta <= INTERVAL)
323            logln("PASS: Expected boundary at " + expectedBoundary);
324        else
325            errln("FAIL: Expected boundary at " + expectedBoundary);
326    }
327
328    /*
329      static void testUsingMillis(Date d, boolean startsInDST)
330      {
331      long millis = d.getTime();
332      long max = millis + (long)(370 * ONE_DAY); // A year plus extra
333
334      boolean lastDST = startsInDST;
335      while (millis < max)
336      {
337      cal.setTime(new Date(millis));
338      boolean inDaylight = cal.inDaylightTime();
339
340      if (inDaylight != lastDST)
341      {
342      logln("Switch " + (inDaylight ? "into" : "out of")
343      + " DST at " + (new Date(millis)));
344      lastDST = inDaylight;
345      }
346
347      millis += 15*ONE_MINUTE;
348      }
349      }
350      */
351
352    /**
353     * Test new rule formats.
354     */
355    @SuppressWarnings("deprecation")
356    public void TestNewRules()
357    {
358        //logln(Locale.getDefault().getDisplayName());
359        //logln(TimeZone.getDefault().getID());
360        //logln(new Date(0));
361
362        if (true)
363        {
364            // Doesn't matter what the default TimeZone is here, since we
365            // are creating our own TimeZone objects.
366
367            SimpleTimeZone tz;
368
369            logln("-----------------------------------------------------------------");
370            logln("Aug 2ndTues .. Mar 15");
371            tz = new SimpleTimeZone(-8*ONE_HOUR, "Test_1",
372                                    Calendar.AUGUST, 2, Calendar.TUESDAY, 2*ONE_HOUR,
373                                    Calendar.MARCH, 15, 0, 2*ONE_HOUR);
374            //logln(tz.toString());
375            logln("========================================");
376            testUsingBinarySearch(tz, new Date(97,0,1), 858416400000L);
377            logln("========================================");
378            testUsingBinarySearch(tz, new Date(97,6,1), 871380000000L);
379
380            logln("-----------------------------------------------------------------");
381            logln("Apr Wed>=14 .. Sep Sun<=20");
382            tz = new SimpleTimeZone(-8*ONE_HOUR, "Test_2",
383                                    Calendar.APRIL, 14, -Calendar.WEDNESDAY, 2*ONE_HOUR,
384                                    Calendar.SEPTEMBER, -20, -Calendar.SUNDAY, 2*ONE_HOUR);
385            //logln(tz.toString());
386            logln("========================================");
387            testUsingBinarySearch(tz, new Date(97,0,1), 861184800000L);
388            logln("========================================");
389            testUsingBinarySearch(tz, new Date(97,6,1), 874227600000L);
390        }
391
392        /*
393          if (true)
394          {
395          logln("========================================");
396          logln("Stepping using millis");
397          testUsingMillis(new Date(97,0,1), false);
398          }
399
400          if (true)
401          {
402          logln("========================================");
403          logln("Stepping using fields");
404          testUsingFields(1997, false);
405          }
406
407          if (false)
408          {
409          cal.clear();
410          cal.set(1997, 3, 5, 10, 0);
411          //    cal.inDaylightTime();
412          logln("Date = " + cal.getTime());
413          logln("Millis = " + cal.getTime().getTime()/3600000);
414          }
415          */
416    }
417
418    //----------------------------------------------------------------------
419    //----------------------------------------------------------------------
420    //----------------------------------------------------------------------
421    // Long Bug
422    //----------------------------------------------------------------------
423    //----------------------------------------------------------------------
424    //----------------------------------------------------------------------
425
426    //public void Test3()
427    //{
428    //    findDaylightBoundaryUsingTimeZone(new Date(97,6,1), true);
429    //}
430
431    /**
432     * Find boundaries by stepping.
433     */
434    @SuppressWarnings("deprecation")
435    void findBoundariesStepwise(int year, long interval, TimeZone z, int expectedChanges)
436    {
437        Date d = new Date(year - 1900, Calendar.JANUARY, 1);
438        long time = d.getTime(); // ms
439        long limit = time + ONE_YEAR + ONE_DAY;
440        boolean lastState = z.inDaylightTime(d);
441        int changes = 0;
442        logln("-- Zone " + z.getID() + " starts in " + year + " with DST = " + lastState);
443        logln("useDaylightTime = " + z.useDaylightTime());
444        while (time < limit)
445        {
446            d.setTime(time);
447            boolean state = z.inDaylightTime(d);
448            if (state != lastState)
449            {
450                logln((state ? "Entry " : "Exit ") +
451                      "at " + d);
452                lastState = state;
453                ++changes;
454            }
455            time += interval;
456        }
457        if (changes == 0)
458        {
459            if (!lastState && !z.useDaylightTime()) logln("No DST");
460            else errln("FAIL: Timezone<" + z.getID() + "> DST all year, or no DST with true useDaylightTime");
461        }
462        else if (changes != 2)
463        {
464            errln("FAIL: Timezone<" + z.getID() + "> " + changes + " changes seen; should see 0 or 2");
465        }
466        else if (!z.useDaylightTime())
467        {
468            errln("FAIL: Timezone<" + z.getID() + "> useDaylightTime false but 2 changes seen");
469        }
470        if (changes != expectedChanges)
471        {
472            errln("FAIL: Timezone<" + z.getID() + "> " + changes + " changes seen; expected " + expectedChanges);
473        }
474    }
475
476    public void TestStepwise()
477    {
478        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("ACT"), 0);
479        // "EST" is disabled because its behavior depends on the mapping property. (6466476).
480        //findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("EST"), 2);
481        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("HST"), 0);
482        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("PST"), 2);
483        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("PST8PDT"), 2);
484        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("SystemV/PST"), 0);
485        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("SystemV/PST8PDT"), 2);
486        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("Japan"), 0);
487        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("Europe/Paris"), 2);
488        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("America/Los_Angeles"), 2);
489        findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone(AUSTRALIA), 2);
490    }
491}
492