1 /*
  2  * Copyright (c) 1997, 2023, 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  * @summary test Time Zone Boundary
 27  * @run junit TimeZoneBoundaryTest
 28  */
 29 
 30 import java.text.DateFormat;
 31 import java.util.Calendar;
 32 import java.util.Date;
 33 import java.util.GregorianCalendar;
 34 import java.util.SimpleTimeZone;
 35 import java.util.TimeZone;
 36 
 37 import org.junit.jupiter.api.Test;
 38 
 39 import static org.junit.jupiter.api.Assertions.fail;
 40 
 41 /**
 42  * A test which discovers the boundaries of DST programmatically and verifies
 43  * that they are correct.
 44  */
 45 public class TimeZoneBoundaryTest
 46 {
 47     static final int ONE_SECOND = 1000;
 48     static final int ONE_MINUTE = 60*ONE_SECOND;
 49     static final int ONE_HOUR = 60*ONE_MINUTE;
 50     static final long ONE_DAY = 24*ONE_HOUR;
 51     static final long ONE_YEAR = (long)(365.25 * ONE_DAY);
 52     static final long SIX_MONTHS = ONE_YEAR / 2;
 53 
 54     static final int MONTH_LENGTH[] = {31,29,31,30,31,30,31,31,30,31,30,31};
 55 
 56     // These values are empirically determined to be correct
 57     static final long PST_1997_BEG  = 860320800000L;
 58     static final long PST_1997_END  = 877856400000L;
 59 
 60     // Minimum interval for binary searches in ms; should be no larger
 61     // than 1000.
 62     static final long INTERVAL = 10; // Milliseconds
 63 
 64     static final String AUSTRALIA = "Australia/Adelaide";
 65     static final long AUSTRALIA_1997_BEG = 877797000000L;
 66     static final long AUSTRALIA_1997_END = 859653000000L;
 67 
 68     /**
 69      * Date.toString().substring() Boundary Test
 70      * Look for a DST changeover to occur within 6 months of the given Date.
 71      * The initial Date.toString() should yield a string containing the
 72      * startMode as a SUBSTRING.  The boundary will be tested to be
 73      * at the expectedBoundary value.
 74      */
 75     void findDaylightBoundaryUsingDate(Date d, String startMode, long expectedBoundary)
 76     {
 77         // Given a date with a year start, find the Daylight onset
 78         // and end.  The given date should be 1/1/xx in some year.
 79 
 80         if (d.toString().indexOf(startMode) == -1)
 81         {
 82             System.out.println("Error: " + startMode + " not present in " + d);
 83         }
 84 
 85         // Use a binary search, assuming that we have a Standard
 86         // time at the midpoint.
 87         long min = d.getTime();
 88         long max = min + SIX_MONTHS;
 89 
 90         while ((max - min) >  INTERVAL)
 91         {
 92             long mid = (min + max) >> 1;
 93             String s = new Date(mid).toString();
 94             // logln(s);
 95             if (s.indexOf(startMode) != -1)
 96             {
 97                 min = mid;
 98             }
 99             else
100             {
101                 max = mid;
102             }
103         }
104 
105         System.out.println("Date Before: " + showDate(min));
106         System.out.println("Date After:  " + showDate(max));
107         long mindelta = expectedBoundary - min;
108         long maxdelta = max - expectedBoundary;
109         if (mindelta >= 0 && mindelta <= INTERVAL &&
110             mindelta >= 0 && mindelta <= INTERVAL)
111             System.out.println("PASS: Expected boundary at " + expectedBoundary);
112         else
113             fail("FAIL: Expected boundary at " + expectedBoundary);
114     }
115 
116     void findDaylightBoundaryUsingTimeZone(Date d, boolean startsInDST, long expectedBoundary)
117     {
118         findDaylightBoundaryUsingTimeZone(d, startsInDST, expectedBoundary,
119                                           TimeZone.getDefault());
120     }
121 
122     void findDaylightBoundaryUsingTimeZone(Date d, boolean startsInDST,
123                                            long expectedBoundary, TimeZone tz)
124     {
125         // Given a date with a year start, find the Daylight onset
126         // and end.  The given date should be 1/1/xx in some year.
127 
128         // Use a binary search, assuming that we have a Standard
129         // time at the midpoint.
130         long min = d.getTime();
131         long max = min + SIX_MONTHS;
132 
133         if (tz.inDaylightTime(d) != startsInDST)
134         {
135             fail("FAIL: " + tz.getID() + " inDaylightTime(" +
136                   d + ") != " + startsInDST);
137             startsInDST = !startsInDST; // Flip over; find the apparent value
138         }
139 
140         if (tz.inDaylightTime(new Date(max)) == startsInDST)
141         {
142             fail("FAIL: " + tz.getID() + " inDaylightTime(" +
143                   (new Date(max)) + ") != " + (!startsInDST));
144             return;
145         }
146 
147         while ((max - min) >  INTERVAL)
148         {
149             long mid = (min + max) >> 1;
150             boolean isIn = tz.inDaylightTime(new Date(mid));
151             if (isIn == startsInDST)
152             {
153                 min = mid;
154             }
155             else
156             {
157                 max = mid;
158             }
159         }
160 
161         System.out.println(tz.getID() + " Before: " + showDate(min, tz));
162         System.out.println(tz.getID() + " After:  " + showDate(max, tz));
163 
164         long mindelta = expectedBoundary - min;
165         long maxdelta = max - expectedBoundary;
166         if (mindelta >= 0 && mindelta <= INTERVAL &&
167             mindelta >= 0 && mindelta <= INTERVAL)
168             System.out.println("PASS: Expected boundary at " + expectedBoundary);
169         else
170             fail("FAIL: Expected boundary at " + expectedBoundary);
171     }
172 
173     private static String showDate(long l)
174     {
175         return showDate(new Date(l));
176     }
177 
178     @SuppressWarnings("deprecation")
179     private static String showDate(Date d)
180     {
181         return "" + d.getYear() + "/" + showNN(d.getMonth()+1) + "/" + showNN(d.getDate()) +
182             " " + showNN(d.getHours()) + ":" + showNN(d.getMinutes()) +
183             " \"" + d + "\" = " +
184             d.getTime();
185     }
186 
187     private static String showDate(long l, TimeZone z)
188     {
189         return showDate(new Date(l), z);
190     }
191 
192     @SuppressWarnings("deprecation")
193     private static String showDate(Date d, TimeZone zone)
194     {
195         DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
196         fmt.setTimeZone(zone);
197         return "" + d.getYear() + "/" + showNN(d.getMonth()+1) + "/" + showNN(d.getDate()) +
198             " " + showNN(d.getHours()) + ":" + showNN(d.getMinutes()) +
199             " \"" + d + "\" = " +
200             fmt.format(d);
201     }
202 
203     private static String showNN(int n)
204     {
205         return ((n < 10) ? "0" : "") + n;
206     }
207 
208     /**
209      * Given a date, a TimeZone, and expected values for inDaylightTime,
210      * useDaylightTime, zone and DST offset, verify that this is the case.
211      */
212     void verifyDST(Date d, TimeZone time_zone,
213                    boolean expUseDaylightTime, boolean expInDaylightTime,
214                    int expZoneOffset, int expDSTOffset)
215     {
216         System.out.println("-- Verifying time " + d +
217               " in zone " + time_zone.getID());
218 
219         if (time_zone.inDaylightTime(d) == expInDaylightTime)
220             System.out.println("PASS: inDaylightTime = " + time_zone.inDaylightTime(d));
221         else
222             fail("FAIL: inDaylightTime = " + time_zone.inDaylightTime(d));
223 
224         if (time_zone.useDaylightTime() == expUseDaylightTime)
225             System.out.println("PASS: useDaylightTime = " + time_zone.useDaylightTime());
226         else
227             fail("FAIL: useDaylightTime = " + time_zone.useDaylightTime());
228 
229         if (time_zone.getRawOffset() == expZoneOffset)
230             System.out.println("PASS: getRawOffset() = " + expZoneOffset/(double)ONE_HOUR);
231         else
232             fail("FAIL: getRawOffset() = " + time_zone.getRawOffset()/(double)ONE_HOUR +
233                   "; expected " + expZoneOffset/(double)ONE_HOUR);
234 
235         GregorianCalendar gc = new GregorianCalendar(time_zone);
236         gc.setTime(d);
237         int offset = time_zone.getOffset(gc.get(gc.ERA), gc.get(gc.YEAR), gc.get(gc.MONTH),
238                                          gc.get(gc.DAY_OF_MONTH), gc.get(gc.DAY_OF_WEEK),
239                                          ((gc.get(gc.HOUR_OF_DAY) * 60 +
240                                            gc.get(gc.MINUTE)) * 60 +
241                                           gc.get(gc.SECOND)) * 1000 +
242                                          gc.get(gc.MILLISECOND));
243         if (offset == expDSTOffset)
244             System.out.println("PASS: getOffset() = " + offset/(double)ONE_HOUR);
245         else
246             fail("FAIL: getOffset() = " + offset/(double)ONE_HOUR +
247                   "; expected " + expDSTOffset/(double)ONE_HOUR);
248     }
249 
250     @SuppressWarnings("deprecation")
251     @Test
252     public void TestBoundaries()
253     {
254         TimeZone pst = TimeZone.getTimeZone("PST");
255         TimeZone save = TimeZone.getDefault();
256         try {
257             TimeZone.setDefault(pst);
258 
259             // DST changeover for PST is 4/6/1997 at 2 hours past midnight
260             Date d = new Date(97,Calendar.APRIL,6);
261 
262             // i is minutes past midnight standard time
263             for (int i=60; i<=180; i+=15)
264             {
265                 boolean inDST = (i >= 120);
266                 Date e = new Date(d.getTime() + i*60*1000);
267                 verifyDST(e, pst, true, inDST, -8*ONE_HOUR,
268                           inDST ? -7*ONE_HOUR : -8*ONE_HOUR);
269             }
270 
271             System.out.println("========================================");
272             findDaylightBoundaryUsingDate(new Date(97,0,1), "PST", PST_1997_BEG);
273             System.out.println("========================================");
274             findDaylightBoundaryUsingDate(new Date(97,6,1), "PDT", PST_1997_END);
275 
276             // Southern hemisphere test
277             System.out.println("========================================");
278             TimeZone z = TimeZone.getTimeZone(AUSTRALIA);
279             findDaylightBoundaryUsingTimeZone(new Date(97,0,1), true, AUSTRALIA_1997_END, z);
280 
281             System.out.println("========================================");
282             findDaylightBoundaryUsingTimeZone(new Date(97,0,1), false, PST_1997_BEG);
283             System.out.println("========================================");
284             findDaylightBoundaryUsingTimeZone(new Date(97,6,1), true, PST_1997_END);
285         } finally {
286             TimeZone.setDefault(save);
287         }
288     }
289 
290     void testUsingBinarySearch(SimpleTimeZone tz, Date d, long expectedBoundary)
291     {
292         // Given a date with a year start, find the Daylight onset
293         // and end.  The given date should be 1/1/xx in some year.
294 
295         // Use a binary search, assuming that we have a Standard
296         // time at the midpoint.
297         long min = d.getTime();
298         long max = min + (long)(365.25 / 2 * ONE_DAY);
299 
300         // First check the boundaries
301         boolean startsInDST = tz.inDaylightTime(d);
302 
303         if (tz.inDaylightTime(new Date(max)) == startsInDST)
304         {
305             System.out.println("Error: inDaylightTime(" + (new Date(max)) + ") != " + (!startsInDST));
306         }
307 
308         while ((max - min) >  INTERVAL)
309         {
310             long mid = (min + max) >> 1;
311             if (tz.inDaylightTime(new Date(mid)) == startsInDST)
312             {
313                 min = mid;
314             }
315             else
316             {
317                 max = mid;
318             }
319         }
320 
321         System.out.println("Binary Search Before: " + showDate(min));
322         System.out.println("Binary Search After:  " + showDate(max));
323 
324         long mindelta = expectedBoundary - min;
325         long maxdelta = max - expectedBoundary;
326         if (mindelta >= 0 && mindelta <= INTERVAL &&
327             mindelta >= 0 && mindelta <= INTERVAL)
328             System.out.println("PASS: Expected boundary at " + expectedBoundary);
329         else
330             fail("FAIL: Expected boundary at " + expectedBoundary);
331     }
332 
333     /**
334      * Test new rule formats.
335      */
336     @SuppressWarnings("deprecation")
337     @Test
338     public void TestNewRules() {
339         // Doesn't matter what the default TimeZone is here, since we
340         // are creating our own TimeZone objects.
341 
342         SimpleTimeZone tz;
343 
344         System.out.println("-----------------------------------------------------------------");
345         System.out.println("Aug 2ndTues .. Mar 15");
346         tz = new SimpleTimeZone(-8*ONE_HOUR, "Test_1",
347                                 Calendar.AUGUST, 2, Calendar.TUESDAY, 2*ONE_HOUR,
348                                 Calendar.MARCH, 15, 0, 2*ONE_HOUR);
349         System.out.println("========================================");
350         testUsingBinarySearch(tz, new Date(97,0,1), 858416400000L);
351         System.out.println("========================================");
352         testUsingBinarySearch(tz, new Date(97,6,1), 871380000000L);
353 
354         System.out.println("-----------------------------------------------------------------");
355         System.out.println("Apr Wed>=14 .. Sep Sun<=20");
356         tz = new SimpleTimeZone(-8*ONE_HOUR, "Test_2",
357                                 Calendar.APRIL, 14, -Calendar.WEDNESDAY, 2*ONE_HOUR,
358                                 Calendar.SEPTEMBER, -20, -Calendar.SUNDAY, 2*ONE_HOUR);
359         System.out.println("========================================");
360         testUsingBinarySearch(tz, new Date(97,0,1), 861184800000L);
361         System.out.println("========================================");
362         testUsingBinarySearch(tz, new Date(97,6,1), 874227600000L);
363     }
364 
365     /**
366      * Find boundaries by stepping.
367      */
368     @SuppressWarnings("deprecation")
369     void findBoundariesStepwise(int year, long interval, TimeZone z, int expectedChanges)
370     {
371         Date d = new Date(year - 1900, Calendar.JANUARY, 1);
372         long time = d.getTime(); // ms
373         long limit = time + ONE_YEAR + ONE_DAY;
374         boolean lastState = z.inDaylightTime(d);
375         int changes = 0;
376         System.out.println("-- Zone " + z.getID() + " starts in " + year + " with DST = " + lastState);
377         System.out.println("useDaylightTime = " + z.useDaylightTime());
378         while (time < limit)
379         {
380             d.setTime(time);
381             boolean state = z.inDaylightTime(d);
382             if (state != lastState)
383             {
384                 System.out.println((state ? "Entry " : "Exit ") +
385                       "at " + d);
386                 lastState = state;
387                 ++changes;
388             }
389             time += interval;
390         }
391         if (changes == 0)
392         {
393             if (!lastState && !z.useDaylightTime()) System.out.println("No DST");
394             else fail("FAIL: Timezone<" + z.getID() + "> DST all year, or no DST with true useDaylightTime");
395         }
396         else if (changes != 2)
397         {
398             fail("FAIL: Timezone<" + z.getID() + "> " + changes + " changes seen; should see 0 or 2");
399         }
400         else if (!z.useDaylightTime())
401         {
402             fail("FAIL: Timezone<" + z.getID() + "> useDaylightTime false but 2 changes seen");
403         }
404         if (changes != expectedChanges)
405         {
406             fail("FAIL: Timezone<" + z.getID() + "> " + changes + " changes seen; expected " + expectedChanges);
407         }
408     }
409 
410     @Test
411     public void TestStepwise()
412     {
413         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("ACT"), 0);
414         // "EST" is disabled because its behavior depends on the mapping property. (6466476).
415         //findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("EST"), 2);
416         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("HST"), 0);
417         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("PST"), 2);
418         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("PST8PDT"), 2);
419         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("SystemV/PST"), 0);
420         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("SystemV/PST8PDT"), 2);
421         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("Japan"), 0);
422         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("Europe/Paris"), 2);
423         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone("America/Los_Angeles"), 2);
424         findBoundariesStepwise(1997, ONE_DAY, TimeZone.getTimeZone(AUSTRALIA), 2);
425     }
426 }